Skip to content

Roadmap & board ​

Legend:

🟢 ProvenRan against a real merchant, card or money
🔵 BuiltGreen tests, never run against reality
🟡 DesignedWorked out on paper. No code
🔴 BlockedNeeds a named unblock
⚪ ParkedDeliberately shelved for now

🔵 keeps getting overstated, so it is deliberately the longest list.


Originally scoped ​

First thesis: a just-in-time gift card exchange. At checkout, match a buyer to one person's card. Warehouse nothing. Capture the spread incumbents charge to finance inventory, absorb fraud on unverified cards and carry dead stock.

Everything below follows from it, including detours for things the world would not supply.


🟢 Shipped and proven ​

ItemWhat was proven
Order pipeline, end to endTwo orders from intake to fulfilled, on the Tempo testnet, with a stand-in in place of the Cinemark driver. The real driver has never run, and no real money moved
Two on-chain settlements, on testnetMPP/Tempo moderato testnet, paid → fulfilled in ~7s
Stripe rail, test modeCheckout, webhook, signature verification, refunds, all on a test key
Merchant screen (7 merchants)Nike won: only it had a public, no-login balance page and a visible card field
Toast enablement: 86.2%127 venues from the public directory, 116 readable, 115 distinct brands. Real rate, one metro only
Square edge behaviourAnswers 200 to a bare fetch() that Toast 403s: an easier edge
Square balance endpoint is unusablePOST /v2/gift-cards/from-gan → 401. Needs merchant credentials, and card plaintext (keyed by GAN)
Square has no public balance pagesquareup.com/gift/check-balance is a soft-404
Toast has no balance lookup at allPer Toast's docs, for anyone, merchant included
Velocity window rollsA test moves the real wall clock across the boundary
Schema drift guardRefuses orders before settlement on schema drift

Clover: 3 of 3, which is a count and not a rate

All three venues we probed offered GIFTCARD. No directory lists Clover venues, and the resolver only finds known ones, so this cannot be sampled. The code marks it unmeasured and summarisePos refuses to rate it.


🔵 Built and tested, never proven ​

Test balances are injected and merchants are fixtures. No real card has been redeemed.

ItemWhat has never happened
In-store (B&M) reveal → settle → refundNo real card revealed at a real register
Web barcode pageNever scanned by a cashier
Barcode encoders (Code 128 / ITF / Code 39, and PDF417 for Target)Never scanned for real. See open issues
Pay on scanSave a card once, hold the price when the barcode opens, charge only if it worked. Tested against a stand-in for Stripe; never called Stripe itself, even on a test key
Two cards per purchaseAll of one gift card and part of the next, each with its own hold. Never run for real
Cinemark driverNever run against the real cinemark.com checkout. Both testnet orders used a stand-in
Best Buy adaptercheckBalance and execute never run live
Nike adapterexecute() blocked on a guided capture never performed
Evidence tiers 2 and 3No merchant- or buyer-asserted settlement yet
Contested dispute resolutionNo real dispute. Proven only against a clock
Relist / retirement-loss pricingNo card partially consumed for real
Adapter health alertingWired this phase, never fired for real
Lit vault (V3)Rebuilt for Lit's current service. Its live smoke passed on 2026-10-10, with a fake card. No production path uses it. Custodial: no unlock rule we cannot forge
Portability harnessNever fired. Needs a proxy and a judgement call
Layer A: Morpho yield venueERC-4626 adapter, release-never-blocks path, buffer guard and yield split, tested on a vault-shaped fixture. No transaction sent. Live round trip blocked on a funded Base Sepolia wallet
Layer B: pooled holdbackReserve sized to the aggregate bad month, not per-LP self-insurance. Beta-Binomial, explicit correlation assumption, checked against closed form, Monte Carlo and analytic moments
Layer B: LP advances50% at placed, where LPs get nothing today. Their own escrow, released early, so not borrowing. Stage 1 tops up instead of paying twice
Layer B: underwriting modelReal interface, IllustrativeUnderwriter behind it. On the discount slider, caveat above the numbers. Not fitted to anything. See DATA_REQUIRED_TO_FIT
x402 / Base railNo mainnet transaction
Apple Wallet passUnsigned: no certificate
Stablecoin escrow, phase 1SalvoEscrowV1, its Lit template salvo-vault-escrow/1, and the browser seal and reply. Tested offline, on an in-process chain and a fake Lit, then deployed on testnet in phase 2 (below). Not reviewed, so not counted non-custodial: on the testnet a listing is taken as the operator's own card, and readiness says not ready. No Stripe
Stablecoin escrow, phase 2 toolingThe live testnet runbook as scripts: escrow:probe-merchants, escrow:deploy (address check, pin, deploy), escrow:check, escrow:smoke and its resume, plus lit:setup and lit:smoke for the escrow template. Each is a dry run unless given --live, and takes keys from --key-file only. Run live on 2026-10-10: escrow:deploy, escrow:check, escrow:smoke, lit:setup and the lit:smoke bind. SalvoEscrowV1 is at 0x75c9aabe8254559f865e14d6e5fa70c738869bef on the Tempo Moderato testnet, and the template is pinned to it. The smoke passed every case, for 8 cents of Lit credit. Its seller payout is due after 2026-10-13 16:03 UTC. Not reviewed
One account and the card till (unified account, phases 1a and 1b)Face ID sign-in that finds the passkey before it makes one, so a new Safari tab or Home Screen icon is never a second account. /account: what pays, Most savings or In and out with a card limit, balances, store credit kept on the phone. /pay/[store]: type the total, tap Pay once, scan "1 of 2" and "2 of 2". Up to five cards, the rest of a part-used card first in line, at most one card left part used and within the cap, with a deposit for its rest held on the shopper's card, never captured to wait. Silence at Target and Walmart counts as used; "it didn't scan" keeps the hold for the next shopper's try, and their No takes it. At Best Buy and Nike the read bills what the card paid within the cap, behind a deposit for the rest, and a shortfall comes from the shopper's own deposit. Exposure measured on the value applied, not the price. Per-store Home Screen icons, a calendar reminder, redirects and the new nav. The second, never-deployed EVM chain removed. No migration. Built and tested offline, not deployed. Never run on Stripe, even a test key, never on a phone, never at a register. Dollars joined the same till in phase 3 (below)
Stablecoin escrow, phase 3 (Salvo Pay on the web)Passkey account and a capped weekly allowance on the phone (the testnet version of the stablecoin allowance below), test dollars from the faucet, /pay (quote, tickets, one-tap Load, the card drawn on screen), /cards, the Salvo Pay branch of /sell (sealed in the browser), the five /api/escrow routes, and the keeper in the 15-minute job. Since phase 3b of the till (below) the Load screen and /cards are gone: /pay is the till's front door, and the cards a phone bought here are under Account with their codes, still finishing and settling as before. Escrow orders ride the mpp rail with an escrow: reference, and every refund, reconcile, fulfilment and sweep path skips them. Off unless ESCROW_RAIL=testnet. Tested offline only. Listing is open on the testnet: our server takes a listing as the operator's own card, for a Salvo test card or a Target card. Not reviewed (phase 5)
Stablecoin escrow V2 (pay for what you use)SalvoEscrowV2 and its Lit template salvo-vault-escrow/2, phase 2 of the till spec. One card sold in parts: a buyer pays the discounted price only for the value used, the seller of a fully used card is paid for all of it, and the last card's remainder stays listed, with its value left tracked on chain, for the next buyer first. The buyer who saw the number deposits that remainder at face; it comes back when the next buyer's use of it holds up, and pays the seller if it turns out spent. No owner, admin, pause or upgrade. Tested offline on an in-process chain and a fake Lit, with its own compile, artifact and frozen template. Deployed on the Tempo Moderato testnet at 0xa30d1631bf644fa608dacc02b6d2aebbc1cd200b, with the template pinned to it and bound on Lit; its live smoke passed every case on 2026-10-13. Testnet only. Switched on (ESCROW_V2_LIVE, phase 3b below): new sales go to V2, and V1 takes no new listing, quote or ticket, while what it sold still releases, pays out and refunds. Not reviewed. No Stripe
Dollars at the till (unified account, phase 3b)The dollars rail is a real backend of the same till: the shopper with "default to stablecoins" goes tap, type, pay, scan as on the card rail. Our server's planTill routes the total over live V2 supply; the phone checks every card on both hosts, fetches the tickets, the chain read and the batch's nonce, fees and gas while the shopper types, and the tap signs and sends one batch with no prompt: the value used and the deposit on the last card's leftover. The batch goes through Tempo's eth_sendRawTransactionSync; Lit's release of every card runs beside the second host's agreement, never after it, and the purchase is booked only once both hosts agree. "Scan 1 of 2", "Scan 2 of 2", with card 2 drawn only after Next. /account is one account for both rails: dollars and the allowance, the saved card, the default and the routing, deposits held as the chain says, and one store credit list (V2 cards once, never as credit to count by hand, the signed-in account's only), which the till pays from first in dollars, sold back on V2. A card the register did not need is said to be store credit, and what it cost is said apart from what the register took, as is a card that did not work. One the chain did not take, or a report, is said, kept on the phone, asked again within seconds while the till is open and again when the till or Account opens; one whose rest went to someone else meanwhile is kept at what was paid for, and said. The total going up with a card on screen uses that card first, as the register does: a leftover listed for the next shopper is bought out of its deposit before the register can take it. A card sold from store credit is off both lists the moment its listing is saved, and a dollars Pay still going through, or a purchase inside its 48 hours, is found again after a sign-out, for its own account only. /sell lists on V2, with take back, "I used it myself", the five-to-seven-day review of a leftover, and Get paid. scripts/till-timing.ts times both rails offline step by step. Built and tested offline, not deployed, never run on a phone. Testnet only. Not reviewed

🟡 Designed, not built ​

ItemNote
Salvo as an agent toolDecided 2026-10-30. An agent asks Salvo for a card at a store for an amount, gets a discounted card from the escrow, enters it at the store's online gift card field, and pays the rest with the user's own wallet (Link, Shop Pay, AP2). Online the page shows what each card covered, so unreadable stores work and dust drains across many cards. Proof of concept with Hermes and Claude, which already call outside tools; Muse, Grok Bot and Instinct as they open to tools
Stablecoin escrow, phases 2 and 4 to 6Phases 1 and 3 and the phase 2 scripts are built (above), and phase 2's testnet deploy, pin and smoke ran on 2026-10-10. Next: the smoke's seller payout (phase 2), then Apple Wallet passes built on the phone (phase 4), then the custody review, dispute and refund screens, leftover-first and selling a card back (phase 5), then the fallback page and the native companion (phase 6). See Where the money goes
One account and the till, phases 4 to 6Phases 2 and 3 are built (above). Still to come from phase 3: the readable stores' confirm read (it waits on the Lit reader probe), and the card-rail seller form shown only to an operator session (today /sell's usual way still takes any card as the operator's, as before). Then the Wallet store pass and nearest store (phase 4); a table for pass updates, alerts and settings sync (phase 5)
Fallback routeBarebones buying when our servers are down: encrypted listings kept outside our database, a static page, Lit checking the contract
Instant principal flipAt match, buy the card from the seller and sell it to the buyer in one step: two sales, not a payment passed through us. Needs legal review. See Money transmission
Stripe ConnectStripe, not us, holds card payments and pays sellers. Needed before anyone but the operator can list a card on the card path. Needs Stripe's approval
Hidden intents (commit-reveal)Publish a fingerprint of each listing, not its terms, and reveal at execution. Built for the crypto rail (above), not for the card-hold rail. See Hidden intents
Stablecoin allowanceA shopper pre-funds their allowance in stablecoins instead of a card hold, so a register purchase needs no Stripe
Layer C: our own curated vaultA MetaMorpho vault we curate, open to outside depositors, instead of depositing into someone else's. A curator with a thesis (proprietary flow generating predictable short-duration demand) beats "we use Morpho"
Layer C: listed cards as external collateralMerchant-honoured face value and transferable, so a liquidator could actually sell it, unlike a claim on our escrow. First version where a Blue market would not be decorative. Still not needed
Cash advance persistence~6 Prisma models. Blocks the entire cash advance (credit_line)
Subscription standing ordersRecurring demand against recurring supply
Amazon reveal-then-verify portalReveal, adapted for a merchant that verifies afterwards
Loyalty programmesaccount_bound_loyalty is a form factor nothing implements
The issued cardOur own card for redemption. Needs BIN sponsorship

🔴 Open issues ​

Dealbreakers

Each of these alone can kill the product. All of them have to hold at once.

  1. The register takes one or two cards. A barcode cannot be cycled fast: each card is its own scan, and the cashier often keys a gift card tender each time. In store that is one card per purchase, two at most. The router loses most of its combinations, and small leftovers pile up.
  2. Time at the register. Tap to barcode is about 3 seconds in dollars and about 10 by card, before the cashier scans anything. Every extra card adds a scan and a Next. Past a few seconds the shopper pays the usual way.
  3. The scan nobody can see. At a store we cannot read, a buyer can claim a card did not scan and nothing settles it (the problem below).
  4. Demand has to match supply in shape, not just in total. With one card per purchase, a card only sells to a total near its balance. A $50 card is dead weight at a store whose baskets are $17. Dust needs a buyer that can use many small cards: agents shopping online, who are not time sensitive and see every card applied.
  5. A card we issue is not the way out for now. One tap would pay the store, but the gift cards then stop paying the register: we front cash and drain them online later. That reopens float, per-brand online demand, and sellers draining cards we already paid for, on top of months of bank and Apple approval. Revisit only once agent spending online is proven per brand.
  6. Haptics. The web app can buzz only inside a tap, and a barcode scan sends the phone nothing, so there is no "it worked" signal at the register.

How it works anyway. In store: one card plus the rest paid the usual way, at stores whose balances we can read, picked as close to the total as possible, leftovers relisted. Online and for dust: a Salvo tool agents call for a discounted card. Stores we cannot read: sweep-only, or skipped.

Headline open problem: returns while a credit-line loan is outstanding

The biggest unsolved item in the credit line. When a buyer returns an order while the cardholder has borrowed against its escrowed payment, the store's refund goes back to the cardholder's card and the escrow refunds the buyer. The loan is then backed by nothing, and the lender depends on the cardholder repaying. Candidate fixes: no borrowing against returnable orders at first, lower borrowing limits, borrowing only for cardholders with history or a parked balance, and automatic repayment from that balance on a return. Not solved yet. See DESIGN_UPDATE.md section 3.

It does not touch gift cards. Every credit-line lending market is isolated, so a loss in one stays in that market.

Headline open problem: a scan nobody can see

At a store where we cannot read the card's balance, a buyer can scan, get the goods, and tap "it didn't scan". We cannot tell. Today that refunds the buyer and the seller eats the card. Most stores are unreadable to us today: only Best Buy and Nike have balance readers.

The fix in the design now: the next scan is the ledger. A failed-scan claim freezes the buyer's money and puts the card first in line. The next buyer's scan decides it: works, the first buyer is refunded; fails, the first buyer is charged. No reads needed. The hole: a seller who spends their own card after listing looks the same as a lying buyer.

Better, in order:

  1. Exact-amount cards. Where the brand allows balance transfers (Target, Walmart, Amazon), move exactly the register total onto a fresh number only we hold at the tap, and show that. The seller cannot spend it, the buyer never sees a leftover, no residual cap, no deposit. A drained card can only be the buyer. Removes most of the leftover machinery too.
  2. Our own retailer account as the ledger. Load the seller's card into a Salvo-held retailer account; its wallet and purchase history show the balance and every transaction with no bot-walled scraping. Per retailer; account lockouts are the risk.
  3. Processor APIs (Blackhawk, InComm, SVS): the ledgers behind most brands, one deal each. Business development.
  4. Next scan as the ledger everywhere else, with a seller bond and buyer caps as the tiebreaker.

Decided 2026-10-22, the build: two balance reads per sale, through scrapers. One right before the barcode shows (balance X; a drained card simply does not sell), one right after "it didn't scan" (balance Y). Y below X means value left during the buyer's 90-second window, so the buyer pays; Y equal to X, the buyer is refunded. A seller draining the card inside that exact window is a coincidence treated as the buyer. No transfers, no retailer accounts, no change to the leftover machinery. Works at every store with a balance page, which is nearly all of them. The scraper layer: a per-store adapter in a headless browser, residential proxies, a captcha service, fingerprint rotation; one read per listing plus two per sale, a few cents each. Target first. Public pages only, so no account lockouts; the account and transfer route stays for later.

The leading fix, decided 2026-10-23: the register talks back through a card we issue. A card tap carries the amount: with a card we issue (Lithic, Marqeta, Stripe Issuing), every tap reaches our server in about a second as "auth for $X at this store". So: gift cards are scanned first, and the buyer taps our card for what is left. That amount tells us exactly what the gift cards paid. $50 total and an auth for $1.87 means the cards covered $48.13; an auth for $50 means a card did not scan. A "didn't scan" claim is checked by the terminal itself, at every store, with no balance page. No tap, no claim. The till always leaves a few cents for the tap, so the proof always exists. Zero extra steps: it is the card tap people already make after gift cards. Optional: a first tap that we approve for a penny reads the total, so nobody types it. Cost: becoming a card issuer (sponsor bank, approval, a funded account the remainders draw from, reimbursed from the buyer's balance).

Two lanes on one engine. Quick: one big card that covers the total, one scan and one tap, charged only for what the tap says it paid, the rest back on sale; a slightly lower discount. Savings: several cards drained in order, the odd amounts first, a better discount plus a bonus for clearing them.

Still useful underneath: balance readers where a public page exists (two reads per sale), and the next-scan floor where nothing else does.

Rejected: the phone's camera watching the register (the buyer controls the phone), capped insured refunds (pays liars), anyone phoning a balance line. Gift cards over NFC at the register do not exist; the issued card is the only tap that talks back, and it must never be the thing that buys the cards.

Returns refund onto a sold card: an edge case, mostly solved ​

A buyer returns goods in store; the store refunds the gift card number. The buyer paid for that value, so it is theirs to spend again, and we are not in the loop or on the hook. The edge: the seller still knows the number too, and could race the buyer for the restored balance. Sellers are unlikely to watch dead cards for returns, so this is parked. Circle back once there is volume. The cash advance has no such case: we own the order, so returns go through us. Decided 2026-10-22.

The tap takes about 3 seconds, and the goal is 2 or less ​

From the tap on Pay to the first barcode, in dollars: 3.9 s and 2.4 s (measured live on 2026-10-22, a desktop browser against the test network and Lit), down from 4.98 s before the speed work. By card it is about 10 s on a desktop server reaching our database across the internet, estimated at 2 to 4 s on Railway; nobody has timed it there yet. What is left in dollars is mostly the release (our server's chain checks, then one Lit run) and the payment getting into a block. Three seconds is acceptable for version 1. Version 2 reaches about 2 s with our own chain node and the release overlapping the payment; under 2 s needs version 3 (our own enclave service, with its trust trade-offs). Not yet measured at all: a phone on cell data. See How fast the till is. Unblocks: a timing run on Railway and on a phone, then the version 2 changes.

Nobody has reviewed the money transmission structure ​

The plan: an instant principal flip, non-custodial settlement, and Stripe Connect for cards. None of it is built, and no lawyer has looked at it. Today's card holds land in our own Stripe account, which is only acceptable while every listed card is the operator's. The public sell screen breaks that today: it records a stranger's card as the operator's. See Money transmission. Unblocks: a fintech lawyer, before any real money or any third-party card.

The offer book is public ​

GET /api/supply/offers lists every open card's store, value, price, discount and last four digits. A bot can watch it and take the best cards first. Harmless while every card is the operator's; not once other sellers list, and a stranger can already list through the public sell screen. See Hidden intents. Unblocks: building commit-reveal, or simply not publishing per-card terms.

Our server's signature is Lit's unlock rule ​

Lit now runs the unlock rule inside its enclave, and a card's key exists only there. But the only rule we can write today is "our server signed this", so we can still open any card alone, and the guard refuses the Lit vault for other people's cards. The rule has to read something we cannot fake: an escrow deposit on chain. Unblocks: the escrow contract, and a product decision that a deposit cannot be refunded once its card has been opened, unless the seller agrees. The Lit side is built, and its live smoke passed on 2026-10-10. The escrow is deployed on testnet but not reviewed.

Stripe has to approve gift card resale ​

Stripe lists selling stored value issued by someone else as a restricted business, allowed only with approval. Pay on scan refuses a live key until then. Running without approval risks a closed account, held funds, and the card networks' blacklist. See Where the money goes. Unblocks: asking Stripe. Plan B is a high-risk processor, at higher fees.

The cash advance has no persistence, which blocks the whole product line ​

The schema has Order, OrderEvent, Intent, ProcessedEvent. Nothing for LP accounts, reputation, auctions, obligations or claims. So matchCreditLine has nothing to match, releaseHoldback never fires, and obligation accounting is inert. Unblocks: a migration and a product decision.

76 dead exports remain ​

.dead-exports-baseline.json accepts them as backlog. The build fails on any new one. Most are cash advance and seller-account code, blocked on the persistence decision. Unblocks: mostly the same migration.

Issuer-risk history is inert ​

recordIssuerOutcome has no production caller, so history stays empty and Phase 8 tightening never engages: the same failure as the velocity window. Deliberately not wired to in-memory state: a fake fix that would recreate the bug. Unblocks: somewhere to store it.

Barcode symbology is unverified ​

Nothing in the repo verifies the Best Buy profile's declared Code 128. A wrong symbology fails silently at a till. Unblocks: one scan at a register, since the reveal page shows three labelled candidates.

The undetermined settlement case ​

If the post-window balance is unreadable, escrow is held: a refund could pay a buyer who spent the card. The one in-store outcome that does not resolve automatically. Unblocks: nothing. It is a decision.

The contested case has no claim table ​

A contested reveal opens a real claim. With no table, it is stored as a marker on the intent, and escrow is held in full, not partially released. Weaker than designed one way, stronger where it matters. Unblocks: the same migration.

Toast is permanently balance-blind ​

Unblocks: nothing, not even a partnership. No lookup exists for anyone.

Only Best Buy and Nike have balance readers ​

Neither reader has read a real card yet. Target and Walmart are listed too, but we cannot read their balances. A sale there relies on the seller's stated value and the buyer's word. A holdback is computed but not enforced, because nothing pays a seller. For Best Buy and Nike, decideReveal still refuses an unknown balance. Unblocks: for Walmart, a balance read Walmart allows. In one test its public form showed a bot wall to a datacenter address. For Target, one test with a real card to see whether its form needs a login.

Online redemption never hands over the card ​

executeIntent unseals the card but never passes it to the adapter, so an online redemption stops with "no card material" and refunds the buyer. The race check on that path passes for the same reason. Nothing in the app starts one today. The operator script that can does not check the form factor, so pointed at an in-store card it would drive the online adapter. See Three kinds of gift card. Unblocks: passing the unsealed card through, then one supervised live run.

The balance sweep cannot read balances in production ​

The sweep builds the Best Buy and Nike adapters with no way to fetch card material, so it reports every Best Buy and Nike card as unknown and available. Target and Walmart get no sweep adapter and are reported as unsupported. A card the seller drained stays listed until someone tries to use it. Unblocks: wiring card material into the sweep, as the reveal already does.


⚪ Decisions parked ​

Square viability → option C ​

Square's gift_cards_allowed is per-order and server-side, so read-only access cannot decide viability. Three options: (A) one abandoned cart per venue per 90 days, (B) per-venue OAuth, (C) accept Square closed. C was chosen and not reopened.

Option A's harness is built, gated off behind SQUARE_ALLOW_CART_PROBE in case that changes.

Issued-card BIN sponsorship ​

An issued card would collapse several problems into one, but needs a sponsor bank and a compliance posture we lack.

The public-repo question ​

The repo is private. Publishing it would expose the mechanism design, most of the moat.