UPI_Without_Internet: Why the Payment Isn’t the Packet, and the Packet Isn’t the Trust Boundary

A mesh-network payment demo that encrypts money before it hops phones, dedupes by ciphertext hash, and settles only when a bridge node reaches the server.

7 to 9 min read • View on GitHub • More from perryvegehan

A chain of phones passes a sealed packet through a dim offline corridor, while one phone at the far end shows a live signal and acts as the bridge node. The image explains the core idea that payment data can move through untrusted relays and still reach the server later.
The payment is not trusted in transit. Only the cryptography is.

AIR 546, GATE 21 CS Computer Science Undergrad.

Pratyush Narain, Creator / Maintainer · perryvegehan (Pratyush Narain) · GitHub
Key Takeaways

The internet is optional. Trust is not.

This repo is not trying to make connectivity pretty. It assumes the ugly case: a payment instruction has to survive a path of offline devices, and none of those devices should be trusted with the payload. That changes the whole design. The system behaves less like a normal app flow and more like a delay-tolerant network for money.

The key move is the bridge node. Most devices in the mesh only relay packets. Only one device, eventually, needs a route back to the server. That is the whole mental shift: the network does not need to be online everywhere. It only needs one honest path to appear later.

A hedcut-style portrait of Pratyush Narain based on his GitHub avatar. The portrait identifies the maintainer behind the project and anchors the article’s source material in a verified reference image.

The real identity of a payment lives in the ciphertext

The security stack is straightforward in the best way. A random AES key encrypts the payment instruction, and that AES key is wrapped with the server’s RSA public key. The payload then rides through the mesh as an opaque blob, protected by AES-GCM so any tampering breaks authentication at the bridge.

The unusual part is not the encryption. It is the identity rule. Instead of trusting a user-supplied packet ID, the system hashes the encrypted blob itself and uses that SHA-256 value as the idempotency key. That means the thing being deduped is exactly the thing that was actually relayed, not a label attached to it by an untrusted device.

This is the project’s central mental model. The mesh only forwards opaque packets. The bridge is the first place where the server can claim, decrypt, and decide what happens next.

A close-up of a steel capsule wrapped in a hash seal, with a spoofable label floating nearby and a malicious hand trying to swap it without opening the capsule. The image explains why the encrypted blob itself becomes the idempotency key.
If the ciphertext changes, the identity changes. That is the point.

Why the mesh does not need to know the payment

The mesh simulator is the easiest part to misunderstand. It is not the product. It is a proof that the trust boundary is where the packet is decrypted, not where it hops. Devices gossip packets forward, TTL counts down, and the packet can survive multiple relays before a bridge ever sees the wire format.

That separation matters because relay devices do not need the private key. They can move the packet without learning the payment, and they can do it without becoming part of the trust chain. In other words, the mesh is only a transport layer, not a witness.

The bridge node is where offline turns into ledger state

BridgeIngestionService is where the demo stops being a networking exercise and starts looking like a payment system. It claims the ciphertext hash first, before decryption, so duplicate uploads do not race into the same settlement path. Then it decrypts, checks freshness against the instruction timestamp, and rejects stale packets that have been sitting around too long.

That order is deliberate. Claim first. Decrypt second. Decide third. It keeps the idempotency boundary outside the payload, which is exactly what you want when multiple bridge nodes can flush uploads in parallel.

// Bridge ingestion pattern
String hash = sha256(ciphertext);
if (!idempotencyService.claim(hash)) {
    return duplicate();
}
PaymentInstruction pi = crypto.decrypt(ciphertext);
if (isExpired(pi.getSignedAt(), maxAgeSeconds)) {
    return stale();
}
return settlementService.settle(pi);
AxisUPI_Without_Internet meshUSSD automation / *99#Official offline rails like UPI Lite X or UPI 123PAYManual deferred sync systems
Connectivity requirementNo continuous network path; only one eventual bridge needs internetNeeds GSM signaling and an active sessionNeeds the supported offline rail and compatible devices or channelsUsually assumes a later manual sync or upload path
Payload visibilityRelays see opaque ciphertext and limited metadataMenu steps and session state are exposed to the automation layerDepends on the rail, but routing is not the core problemOften visible inside local app state until sync
Offline durationDesigned for multi-hop delay tolerant deliveryShort, fragile sessions can time outShort to moderate offline useDepends on how long the local store can buffer
Primary trust boundaryAt the bridge node, after claim and decryptAt the telecom session and app automation layerAt the official payment railAt the eventual sync or reconciliation step
Failure modeTampering, duplicate upload, or stale packet rejectionSession timeout or automation failureDevice and rail compatibility limitsMerge conflicts and inconsistent local state
Best use casePlaces with no reliable path to the network at allUsers who still have a GSM link but need a simpler flowMainstream offline taps or low-friction feature-phone accessApplications that can tolerate manual reconciliation

The implementation also goes one step further on the ledger side. The Account entity uses optimistic locking, which means the system is prepared for parallel bridge uploads instead of pretending they will never happen. That is the right kind of paranoia for money.

The ledger still has to survive race conditions

This is where the prototype earns credibility. A toy mesh demo would stop at packet forwarding. This repo keeps going into settlement behavior, which is where concurrency bugs become financial bugs. Optimistic locking gives the account balance a versioned guardrail when two bridge nodes try to write at once.

The result is not a full production rail. It is something more useful for a repo explainer: a compact example of how you design for replay, duplication, and simultaneous ingestion without pretending the offline world will behave politely.

What this project is actually competing with

The cleanest comparison is not against a normal online payment app. It is against other ways to survive weak or missing connectivity. Some solutions lean on GSM and session automation. Others lean on official offline rails. This project leans on store-and-forward mesh delivery, which is a different answer to a different failure mode.

ApproachStrengthWeaknessWhere it fits
Mesh relay with bridge uploadWorks even when no direct network path existsNeeds cryptography, dedupe, and a bridge laterBasements, remote sites, emergency connectivity
USSD automationUses the existing telecom signaling pathShort sessions and brittle flowsUsers who still have GSM access
Official offline railsLowest friction for supported devicesRequires platform support and policy constraintsMainstream consumer payments
Manual deferred syncSimple to buildEasy to drift into inconsistent stateInternal tools and non-financial workflows

That comparison is the real story. This repo is not trying to win every offline payments scenario. It is showing what you build when you cannot assume a stable network path, cannot trust relay nodes, and still need the eventual settlement to be consistent.

The prototype is simple. The idea is durable.

The stack is intentionally lightweight. Spring Boot, H2, JPA, and a server-rendered dashboard keep the demo easy to run. But the ideas underneath it are durable: authenticated encryption, ciphertext-based idempotency, freshness checks, and optimistic locking are not demo-only tricks.

That is why the project is interesting. It treats every untrusted hop as a fact of life, not a special case. The payment is not the packet, and the packet is not the trust boundary. The trust boundary is where the server can finally verify what the mesh only carried.