Guarantees
Promises and limits.
Audit status: not audited. Its safety rules are checked by automated tests that run thousands of random scenarios. Test networks only for now. Real-money networks will launch with fixed caps (100 USDC per budget, 1,000 USDC in total).
The three claims we make, and no stronger ones
- 1
The money is really there
When the budget is created, the USDC is set aside on the blockchain. Only this seller can collect it.
Exact claim · Every redeemable note is backed by funds reserved exclusively for its payee until the budget expires.
- 2
It can never go over
The agent, or whoever holds the key, can’t sign for more than the budget. Not even if the key is stolen.
Exact claim · The spender cannot authorize more than the certificate’s face value.
- 3
Only the named seller gets paid
Anyone may send a payment slip to the blockchain, but the money can only land with the seller you chose.
Exact claim · Anyone can redeem a redeemable note, but its value can only be delivered to the certificate’s payee.
What if…?
The situations people ask about most, with the honest answer and the worst case.
My agent’s key is stolen
LimitedThe thief can only pay the one seller you chose, and only up to what’s left in the budget. It can’t send money to itself or anyone else.
Worst case · At most what was left in that one budget.
What each party is guaranteed
| Party | Guarantee | Conditions |
|---|---|---|
| Payee | Every redeemable note is backed by funds reserved exclusively for it; redeeming a redeemable note pays exactly cumulative − redeemed. | Redeems before expiry; token not frozen; chain live; its acceptance state is authoritative. |
| Funder | Never loses more than the face value; gets the remainder back after expiry. | — |
| Funder (spender key stolen) | Loss ≤ the remaining face value of budgets bound to that key. | — |
| Spender (agent or person) | Cannot be charged more than the highest cumulative it signed; retries never create extra charges; network failures never raise its obligation. | Signatures are over cumulative totals; requestId idempotency; durable outbox. |
| Everyone | Funds go only to the payee named at issuance. | — |
Not guaranteed: that the payee delivers the service; that notes reach the payee (if a note is lost, the payee just doesn’t serve). The payee must redeem before expiry. The stablecoin issuer can freeze funds. Chain liveness is assumed at redemption time.
Control: what a funder can and can’t do
- Choose exactly where money can be spent (one budget per place).
- Choose how much, and top up at any time.
- Choose how long, and extend it.
- Not renew: small amounts on short cycles work as an allowance.
- Can’t freeze or cancel a budget before expiry.
- Can’t lower a limit after issuing.
- Can’t block one purchase at an allowed place.
- Can’t see purchases before the shop collects, unless the holder’s app sends receipts.
There is no freeze button on purpose: the seller accepts notes instantly, even offline, only because the money can’t be pulled back. Control happens at issue and renewal time.
Privacy
Payments are public on the blockchain but not linked to names. Anyone can see that some address paid a canteen, but not who. We don’t claim anonymity: flows between addresses are public.
Full threat model (29 cases)
| Threat | Outcome | Why |
|---|---|---|
| Agent (spender) key stolen | Attacker can pay only the named payee, up to the remaining face value | Payee-scoped + cap |
| Agent goes rogue or loops | Spending stops at the face value; the client also enforces a per-request price cap | Cap on-chain and in the client |
| Payee tries to overcharge | Not possible beyond notes the spender signed | Notes are cumulative totals signed by the spender |
| Payee serves nothing | The funder loses up to what the agent signed | Payment ≠ service; bounded by the face value |
| Funder tries to pull funds early | Not possible | No cancel; reclaim only after expiry |
| Very short expiry | Rejected below 1 h by the contract; servers require a minimum remaining lifetime | Contract + server checks |
| Replay on another chain or contract | Invalid | EIP-712 domain |
| Signature malleability | Rejected | OpenZeppelin ECDSA low-s |
| Front-running a redeem | Harmless | Funds always go to the payee |
| Payee runs two servers without a shared store | The payee may serve more than it can redeem (its own loss) | The store must be shared |
| Payee misses expiry | The payee loses unredeemed value | The redeemer’s safety margin and alerts |
| Hostile or unusual token behaviour | Out of the attack surface | One immutable token per deployment (Circle USDC) |
| Contract-signature (ERC-1271) revocation | Not applicable | Spenders are ECDSA-only; validity never depends on chain state |
| Buyer retries after a timeout | No double charge, no duplicate side effect | requestId idempotency |
| Buyer crashes mid-request | No higher note is ever signed; the pending note is resent | Durable outbox |
| Seller crashes after accepting, before serving | The retry resumes it; otherwise the sweeper resolves it | Idempotent application status |
| Many concurrent requests reusing the same credit | Only as many are admitted as the credit allows | Reserved accounting with an atomic re-check |
| Funder’s own wallet as the spender, or payee = spender | Rejected by the contract | Structural key isolation |
| Unaudited contract bug on mainnet | Exposure bounded deployment-wide | 1,000 USDC deployment cap + 100 USDC per budget |
| Stablecoin freeze or blocklist | Funds stuck | Inherent to the token; disclosed |
| The RPC lies to the seller | The seller may accept notes against a fake budget | Use a trusted RPC; disclosed |
| Customer’s phone stolen | The thief can spend only at the named shop(s), up to the remaining face value | Payee-scoped + cap; PIN-encrypted key |
| Several POS devices offline and unsynced | The same range may be accepted twice (the shop’s own loss) | Primary-POS rule or per-device float |
| Gift link forwarded or leaked | Whoever holds it can spend it, at that shop only | Treat gift links like cash |
| First-time customer while the POS is offline | A fabricated budget is possible | Shown as “Accepted at your own risk: not checked yet”, capped by the first-visit limit |
| Agent tries to raise its own budget via MCP | Not possible | No issue or top-up tools; the agent key isn’t the funder |
| Seller’s store is wiped | Old notes never count as new value; the seller loses at most its redemption lag | RECOVERED state; the buyer never loses credit |
| Onlookers analyse the chain | They see addresses, amounts and times, not names | Fresh keys for people; names never on-chain |
| Fake shop in Places | A funder could lock money for a scammer | Places are added by in-person QR scan or the seller’s own domain |
Out of scope, stated plainly: paying strangers offline; strong privacy; freezing a budget early (by design); disputes and refunds.
What we left out, and why
Some features sound useful but can’t be made safe with software alone. Flying Money doesn’t offer them:
- Paying strangers offline. Without a connection, nothing stops the same money being shown to two people at once. Offline, a shop only accepts customers it has already checked, up to a limit it chooses.
- One pot that pays anyone. If one balance could pay many places, only the first to collect would be paid. Every budget pays exactly one place, so its money is really there for that place.
- Passing a budget along. A chain of hand-offs would let an earlier holder take the money back from a later one. A budget stays with the key you gave it to.
- One key for everything. Each budget has its own spending key, so if one leaks, the loss can’t exceed that budget.
- A blockchain transaction per payment. Too slow and too costly for small payments. One collection covers any number of them.