Docs · Reference · Markdown
Protocol
Objects
A certificate lives on one chain in the FlyingMoney contract:
| Field | Meaning |
|---|---|
funder |
Locked the money; gets the remainder back after expiry |
payee |
The only address that can ever receive the money |
spender |
A secp256k1 key that signs notes. It holds nothing and sends no transactions |
faceValue |
The budget, in USDC base units (6 decimals) |
redeemed |
How much has been paid out so far |
expiresAt |
Unix seconds |
id = keccak256(abi.encode(chainId, contract, funder, funderNonce)).
A note is an EIP-712 signature by the spender over a running total:
domain = { name: "FlyingMoney", version: "1", chainId, verifyingContract }
Note(bytes32 certificateId, uint256 cumulative, bytes32 memo)
cumulative only ever grows. Redeeming a note pays cumulative − redeemed to the payee. memo is the request id, so one note maps to one request.
Wire format
Every object on the wire is fm1. + base64url(JSON), with integers as decimal strings. Headers:
| Header | Direction | Content |
|---|---|---|
Flying-Money-Offer |
seller → buyer, with 402 |
{ scheme: "flying-money", v: 1, price, minRemainingLifetime, accepts: [{ chainId, contract, token, payee }], memoHint? } |
Flying-Money-Note |
buyer → seller | { v: 1, chainId, contract, certificateId, cumulative, memo, sig } |
Flying-Money-Receipt |
seller → buyer | { certificateId, requestId, status, accepted, consumed, reserved, credit, remaining, expiresAt } |
Flying-Money-Reason |
seller → buyer, with 402 |
why the note was refused: insufficient, wrong-payee, expiring… |
Headers are at most 2,048 bytes. A signed note (the “payment slip” in the app) is about 544 characters, small enough for one QR code at error-correction level M.
Seller algorithm
The seller keeps, per certificate: accepted (the best total it holds), consumed (value served), reserved (in flight), and one outcome per requestId.
- Parse the note. Unknown chain or contract →
402offer. - Read the certificate (cached; payee and spender never change).
- A known
requestIdreturns its stored outcome, after an ECDSA check (replays are free). - Check payee,
closed, remaining lifetime ≥minRemainingLifetime, signature,cumulative ≤ faceValue. - Atomically:
consumed + reserved + price ≤ max(accepted, cumulative), thenreserved += priceandaccepted = max(accepted, cumulative)(no overspend under concurrency). - Serve. Success →
consumed += price; failure → the price becomes credit. - Return a receipt.
The redeemer only ever redeems served value, before expiresAt − 30 min.
Buyer algorithm
Per certificate, the buyer durably stores accepted, consumed and at most one pending note.
- On a
402, pick a certificate for that payee and chain with enough left and enough lifetime. - If a note is pending, resend exactly that note until a final receipt.
- Sign
next = max(accepted, consumed + price)with a freshrequestId. Refuse ifnext > faceValue. - Save the pending note, then send it.
- On a timeout, resend the same note. Never sign a new one.
- On a receipt, update
acceptedandconsumedand clear the pending note.
No growth on failure: a network failure, timeout or crash never makes the spender sign a higher total.
At a counter
The same objects travel as QR codes: the till shows a price QR (an offer with memoHint = order id), and the customer's phone shows a payment-slip QR (the signed note) with memo = keccak256(orderId). Tills report GUARANTEED (shown as "Accepted: covered by a checked budget"; certificate verified on-chain by this till, note passes the seller algorithm), UNVERIFIED (shown as "Accepted at your own risk: not checked yet"; offline, never-seen certificate, capped by a first-visit limit) or REJECTED. See People & shops.