At 4:23 PM on Saturday, a teammate's iPhone signed a $0.25 sandbox payment with its Secure Enclave key and passed it by camera QR to a shop's iPhone, which held it offline. When the shop's phone came back online, Solana devnet checked the signature and paid the shop. At 4:31 PM we signed the same allowance and serial again for a different shop's phone, and the chain refused it with SerialUsed, error 6011. We ran it again at 5:05 and 5:09 PM with the same result. All four are public transactions you can open below.

That refusal is the check we built LOKA Pay around. LOKA Pay is a phone wallet designed so a shop can take a payment with zero bars and settle it on Solana devnet once its phone gets signal back. zerobars.us is LOKA Pay's site.

Try it (no login)

Status, Sat 6:40 PM. The full loop runs on our own iPhones on TestFlight build 4: a payer pays with no signal, the shop's phone settles the payment when it finds signal, and a second spend of the same serial was refused on chain both times we tried it. Build 5, with a faster QR scanner, is on TestFlight too. Judges can't install the app yet because Apple's external beta review of our first build has been waiting since Sat 12:05 AM, so the demo runs on our phones. Payments move between the phones by camera QR. Bluetooth delivery hasn't run between two phones yet. Everything runs on sandbox test tokens on devnet. No real money moves.

Inspiration

Most ways to pay with a phone need a network at the moment you pay, on your side or the shop's. In a storm, that moment might have no network at all.

During Hurricane Helene, the FCC reported 17.7% of cell sites out of service in the areas of Georgia the storm hit (FCC status report, Oct 2, 2024). The FCC also says a site being down doesn't always mean nobody in that area has service. If a payment can't reach a server, the shop owner either hands over the goods on a promise or loses the sale.

Our teammate Khadim co-founded LOKA, a messenger that sends end-to-end encrypted texts phone to phone over Bluetooth with no signal at all. So the question for this weekend was pretty simple. If a text can move with zero bars, why can't a payment?

The piece that makes this work is the chip in an iPhone (the Secure Enclave). It signs with a curve called P-256, and Solana can now check P-256 signatures directly through its secp256r1 precompile. So a key made inside the phone's own chip can be the thing the chain trusts, and that key never leaves the chip.

What it does

We entered the Fintech track to go after one case: paying when there's no network at all. Here's how it works, step by step. All six steps have run on our own iPhones, with the note moving by camera QR.

  1. Before you lose signal, you lock a spending cap into an allowance on Solana. You can never settle more than that cap.
  2. To pay, you confirm with Face ID or your passcode, and your iPhone's Secure Enclave signs a small payment note (141 bytes).
  3. The note goes to the shop's phone by QR code or Bluetooth.
  4. The shop's phone shows "Received, pending (sandbox)". Each allowance carries an offline limit signed into its certificate, and the shop's phone refuses any note that would push what that payer owes it, unsettled, past that limit.
  5. When the shop's phone gets signal, our settlement server (api.zerobars.us, on a Vultr VPS) sends the note to Solana. Our program checks the P-256 signature through the secp256r1 precompile, marks that note's serial as spent, and credits the shop's balance from the vault.
  6. If anyone reuses a serial, the chain refuses it with SerialUsed. The app has a "Try to cheat" control that re-signs the last note's allowance and serial for a second shop. We ran it on our iPhones twice on Saturday, at 4:31 and 5:09 PM, and the chain refused it both times with error 6011. On the first run the other shop's phone showed Failed (already spent).

We designed this for the shop owner at the moment the network drops. The goal is that they hand over the goods and still see the payment land once their phone finds signal.

How we built it

  • iPhone: Swift and SwiftUI with CryptoKit. The signing key is created in the Secure Enclave, Face ID or the passcode is required to use it, and there's no software fallback. Pay and Receive do camera QR both ways. Bluetooth send and receive are wired into LOKA's mesh in code but haven't delivered a payment between two phones yet. Every money screen has a named error state and a "No internet" chip.
  • Solana program: Rust and Anchor (anchor-lang 1.1.2) on devnet. Each allowance has 256 serials tracked in an on-chain bitmap, which is how a reused serial gets caught. The secp256r1 precompile has been active on mainnet since slot 345600000, so the core check isn't a devnet-only feature.
  • Protocol: one spec plus one shared file of test vectors that the Swift, Kotlin, JavaScript and Rust code all have to pass in CI. A note is 141 bytes, or 238 on the wire once the signature and public key are attached.
  • Site: Next.js, React and Tailwind on Vercel. /live streams devnet and /verify checks signatures right in your browser, and /proof reads devnet when the page loads. None of them need a login.
  • API: api.zerobars.us runs on a Vultr server and answers /api/health with the cluster, program id and current slot. The settle, registration and remittance (/send) routes are merged and deployed from main; /api/health shows the commit it runs.
  • Android: a P-256 signer on the Android Keystore (StrongBox when the phone has it) is merged but hasn't run on a device.
  • Voice call: zerobars.us/call reads a real settled payment from devnet, ElevenLabs (eleven_v4) turns it into speech, and the Vonage Voice API calls a US number. The number is never stored, and a desk code from our table is required.
  • CI: GitHub Actions runs iOS, Android, web (Playwright), the Anchor program tests on a local validator that first checks secp256r1 is active, and a hygiene job with gitleaks.
  • Review: we used AI coding assistants while building. We checked their work against the spec and the shared test vectors, and ran Codex and Greptile as reviewers on our pull requests, some of them for several rounds.

Built before Hacklanta (disclosure). LOKA v0.6.66, the offline Bluetooth mesh messenger Khadim co-founded, was imported as one commit tagged pre-hacklanta-foundation (f681863). A pre-event kit (README skeleton, plan, task files, CI drafts, the fact sheet keys and the import-scrub tools) is tagged pre-hacklanta-kit (259d53f). We also did planning outside the repo before the event, including a Wolof voice test with ElevenLabs that we threw out. Everything payments-related came after those tags: the Solana program, the payment code on iPhone and Android, the protocol and test vectors, the website, and the settlement server. Every commit made during hacking: https://github.com/StephenSook/loka-pay/compare/pre-hacklanta-kit...main

Challenges we ran into

The big one is double spending. A phone with no signal can't ask anyone whether a note was already spent, and nothing offline can fully stop that. So we bound it instead of pretending to prevent it. The payer can never settle more than their cap. Each allowance carries an offline limit, and the shop's phone refuses notes past it, so the risk a shop carries from one payer while offline has a fixed ceiling. And when someone does try a second spend, the chain refuses it. We wrote all of this down in plain words at https://zerobars.us/trust, along with who holds which key.

Every note is 238 bytes on the wire and needs its own signature check, so a settle transaction can't carry many notes.

Bluetooth on iPhone has real limits, like range and how little an app can do in the background. That's why the app also does camera QR both ways. A QR code doesn't need a radio at all.

We also made one call on purpose. We'd rather show no screen at all than a screen that says "Paid" when the server can't confirm it. So setup and every money screen stay off until the app build carries our settlement server's key. Build 3 was the first build to carry that key, and build 4 is the build that paid.

Accomplishments that we're proud of

On that first settle, the secp256r1 precompile checked the note's P-256 signature on chain and our program credited the shop (explorer). At 4:23 PM on Saturday the same check ran on a P-256 signature made inside an iPhone's Secure Enclave, and our program credited the shop (explorer).

The refused one is the part we're proudest of. We signed the same allowance and serial again, and the chain refused it with SerialUsed, error 6011 (explorer). We did the same from an iPhone at 4:31 PM and got the same refusal (explorer). It's the double-spend check working where anyone can look at it.

/verify has a "Play the attacker" mode. Change a signed note's amount or payee and the check fails and tells you exactly which bytes changed.

/proof reads the vault and every allowance at the same slot, and what the vault holds matches what it owes.

What we learned

You can't prevent a double spend offline. You can only bound it, and once we accepted that, the design got a lot simpler.

A money screen needs a name for every state it can be in, especially the in-between one where a payment is received but not settled yet.

On real phones, the hard part wasn't the cryptography. In our first two-phone test it took about 55 seconds of re-aiming to scan the allowance code and about 15 seconds to scan the note, so TestFlight build 5 switches the scanner to a macro-capable camera with near focus. Bluetooth delivery also turned out to need the two phones to be LOKA contacts first.

The biggest thing I'm taking away is that if someone can't check a claim without logging in, it doesn't count. So our claims table sits on a public page. Every claim marked Live shows when it was read and how old that read is, and the ones we haven't proven yet say Building.

What's next for LOKA Pay

  • Time each payment from the shop's phone getting signal to the payment being final on devnet, and publish the measured run where anyone can recompute it.
  • Move the program's upgrade authority from one teammate's key to a 2-of-3 multisig. Right now one key can upgrade it, and /trust says so.
  • Run the Android hardware-key payment on a real phone and put the relay APK on GitHub Releases.
  • Pilot it with a real shop.
  • Mainnet comes after the multisig and an outside audit. Moving real money would likely count as money transmission and need KYC, so we'd partner with a licensed provider instead of trying to become one.

Built With

Share this project:

Updates

Submission history