Skip to main content

Command Palette

Search for a command to run...

Settlement can wait, Acceptance cannot: Cryptographic Offline Payments for the Nigerian Market

Payments Without Internet: A Stored-Value Ceiling Protocol for Connectivity-Constrained Markets

Updated
25 min read
Settlement can wait, Acceptance cannot: Cryptographic Offline Payments for the Nigerian Market
A

I am a developer from Nigeria interested in mobile technologies, backend development, and system designs. I am currently Fluttering fulltime @Chore Technologies.

Abstract

Every payment method deployed in Nigeria — NIP, USSD, NQR, POS — requires the payer and the receiver to be online at the exact moment of sale. For a material fraction of transactions, every day, they are not.

This paper describes OfflinePay, a protocol for peer-to-peer payments that work with zero connectivity. The payer pre-funds an offline wallet (placing a lien on the funds), receives a cryptographically signed spending ceiling, and generates Ed25519-signed payment tokens that receivers verify entirely offline. Settlement happens when either party reaches the internet. A gossip layer lets any device that touches the transaction graph carry settlement evidence — so if a vendor goes online before the payer does, the vendor's upload settles everyone upstream.

The cryptographic model is a direct adaptation of two systems that have moved hundreds of billions of dollars offline: Transport for London's Oyster Card (stored-value ceiling, offline verification) and EMV offline contactless floor limits (pre-authorized spending within a bounded exposure). What this work adds is a peer propagation layer, a two-step offline handshake that binds payment to a receiver-issued request (closing QR-swap attacks), and a two-layer encryption scheme that makes gossip carriage safe across untrusted intermediaries.

1. Why This Exists

Nigeria processed over ₦600 trillion in electronic payments in 2023. NIP handled 9.7 billion transactions. These are genuine achievements — NIP recently became the first instant payment system in Africa to reach AfricaNenda's "Mature" inclusivity tier.

But every one of those transactions required a live connection to a central switch. When that connection breaks, the payment fails and the user falls back to cash. The connection breaks often, through at least many independent mechanisms.

The switch goes down. NIBSS NIP experienced at least three major outages in 2024 alone. In February, the channel went down for hours. One user on Twitter: "I was almost disgraced today because of this at a mall I went. I don't carry my debit cards around and I don't have enough cash for what I picked on the shelf, 4 bank apps refuse to make payment, I had to drop the items and leave." In June, it happened again — the third time that year. The CBN itself acknowledged this concentration risk in December 2025 when it ordered dual connectivity between NIBSS and UPSL for all POS transactions. That directive is, functionally, an admission that single-switch dependence is a problem they haven't solved.

Banks take their systems offline. GTBank migrated to Finacle in October 2024, closing branches early on a Friday and warning of an 11-hour digital blackout from Sunday night into Monday morning. The outage extended past the promised deadline. Banking systems often require frequent maintenance that leads to downtime for customers and closing payment channels temporarily.

Cell towers die when diesel runs out. Between January and August 2025, telecom operators recorded over 19,000 fibre cuts and 3,000 cases of equipment theft. A base station running on a diesel generator goes dark when the tank empties, and every device in its coverage area loses data.

Data Plan Expiration. Internet data subscriptions are notorious for expiring and exhausting at points when users need to make payments, and with no means of connection to even login on their app, users are forced to look for external internet connections, call friends to renew subscriptions for them, or default to other payment means like cash (if available and enough). OfflinePay can fill this gap.

Low battery kills connectivity before it kills the screen. A phone at 5% can display a QR code. It cannot sustain the radio power draw of a 3G/4G session. Users disable data to preserve charge. Rational behaviour that makes every online payment method unavailable.

Dead zones are everywhere. Underground markets, reinforced-concrete malls, fuel stations in valleys. Common points of sale, common dead spots.

The design goal is simple: a payment must be accepted — binding on the payer, final for the receiver — without either device touching the internet.

Settlement can wait. Acceptance cannot.

2. Prior Art

The belief that offline digital payments are inherently unsafe is common and wrong.

Oyster Card. Since 2003, Transport for London has settled fare payments entirely offline at the gate. A reader verifies the card, debits a stored-value balance on the chip, logs the transaction locally. Settlement with TfL's back office can lag the ride by hours. This works because the balance was pre-funded. The gate isn't asking the bank anything — it's decrementing value the chip already holds.

EMV floor limits. Every contactless card terminal operates with a floor limit: a transaction amount below which authorization happens offline, without consulting the issuer. The card produces a cryptogram, the terminal verifies it against a cached issuer certificate, accepts the payment, and batches it for later clearing. Visa and Mastercard have tolerated this since the 1990s because the expected loss from offline fraud below the floor is smaller than the revenue loss from declining transactions during outages.

What OfflinePay adds. Oyster proves that a stored-value ceiling verified offline is safe at scale. EMV proves that payment networks already accept offline authorization within bounded exposure. OfflinePay synthesizes both — a pre-funded offline wallet (Oyster) authorized up to a cryptographic ceiling (EMV floor limit, generalized), with settlement deferred until either party finds connectivity.

The genuinely new pieces are: a gossip propagation layer that turns every smartphone into a settlement carrier for every transaction it has witnessed (as far as I can tell, the first application of epidemic dissemination to retail payment settlement), and a receiver-initiated request binding that prevents QR-swap attacks without requiring online coordination.

How OfflinePay Compares to What's Deployed Today

NIP USSD NQR POS OfflinePay
Needs internet at point of sale Yes Yes Yes Yes No
Works during NIBSS outage No No No No Yes
Works without data subscription No Yes (2G) No N/A Yes
Works at 5% battery No Partial No N/A Yes
Settlement speed Instant Instant Instant ~T+1 Minutes to hours
Hardware needed Smartphone Any phone Smartphone Dedicated terminal Smartphone

The tradeoff is settlement latency. For the use cases this targets — rural markets, fuel stations, basement shops — that tradeoff is favourable.

3. System Architecture

OfflinePay is a single C2C app — every registered user can both send and receive. "Payer" and "receiver" are per-transaction roles, not separate app builds. There is no distinct merchant application.

The logical components:

  1. C2C mobile app (Flutter). Single codebase. Generates payment tokens, verifies incoming payments, renders animated QR or NFC APDU chunks, queues gossip blobs, maintains an on-device SQLite journal. Role (sender or receiver) is determined per-transaction.

  2. Wallet / Ceiling service (Go). Issues ceiling tokens, places liens, manages Ed25519 key lifecycle, handles device-loss recovery.

  3. Identity service (Go). Issues bank-signed DisplayCards that bind a user's display name to their user ID, so receivers can be named to payers without impersonation risk.

  4. Settlement service (Go). Two-phase settlement engine with a system-suspense accounting model, conflict resolver, auto-settle sweep.

  5. Reconciliation service (Go). Payer-side and receiver-side sync, per-batch settlement receipts, nightly double-entry ledger audit.

For this PoC the entire backend ships as a single Go HTTP binary. The OpenAPI spec is the client contract; services are Go packages invoked in-process. Persistence uses PostgreSQL via SQLC with Redis as an optional cache-aside layer. A transactional outbox backed by NATS JetStream carries online transfer events — no ledger mutation happens inside a request handler.

All amounts are integers in kobo (1 Naira = 100 kobo). No floating-point arithmetic touches any monetary value anywhere in the system.

4. The Offline Wallet and the Lien

Each user has two wallets. A main wallet (online-only, normal account behaviour) and an offline wallet whose balance is derived on-device from the ceiling amount minus the sum of payments already signed — there is no separate "offline" account in the database.

Funding the offline wallet is an online operation. It does three things atomically:

  1. Debit the main wallet by the funded amount.

  2. Place a lien on those funds — a hard hold, stored in a lien_holding account. They cannot be spent through any other channel.

  3. Issue a signed ceiling token authorizing the offline wallet to spend up to that amount.

The user experience maps to "withdrawing cash from POS." The funds leave the main balance. The offline wallet holds the authorization to spend them.

Why does the lien matter? Because it makes deferred settlement safe. When the settlement service finally processes an offline transaction — possibly 72 hours later — it doesn't attempt a fresh debit against the user's available balance. The funds were held at issuance. Settlement is a ledger move from lien_holding through system-suspense to the receiver's main account. The insufficient-funds check happened once, at issuance, when the user was online and the bank was authoritative. Nothing that happens after — the user transferring money online, receiving unexpected debits — can cause a receiver's valid claim to bounce.

Ceiling Token Lifecycle

Event What happens
Issuance Lien placed on main wallet. Ceiling token signed by bank. Payer's Ed25519 private key provisioned into hardware keystore.
Spend (offline) Remaining ceiling decremented on-device. No server interaction.
Drain User returns online, moves unspent balance back to main wallet. Lien released, ceiling revoked.
Expiry Token becomes invalid after expires_at + grace_period. Unspent lien auto-released.
Recovery Device lost — user triggers recovery from a new device. Ceiling moves to RECOVERY_PENDING: no new payments can be signed, but merchants carrying already-signed tokens can still submit claims. After the grace window elapses (default ~75 hours), the remaining lien drains to main and the ceiling becomes REVOKED.

Ceiling tokens are short-lived by design — 24 to 72 hours is the typical TTL, configurable per KYC tier.

5. The Protocol

Phase 1 — Pre-Authorization (online)

The user funds their offline wallet. The service generates an Ed25519 keypair on the device (private key goes into Android Keystore or iOS Secure Enclave, never leaves the hardware), debits the main wallet, creates the lien, and issues a ceiling token:

CeilingToken {
    payer_id:        UUID
    ceiling_amount:  uint64     // kobo
    issued_at:       int64      // unix seconds
    expires_at:      int64      // unix seconds
    sequence_start:  uint64
    payer_pubkey:    [32]byte
    key_id:          string     // identifies which bank signing key was used
    bank_signature:  [64]byte   // Ed25519(sk_bank, H(all fields above))
}

The bank signs with one of several active keys, identified by key_id. Devices cache bank public keys; key_id tells them which one to use for verification.

The system also provisions a device-session token — a separate Ed25519-signed credential that allows the device to unlock the offline wallet UI on cold start without network. The session token is scoped to offline_pay, bound to the device ID, and gated by biometric or PIN. This means the user can open the app and pay even if they haven't had connectivity since their last login. See Section 11 for the threat implications.

Phase 2 — Offline Transaction (no connectivity)

This is a two-step offline handshake. The receiver goes first.

Step 1: Receiver issues a PaymentRequest. The receiver's app generates a short-lived, signed request:

PaymentRequest {
    receiver_id:              UUID
    receiver_display_card:    DisplayCard    // bank-signed identity credential
    amount:                   uint64         // kobo; 0 = unbound (payer picks)
    session_nonce:            [16]byte       // random, single-use
    issued_at, expires_at:    int64
    receiver_device_pubkey:   [32]byte
    receiver_signature:       [64]byte
}

The embedded DisplayCard is signed by the bank's active signing key. It binds a display name and account number to the receiver's user ID. The payer's app verifies this signature against the cached bank public key before proceeding — so the name shown on the payer's screen is attested by the bank, not self-asserted by the receiver.

Step 2: Payer counter-signs with a PaymentToken. The payer scans the PaymentRequest, verifies the DisplayCard signature and the receiver's signature, then builds a token that binds to the request:

PaymentToken {
    payer_id:           UUID
    payee_id:           UUID
    amount:             uint64     // kobo
    sequence_number:    uint64     // monotonic per ceiling
    remaining_ceiling:  uint64
    timestamp:          int64      // device clock — audit only
    ceiling_token_id:   string
    session_nonce:      [16]byte   // copied from the PaymentRequest
    request_hash:       [32]byte   // SHA-256(canonical(PaymentRequest))
    payer_signature:    [64]byte
}

Three properties to note. sequence_number is strictly increasing per ceiling — this is how double-spend gets detected. timestamp is not used for ordering (device clocks are unreliable; the sequence number is the only ordering primitive). And request_hash binds this payment to a specific PaymentRequest — if someone swaps the PR after the payer has signed, the hash won't match and the server rejects it at settlement.

The payer's app signs the token with their hardware-backed private key, packages it with any gossip blobs they're carrying (Section 7), encrypts the package (Section 8), and transmits it back to the receiver as an animated QR code or NFC tap.

Phase 3 — Receiver Verification (offline)

The receiver's app scans the animated QR (or receives via NFC), decrypts the payload, and runs six checks — all locally, no network:

  1. Bank signature on the ceiling token. Verified against the cached bank public key matched by key_id.

  2. Payer signature on the payment token. Verified against the payer public key embedded in the (now-verified) ceiling token.

  3. PaymentRequest binding. The receiver recomputes SHA-256(canonical(PR)) from its own copy of the request it issued moments earlier, compares it to the token's request_hash, checks session_nonce equality, and (for non-unbound requests) amount equality. A swapped PR fails here.

  4. Sequence monotonicity. The incoming sequence_number must be strictly greater than the last seen for this payer (tracked in local SQLite).

  5. Ceiling non-exhaustion. remaining_ceiling >= 0 after this transaction.

  6. Expiry check. now <= expires_at + 30 minutes (grace period for clock drift). A separate, shorter expiry applies to the PaymentRequest itself.

All pass; accept, write to SQLite queue in WAL mode durably before the success UI appears. Failure on any check; reject with specific reason.

Phase 4 — Settlement (online)

This happens in two phases, routed through a system-suspense account.

Phase 4a: Claim. Whichever device reaches connectivity first (payer or receiver) uploads the signed batch. The settlement service re-verifies all four signatures (bank on ceiling, payer on payment, bank on DisplayCard, receiver on PaymentRequest), recomputes request_hash and compares byte-for-byte, then processes tokens in sequence-number order per ceiling. For each valid claim, it posts balanced ledger entries: debit system-suspense, credit receiving_pending for the receiver. The receiver sees funds appear in pending.

Phase 4b: Final settlement. Either the payer comes online (triggering finalization), or the 72-hour auto-settlement timeout expires. The service walks pending claims in sequence order and, for each one within ceiling capacity, posts: debit lien_holding and credit system-suspense (releasing the payer's held funds), then debit receiving_pending and credit main for the receiver (funds become spendable). Any claims that exceed the remaining ceiling receive partial settlement or rejection. When all pending claims for a ceiling reach a terminal state, system-suspense returns to zero.

The lien makes auto-settlement safe. The payer cannot "not have the money." The money was held when the ceiling was issued.

6. Double-Spend

A payer who wants to cheat signs two payment tokens against the same ceiling and presents them to two different receivers who can't talk to each other. If both verify and accept, the payer has committed the same funds twice.

Detection is straightforward. Every token has a sequence_number. At settlement, the service indexes all tokens by (payer_id, ceiling_id, sequence_number). A collision is trivially detectable. Even without sequence collisions, total spending can exceed the ceiling if the payer visits multiple offline receivers. Resolution: process tokens in sequence order, settle each fully while ceiling has capacity, partially settle the first one that exceeds remaining capacity, reject everything after.

The session_nonce provides a second deduplication axis — each PaymentRequest can back exactly one accepted payment, enforced by a unique constraint on (payee_user_id, session_nonce). This means a receiver can't replay the same PR to extract multiple payments from a payer.

Security Bound

Theorem. The maximum loss to the system from a single malicious payer in a single ceiling period is exactly ceiling_amount kobo, independent of the number of colluding receivers or the number of fraudulent tokens produced.

Proof. At ceiling issuance, the bank places a lien of ceiling_amount on the payer's lien_holding account. This is the sole source of funds for settlement. The settlement service processes claims in sequence order and stops when the cumulative settled amount reaches ceiling_amount. No claim is settled from any other source. After all pending claims reach a terminal state, system-suspense returns to zero and total credits to receivers equal total debits from the lien. The payer pre-funded this amount at Phase 1, so the aggregate loss to the system is zero.

The economic case for this attack is structurally poor. The attacker must deposit real money upfront, can at best double-spend that same money (not create money from nothing), and gets flagged for future ceiling reductions. Visa has accepted a similar risk profile for thirty years.

7. Gossip

A payer buys fuel from a rural station. The station hasn't been online in two days. The station owner buys groceries from a market vendor that evening. The vendor hasn't been online either. Later, the vendor connects to home Wi-Fi.

Three transactions need to settle. Only the vendor's phone is online. Without gossip, only the vendor's own transaction settles. The other two wait.

Gossip solves this. Every payment's animated QR carries not just the direct payment token but sealed gossip blobs from other transactions the payer has witnessed or carried. When the station receives a payment, it inherits all the payer's gossip. When the station later pays the vendor, the vendor inherits everything — including the original payer to station transaction. When the vendor goes online, one upload settles the entire chain.

A gossip blob carries a full claim: the ceiling token, the payment token, and the PaymentRequest — everything the server needs. On upload, the gossip service opens the sealed box, decodes the inner payload, rebuilds a claim, and routes it into the same SubmitClaim code path as a direct submission. Deduplication by (payer_id, sequence_number) absorbs duplicates.

Settlement latency stops being "when does each person get online" and becomes "when does anyone in the transaction graph get online."

The rules keep it bounded. Each device carries at most 500 blobs. Oldest-first eviction. A blob's hop count increments each time it's forwarded; at 3 hops, propagation stops. Devices track seen hashes via a Bloom filter. The server deduplicates idempotently.

Gossip is privacy-sensitive in most protocols. Not here. Every gossip blob is sealed with the server's X25519 public key (Section 8). Carriers can't read what they're carrying. They transport opaque ciphertext. Only the server decrypts, only at settlement.

8. Encryption

Two layers, each solving a different problem.

Layer 1 — Realm key (AES-256-GCM). A symmetric key shared by all registered app instances, provisioned at registration, stored in the device's hardware keystore. All QR/NFC data is encrypted with this key. Someone scanning the QR with a generic camera app sees binary noise.

The payload starts with a single cleartext byte: key_version. This tells the receiving app which realm key to use — essential during rotation when payer and receiver might briefly be on different key versions. Per-frame nonces are derived from a random base nonce plus the frame index, so each frame in a single stream is independently authenticated.

Layer 2 — Server sealed box (X25519 + libsodium crypto_box_seal). Applied only to gossip blobs, not to the direct payment token (the receiver needs to read and verify that one offline). Once sealed, a gossip blob can only be opened by the server's private key. This is what makes gossip carriage safe at arbitrary hop depth.

Both QR and NFC share the same sealed wire bytes — the NFC path wraps them in an APDU pull protocol, the QR path chunks them into animated frames. The transport is abstracted; the payment envelope is identical regardless of delivery mechanism.

9. Transport

Animated QR — Primary

Payments move in one direction: payer's screen to receiver's camera. The payload splits across QR frames rendered as an animation — 10–15 fps, looping every 2–3 seconds, roughly 2 KB per frame, giving ~72 KB total capacity per loop.

The receiver's camera captures frames in any order (each carries a frame_index). Missed frames get caught on the next loop. A SHA-256 hash in the header frame and a matching checksum in the trailer verify integrity once all frames are reassembled.

NFC — Secondary

Same sealed wire bytes, transmitted via Host Card Emulation using a simple ISO-DEP pull protocol:

  1. Reader → HCE: SELECT(AID).

  2. HCE → Reader: 90 00.

  3. Reader → HCE: GET_CHUNK(index).

  4. HCE → Reader: total_chunks + chunk data + 90 00.

The reader discovers the total chunk count from the first response and drains the rest. Sub-second transfer versus 2–4 seconds for QR. Since the NFC link already carries CRC and AES-GCM authenticates the payload, the SHA-256 header/trailer framing used for QR is skipped on NFC.

iOS cannot do third-party card emulation, so iOS users pay via QR only; iOS devices can still act as receivers (readers). Android supports both directions.


10. Settlement and Reconciliation

Account Model

Every user has three kind-keyed accounts:

Account Role
main Spendable wallet. Credited by top-ups, incoming transfers, and Phase 4b settlement.
lien_holding Funds held behind an active ceiling token.
receiving_pending Claims accepted in Phase 4a, not yet finalized.

A single system-owned system-suspense account serves as the counterparty between settlement phases postings. It is the only account permitted to carry a negative balance.

Double-Entry Posting Flow

Phase Debit Credit
Fund offline main (payer) lien_holding (payer)
Per claim (amount A) suspense receiving_pending (receiver)
Settled lien_holding (payer), receiving_pending (receiver) suspense, main (receiver)
Unsettled (shortfall) receiving_pending (receiver) suspense
Expiry / drain / recovery release lien_holding (payer) main (payer)

After all pending claims for a ceiling reach a terminal state, suspense returns to zero. The nightly reconciler verifies this.

Transaction State Machine

QUEUED → SUBMITTED → PENDING → SETTLED | PARTIALLY_SETTLED | REJECTED

QUEUED → EXPIRED (if not submitted within ceiling TTL + grace)

QUEUED — accepted offline, in the device's SQLite. SUBMITTED — uploaded, server verifying. PENDING — signatures verified, receiver credited pending. SETTLED — lien converted, receiver credited main. PARTIALLY_SETTLED — ceiling exhausted, partial credit with exact shortfall in receipt. REJECTED — invalid signature, expired, or fraud flag. EXPIRED — not submitted within ceiling TTL + grace. The one state everyone wants to avoid.

Settlement submissions are idempotent — (payer_id, sequence_number) is the key. The auto-settle sweep runs every 15 minutes, finalizing payers whose oldest pending claim is older than 72 hours.

Reconciliation

Three loops.

Payer reconciliation. Payer comes online, app syncs the local transaction log against settled claims. Three outcomes: matched (normal), unmatched-local (payer paid but no claim submitted — likely the receiver lost their device or hasn't synced), unmatched-server (claim settled that the payer doesn't recognize — potential fraud, triggers investigation).

Receiver reconciliation. For every batch submission, the server returns an itemized receipt: tokens submitted, settled (with amounts), partially settled (with shortfall and reason), rejected (with reason per rejection). The app stores these alongside the local queue for full auditability.

Ledger reconciliation. Nightly job verifies three invariants: sum of all debits equals sum of all credits, no ceiling has been over-drawn (sum of settled amounts ≤ ceiling_amount), and every lien maps to an active, settled, or released ceiling with no orphans. A failed check halts auto-settlement and pages the operator.

11. Threat Model

Malicious payer trying to double-spend: detected at settlement via sequence ordering, total exposure capped at ceiling, fraud score incremented. Trying to extract the signing key: it's in Android Keystore / iOS Secure Enclave; extraction requires a hardware-level attack, and the reward is at most the remaining ceiling.

Malicious receiver fabricating a payment: impossible without the payer's hardware-backed private key. Replaying a valid token: blocked by (payer_id, sequence_number) and (payee_id, session_nonce) unique constraints — two independent replay guards. Swapping the PaymentRequest after the payer signed: request_hash is part of the payer's signed payload; a substituted PR produces a different hash and fails server-side verification. Spoofing the displayed name: the DisplayCard is bank-signed; the payer verifies this signature before counter-signing.

Network attacker eavesdropping on QR: Layer 1 encryption renders it opaque. Even with Layer 1 access, gossip blobs are sealed to the server only.

Device loss: ceiling TTL (default 24h) caps exposure. The ceiling recovery flow (RECOVERY_PENDING state) keeps the lien locked for a grace window (~75 hours) so in-flight merchant claims can still land, then drains the remaining balance back to main. A stolen phone is further gated by biometric or PIN (Argon2id-hashed, 5-attempt lockout) via the offline authentication layer.

Offline authentication deserves a note. The device caches an Ed25519-signed device-session token (minted on the last online login, 14-day TTL) that allows cold-start unlock without connectivity. The token is scoped to offline_pay and bound to the device's hardware identity. It authorizes viewing cached balances, generating QR codes, scanning incoming payments, and carrying gossip. It does not authorize topping up the offline wallet, withdrawing to bank, or changing credentials. The blast radius of a compromised offline session is bounded by the existing ceiling — funds that were already lien'd.

Issuer key compromise: key_id enables immediate rotation. Compromised key revoked, new ceilings issue under the new key. Existing ceilings under the compromised key are identifiable and revocable.

12. Throughput

Offline throughput is bounded by transport, not server capacity. An NFC tap completes in under a second. A QR scan completes in 2–4 seconds. A receiver processing a queue of customers can handle 15–30 transactions per minute via NFC, 10–15 via QR.

Settlement throughput is a Go service backed by PostgreSQL. Based on known benchmarks sustained throughput of 500+ TPS is achievable, with burst capacity to several thousand. Settlement is batch-oriented — uploading 200 queued transactions triggers a single batch, not 200 individual roundtrips.

Gossip capacity is self-limiting by design. At 500 blobs per device with a 3-hop limit, the Bloom filter and carry cap keep devices from being overwhelmed. The important property isn't maximum reach — it's minimum latency. Any single device reaching connectivity settles everything it carries.

13. Limitations

Ceiling sizing is a policy problem. Too small forces frequent top-ups. Too large increases device-loss exposure. The design allows per-KYC-tier ceilings, but calibration is operational work.

Gossip doesn't reach everyone. The 3-hop limit and 500-blob cap bound propagation. The 72-hour auto-settlement timeout is the backstop for devices that stay offline for days.

Clock drift. The 30-minute grace period is pragmatic. Deeply drifted devices (>30 minutes) will fail verification.

iOS NFC. Apple doesn't allow third-party card emulation. iOS users pay via QR only; iOS devices can still receive via NFC reader mode. No protocol changes needed if Apple opens this up.

HSM key binding. The current implementation stores payer signing keys in flutter_secure_storage with OS-managed Keystore/Keychain semantics. True non-exportable hardware-enclave binding (StrongBox on Android, Secure Enclave on iOS) is a tracked follow-up that will require a protocol-level decision on ECDSA P-256 versus Ed25519, since iOS Secure Enclave does not offer Ed25519 natively.

Regulatory. Live deployment requires CBN approval. The ceiling token should be positioned as a pre-authorized spending limit (like a card pre-auth), not as e-money or stored value, to stay clear of mobile money licensing requirements.

14. Conclusion

The dual-wallet lien model converts "how do we authorize spending without the bank" (hard) into "how do we enforce a ceiling on pre-held funds" (solved). The receiver-initiated PaymentRequest binding closes the class of QR-swap and name-spoofing attacks that a naive one-way protocol would leave open. The gossip layer turns settlement latency from a per-device problem into a graph-connectivity problem. The two-layer encryption makes gossip safe across untrusted carriers.

The open question is not technical. It is whether the Nigerian banking and regulatory ecosystem will permit an offline payment instrument at this level of generality. The precedent — Visa and Mastercard have tolerated exactly this tradeoff for thirty years, and Oyster has run on the same principle since 2003 — suggests the answer should be yes.

Public Repo here.

References

  1. Transport for London. Oyster Fare Payment System — Technical Architecture. 2003–2024.

  2. EMVCo. EMV Contactless Specifications for Payment Systems — Book C. Version 2.9, 2022.

  3. Visa. Offline Authorization and Floor Limits. Merchant Operations Manual, 2021.

  4. Bernstein, D.J. Ed25519: High-Speed High-Security Signatures. 2011.

  5. Bernstein, D.J., et al. NaCl: Networking and Cryptography Library. libsodium documentation.

  6. NIST. SP 800-38D: GCM and GMAC. 2007.

  7. Gilbert, S., Lynch, N. Perspectives on the CAP Theorem. IEEE Computer, 2012.

  8. Demers, A., et al. Epidemic Algorithms for Replicated Database Maintenance. PODC, 1987.

  9. Nigerian Communications Commission. 2024 Subscriber/Network Performance Report. 2025.

  10. Central Bank of Nigeria. Directive on Dual Connectivity for POS Transactions. December 2025.

  11. Nairametrics. NIBSS Server Down as Nigerian Customers Lament. February 2024.

  12. AfricaNenda. NIP Nigeria Becomes Africa's First "Mature" Instant Payment System. November 2025.

  13. NCC / Vanguard News. Only 23% of Rural Communities Have Internet Access in Nigeria. October 2025.

This paper describes a system under active development. Feedback and adversarial review from payments engineers and security practitioners are welcome.
Contact: intellectualjemeel@gmail.com, Whatsapp+2348108678294, @intellect4all