Appearance
How fast the till is
From the tap on Pay to the first barcode on screen: what we measured, what takes the time, and how far each version can go.
The short answer
- Dollars, after the speed work: 2.88 s and 2.40 s (measured live, 2026-10-14, two taps by two shoppers, the second buying the first one's leftover). The same morning, before it, one tap took 4.98 s. What is left is the release (1.3 to 1.8 s: our server's chain checks, then Lit) and the payment getting into a block (0.6 to 0.84 s).
- Card: 9.98 to 11.1 s on a desktop server (measured live, 2026-10-14). Most of that is the desktop reaching our database across the internet. On Railway it should be 2 to 4 s (estimated). Only a run on Railway will say where in that range.
- Version 1 is 3 s. Dollars meets it today, measured live on a desktop. Card is estimated to; a run on Railway will say.
- Version 2 can reach 2 s in dollars, still on Lit's service: about 1.4 to 2.0 s (estimated). It needs one new escrow contract, so every listing is sealed again, once. Mainnet needs a new escrow anyway, so the two should be one move. By card, 2 s depends on Railway: 1.3 to 2.1 s if the lower estimate holds, 3.0 to 3.4 s if the higher one does.
- Toward 1 s, and under it, needs version 3: our own always-on enclave service in place of Lit's, and Tempo's finality proofs in place of two hosts agreeing. The proofs make us harder to censor through the chain hosts, but not through Tempo's validators or the stablecoin's issuer. The enclave service makes us an operator, so its upgrades need a timelock and signers we do not hold alone.
- The first things to measure are how long Lit takes to start a run, a phone on cell data, and a run on Railway. Together they decide whether version 2 reaches 2 s on both rails.
How to read the numbers
Every number says how we know it, and when. Every live tap on this page is one tap (n=1) unless it says otherwise.
- Measured live: a real browser against the real services: Tempo's test network, Lit, and the card processor in test mode. Our server ran on a desktop, not on Railway, and reached the database through Railway's public proxy, where a bare round trip cost about 95 ms.
- Measured locally:
pnpm till:timingon one machine. Our real code, with stand-ins for every outside service, sometimes with delays added. - Estimated: worked out from the measurements and from public docs and code. Not timed.
Version numbers on this page are the till's. SalvoEscrowV2 is the escrow contract on the test network today. Version 2 of the till needs a new one, called SalvoEscrowV3 here.
Where we are
In dollars
After the speed work
Measured live on 2026-10-14, later the same day: the same setup, two shoppers. Shopper A paid $28.91 for $36.13 on two cards; shopper B, seconds after A tapped Done, paid $3.10 for A's $3.87 leftover. The tap is at 0 s.
| Step | Shopper A | Shopper B | What happened |
|---|---|---|---|
| Waiting for tickets | 0.05 s | 0 | Asked for as the price came back, 1.9 s before A's tap |
| Reads before the send | 0.18 s | about 0 | One round, both hosts, side by side |
| Send | 0.60 s | 0.84 s | One batch payment, answered once it was in a block |
| Release | 1.76 s | 1.31 s | Asked on the first host's receipt and answered on the first ask: our server's checks on both hosts, then Lit |
| Drawing the barcode | 0.24 s | 0.19 s | |
| Tap to first barcode | 2.88 s | 2.40 s |
Two taps, one desktop, so treat these as a good day rather than an average. The release is now the largest part, and inside it our own server's chain checks are as large as Lit's run (see below).
Before the speed work
One tap, two cards. Measured live on 2026-10-14: a headless browser on a desktop, our server on the same desktop, Tempo's test network and Lit. The tap is at 0 s.
| Step | Starts at | Took | What happened |
|---|---|---|---|
| Waiting for tickets | 0.00 s | 0.93 s | Two ticket requests, one per card, asked for 1.47 s before the tap, 2.4 s each, side by side. After the tap: Lit finishing (0.28 s), then our ticket route reading the chain (0.65 s) |
| Reads before the send | 0.93 s | 0.76 s | 20 calls in 9 rounds, some overlapping. Most rounds asked both hosts; the last two asked Tempo's host only |
| Send | 1.70 s | 0.34 s | One batch payment, sent and answered once it was in a block |
| Release asked too early | 2.05 s | 0.42 s | Answered "not there": dRPC was a block behind |
| Second host agreeing | 2.47 s | 0.61 s | Five rounds of two calls, one per host. They began at 2.42 s, while the early ask was still answering. Then 0.20 s idle before the release was asked again |
| Release | 3.08 s | 1.81 s | Our server's own checks on both hosts 0.93 s, a database read 0.09 s, Lit 0.78 s |
| Drawing the barcode | 4.89 s | 0.08 s | |
| Tap to first barcode | 4.98 s |
A ticket is Lit's signed permission to buy one card, naming this shopper, made before any money moves. Each ticket request took about 2.4 s: one database read (about 0.19 s, the desktop's proxy cost), Lit (1.41 s and 1.54 s), then about 0.65 s of our ticket route reading the chain on both hosts, round after round. Those reads check the card rules only the contract enforces: who may sell the card, the new-seller cap, and a card listed twice. Both Lit runs had finished 0.28 s after the tap. So of the 0.93 s the tap waited, 0.28 s was Lit and 0.65 s was our own reads.
The release was not one 1.8 s Lit run either. Lit took 0.78 s. About half of the release (0.93 s) was our own server checking the chain on both hosts before it paid for a Lit run.
| Other numbers | Value | Kind |
|---|---|---|
| A block on Tempo's test network | Every 0.61 s, final at once | Measured live, 2026-10-13 |
| A call from the page to Tempo's own host | 58 ms typical; 9 in 10 under 79 ms | Measured live, 2026-10-14, 242 calls |
| A call from the page to dRPC | 100 ms typical; 9 in 10 under 227 ms | Measured live, 2026-10-14, 220 calls |
| A call from our server | Tempo's host 65 ms typical, dRPC 113 ms typical | Measured live, 2026-10-14, 55 calls each, from the desktop |
| A shopper's first call to each host, on a fresh page | 0.16 to 0.24 s | Measured live, 2026-10-14, 4 calls |
| The seller's page, its first chain calls | 1.92 to 1.96 s, cause not known | Measured live, 2026-10-14, 4 calls |
| A Lit run, each one timed | 1.41 and 1.54 s for the tickets, the first Lit calls of the run. 0.77 and 0.78 s for the releases, about 3.7 s later | Measured live, 2026-10-14, 4 runs |
| A Lit run, on average | 1.22 s over 15 runs. Only the total was kept | Measured live, 2026-10-13, the escrow smoke |
| What a Lit run costs | About a cent: 15 runs cost 15 cents | Measured live, 2026-10-13 |
| A chain transaction in the escrow smoke, with the old way of waiting for its receipt | 1.81 s on average over 16: listings, payments, reports and refunds, each waited for on both hosts | Measured live, 2026-10-13 |
| The escrow smoke, pay and release | 5.45 s: pay 4.01 s, then release 1.43 s, with the ticket already in hand. The ticket took another 2.22 s | Measured live, 2026-10-13, one sale |
| Our own code, with no network delay | 0.64 s | Measured locally, 2026-10-14, 2 runs on a desktop |
That last row is not a hard floor. It still includes the chain inside the process and the pinned Lit code running on a fake Lit, whose release alone took 206 ms. A faster service of our own could go under it (see version 3).
Next and Done in dollars made no network calls in that run. Each took about 0.12 s, saving a screenshot included (measured live, 2026-10-14, one tap). Offline, Next took 82 ms and Done 148 ms (measured locally, 2026-10-14). The new Done step posts a proof to our server, and is not measured yet.
By card
The card rail never touches the chain or Lit. Pay places a hold on the shopper's saved card for each gift card and one for the deposit, then our server opens card 1's number from our own vault. The card processor ran in test mode.
Measured live on a desktop server, one tap per run. Each number is how long our server took to answer that request.
| Step | 2026-10-13, before fixes | 2026-10-14, after fixes | On Railway, estimated |
|---|---|---|---|
| Price, while typing (before the tap) | 1.47 s | 1.53 s | About 1.1 s, worked out before the fixes |
| Pay: tap to first code | 15.1 s | 9.98 s | 2 to 4 s |
| Next | 7.6 s | 4.24 s | Not worked out. Before the fixes: about 2.7 s |
| Done | 5.3 s | 3.99 s | Not worked out. Before the fixes: about 1.3 s |
| Shopper B, price | 0.57 s | 0.77 s | |
| Shopper B, Pay (one leftover card) | 8.0 s | 7.64 s | About 2.4 s, worked out before the fixes |
| Shopper B, Done | 3.3 s | 2.11 s | About 0.7 s, worked out before the fixes |
Each figure "worked out before the fixes" takes 93 ms per query off the desktop time, a method with a weak base (see below). The run after the fixes kept no query counts for Next or Done, so those cannot be worked out for it.
The browser's own times, tap to the barcode on screen, were reported as 15.6 s before the fixes and 10.1 s after. No saved file holds them.
Inside Pay:
| Before fixes (2026-10-13) | After fixes (2026-10-14), reported | |
|---|---|---|
| Database queries | 86 | 67 |
| Card processor calls | 18, 6.6 s in total | 10 |
| Customer read | 5 times | 2 times |
| Allowance list (the $200 limit check) | 7 times | 4 times |
| Holds | 3, one after another: 0.71 s, 0.97 s, 2.43 s | 3 at once: 0.6 to 1.05 s each |
| Read back | 2 times | 1 time |
The before-fix column is in the saved breakdown. The after-fix column was reported from the run, but its server log was overwritten, so no saved file holds it. Treat it as unchecked until the rerun.
From the desktop, a bare database round trip took about 95 ms (measured live, 2026-10-13 and 2026-10-14). The only per-query times on file are 41 queries from the dollars run on 2026-10-14. About half took 90 to 95 ms and half 180 to 195 ms, 179 ms typical, which looks like two round trips each. A card processor call took 100 to 160 ms (measured live, 2026-10-13). On Railway's private network a query should take 1 to 2 ms, and a card processor call about the same as from the desktop (estimated).
Why "2 to 4 s" on Railway. Two ways of working it out disagree, and neither is solid. Adding up the card processor's calls gives 2 to 2.5 s. Taking 93 ms per query off the desktop time gives about 3.75 s. The 1.3 s between them may be queries that take two round trips, which the second method counts as one. If so, the lower figure is nearer. But the three holds now overlap, so taking time off query by query is unreliable either way. Only a run on Railway settles it.
What takes the time
In dollars, ranked
The 4.98 s tap, measured live on 2026-10-14, one tap.
Reading the chain, one round at a time, on two hosts: 3.17 s, about two thirds.
- Our server's checks inside the release: 0.93 s.
- The phone's reads before the send: 0.76 s.
- Our ticket route checking the card's rules after Lit answered: 0.65 s. Nothing on this page named this step before. It is a lever of its own.
- The release asked too early: 0.42 s.
- The second host agreeing: 0.41 s.
Both hosts must agree on every read, so each round waits for the slower one, dRPC. dRPC was also often a block behind, so we waited for the next block. Tempo's own host answers bursts with "too many requests", and our retries then wait 0.25 s, 0.5 s, then 1 s.
Lit: 1.05 s, about a fifth. The end of the two ticket runs after the tap (0.28 s), and the release's run (0.78 s). Each Lit run starts in a fresh worker: Lit's open-source code runs exactly one request per worker, then drops it. We infer, but have not confirmed, that each run's connections to the chain hosts also start cold: the code shows one shared web client, and nothing on how a run reuses connections. Our own pinned code then reads the chain twice in a row before it decides. Lit publishes no speed figures and sells no faster tier. It charges $0.01 a second, with a one-second minimum.
The send: 0.34 s. Half a block on average, plus one round trip. This is close to the floor. We keep it.
The rest: 0.42 s. The pause before the release was asked again (0.20 s), a database read in the release (0.09 s, the desktop's proxy cost), drawing the barcode (0.08 s), and small gaps between steps.
Tempo has no early confirmations. Its "notarized" signal comes before "finalized", but notarized is not final, so it can never release a card.
After version 1 (estimated), the order changes: the release first (1.2 to 1.8 s, our own checks before Lit included), then the slower host's next block (0.3 to 0.6 s), then the send (0.34 s). Version 2 goes after those three, and after the ticket route's reads.
By card, ranked
The 9.98 s Pay, measured live on 2026-10-14, one tap.
- The database, from a desktop: the biggest share, not timed directly. 67 queries at one round trip each would be 6.2 s (worked out, not timed), and some take two. This is the desktop, not the design. On Railway it is about 0.1 s (estimated).
- The card processor: most of the tap on Railway. Ten calls at 100 to 160 ms each: the customer twice, the allowance list four times, the slowest hold, one read back.
- The slowest hold. The three holds now run at once, so the tap waits for the slowest: 0.6 to 1.05 s (reported). On 2026-10-13 one took 2.43 s. Every hold so far was in test mode. Real bank authorizations are not measured.
- Maybe a cold first request. Shopper A's price took 1.47 s and 1.53 s. Shopper B's, on the same route 30 to 40 s later, took 0.57 s and 0.77 s. That points to a cost on the first request to that route, more than to its four queries. It may slow A's Pay too. Not confirmed.
Version 1: 3 s
Three seconds from the tap to the first barcode, on either rail, is the line for version 1 (set on 2026-10-14). The timing script's targets, 1.5 s by card and 2 s in dollars, move to version 2.
What ships:
| Change | Rail | What it removes | Status |
|---|---|---|---|
| The release waits for a host a block behind, instead of answering "not there" | Dollars | The 0.42 s early ask and the 0.20 s pause before the retry. The wait for the block itself stays | Measured live: the release answered on the first ask |
| Fewer reads before the send, all at once | Dollars | Most of the 0.76 s | Measured live: 0.18 s |
| Tickets asked for 0.4 s after typing stops | Dollars | Part of the 0.93 s ticket wait: each second the tickets start earlier is a second less to wait | Measured live: 0.05 s |
| A paid card is no longer held for its buyer's quote, and a leftover is freed when its buyer taps Done | Dollars | The next shopper being told there was nothing (found live, fixed the same day) | Measured live: shopper B bought A's leftover seconds after A's Done |
| The customer read twice, not five times. The allowance list four times, not seven. The three holds at once. One read back | Card | 5.2 s on the desktop (15.1 s down to 9.98 s) | Measured live on a desktop, 2026-10-14, one tap |
Not built yet: fewer rounds in the ticket route's own chain reads (0.65 s on 2026-10-14). If dollars misses 3 s, that is the first lever. It is in the version 2 table below, and needs no new escrow.
Where that should land:
| Rail | Tap to first barcode | Kind |
|---|---|---|
| Dollars | 2.8 to 3.2 s | Estimated, 2026-10-14, before the live run |
| Dollars, live | 2.88 and 2.40 s | Measured live on a desktop, 2026-10-14, 2 taps |
| Dollars, offline with the measured delays added | 2.41 and 2.50 s | Measured locally, 2026-10-14, 2 runs |
| Card, on Railway | 2 to 4 s | Estimated, 2026-10-14 |
The offline run adds 70 ms to every request and 1.2 s to every Lit run, and keeps the second host a block behind. It comes out quicker than the live estimate for three reasons. It has no cold connections and no busy hosts. And its send takes 0.13 s against 0.34 s live, because the chain inside the process does not wait for Tempo's next block. Two runs say nothing about the spread.
Version 1 is met when a run on Railway shows a median of 3 s or less over at least 10 taps on both rails, and a phone on cell data agrees. Until then, 3 s is likely but not shown. If the card rail misses it, the card changes in version 2 below need no new escrow, so they can move into version 1. Done with the database's own advisory locks, they need no schema change either. A reservation table would need one.
Version 2: the path to 2 s
In dollars, about 1.4 to 2.0 s looks feasible in version 2, still on Lit's service. By card, 2 s depends on what Railway shows. Every saving below is estimated (2026-10-14) unless it says measured.
One new escrow, once
SalvoEscrowV2 fixes the Lit key it trusts when it is deployed, and nothing can change it. Each card is sealed to a key that comes from the fingerprint of the code Lit runs. So any change to that code means a new escrow contract, and each seller seals their listings again. V2 listings keep working on V2 until they do.
Mainnet needs a new escrow anyway: the pinned code names Tempo's test network (chain 42431) and its two hosts. So every change to Lit's code goes into one release, SalvoEscrowV3, shipped with the move to mainnet, never one at a time. Sellers then seal once for both.
Dollars, with no new escrow
| Change | Saving | Cost |
|---|---|---|
| Fewer rounds in the ticket route's own reads. After Lit answers, our route checks the card's rules on both hosts, round after round. One or two batched rounds would do | About 0.3 to 0.5 s of the ticket wait on a fast tap. 0.65 s was measured live on 2026-10-14. Nothing once tickets come when the till opens | None, as long as the check still runs before the ticket goes back |
| Tickets when the till opens, for the cards the till would pick, or with the price | Whatever ticket wait is left after version 1. On 2026-10-14 a tap 1.47 s after the tickets were asked for still waited 0.93 s (measured live, one tap) | About a cent of Lit for each ticket never used. At Best Buy and Nike every ticket reads the card's balance on the store's own site, from inside Lit. Tickets at every till open multiply those reads on visits that buy nothing, which makes us easier for an issuer to spot and block. Early tickets at Target and Walmart only, where no balance is read, avoid that. Tickets do not name the amount and last 5 minutes (Best Buy, Nike) or 10 (Target, Walmart). One more paid request to limit. A shopper who takes over 15 s makes the tap read one round again (0.1 to 0.25 s) |
| Warm connections, and a warm Lit. Our server keeps its connection to Lit open, and pings Lit when a till opens. The phone touches each host every 20 s | 0 when already warm. A shopper's first call to each host took 0.16 to 0.24 s against 0.06 to 0.10 s warm (measured live), so about 0.1 to 0.18 s per host. A cold connection to Lit: 0.1 to 0.25 s. The first two Lit runs on 2026-10-14 took 1.41 and 1.54 s, the next two 0.77 and 0.78 s, a hint of 0.6 to 0.7 s extra on a first call (not confirmed) | None for a connection. Node's web client drops an idle connection after 4 s by default, unless the server asks for longer (up to 600 s); whether Lit's does is not checked. If the extra time is on Lit's side, warming it takes a run: about a cent per till opened |
| A live block feed on the phone. One open connection per host that announces each new block, instead of asking every 100 ms | 0.1 to 0.25 s | Tempo documents only its own consensus feed, over WebSocket, served by validators and certified follower nodes, so a public host may not offer it. The standard new-block feed needs a test |
| Draw on release. Show the code when Lit's answer arrives, and mark the purchase paid when the second host agrees | 0 on a typical tap, 0.1 to 0.5 s on a slow one | The receipt says "confirming" until the hosts agree. Nothing is lost on the card: Lit checked both hosts before it opened it |
Dollars, in the one new escrow
| Change | Saving | Cost |
|---|---|---|
| Start Lit while the block is made. The phone asks for the release the moment it sends, naming the payment. The new code waits until both hosts show the payment in a block, then checks it as today | 0.3 to 0.9 s. The send, the slower host and our own check all happen while Lit starts up. The biggest single saving on Lit's service | Lit bills the waiting ($0.01 a second, one-second minimum). A payment that never lands burns a few cents, so the limits per address stay. Lit allows 50 outside requests a run. The read check uses 4 and each poll costs 2, one per host, so about 23 polls: a wait of up to about 3.4 s at one poll every 150 ms. A slower block or a burst of busy answers runs out, and the run is refused but still paid. Polling one host until it shows the payment, then both, would roughly double the room |
| One read round inside Lit, at the payment's own block, instead of two in a row | 0.08 to 0.25 s | None beyond the new escrow |
| Our own Tempo node in place of free dRPC. Tempo's own host stays as the other witness | 0.25 to 0.5 s. dRPC's lag of a block becomes about 0.1 s, and its limit of three calls a batch goes away | A bare-metal server: at least 16 cores, 32 GB and 1 TB of local NVMe, no network disks, so not Railway (Tempo's requirements). About $150 to $400 a month, a researcher's guess, not a quote. We run it. If it stops, releases stop, as they do today if dRPC stops |
| Or a paid provider in place of free dRPC | dRPC's lag and batch limit, if the provider keeps up. Whether it lags is unknown | Its key would sit in Lit's public, frozen code, so it needs a spending cap. It does not end "too many requests": those come from Tempo's own host, which stays |
| Replace Tempo's own host, or add a third so any two that agree will do | Ends Tempo's "too many requests" retries: 0.25 to 1.75 s on a slow tap, about 0 on a typical one | It changes who the read trusts. Two hosts we run, or one we run and one we pay, must never be enough to agree on their own. The proofs in version 3 end the retries without that trade |
With our own node, the two-host rule is as strong as today. Tempo's host must still match ours byte for byte, so we cannot lie alone. It does nothing for censorship, and it keeps Tempo's busy answers, since Tempo's host is still one of the two.
By card
No escrow, so these can ship at any time.
| Change | Saving on Railway | Cost |
|---|---|---|
| Batch the database reads, and read the customer once | 0.06 to 0.12 s | None |
| Read the saved card and the allowance list when the till prices, and keep them about 30 s | 0.15 to 0.3 s | Safe only with a guard at each hold. Today that guard is listing the holds again after each one, which the next change removes. So this ships with the lock, or not at all |
| A lock per customer in our own database, in place of listing the holds again after each one | 0.15 to 0.3 s | Every hold must reserve first, so the $200 limit stays as strict. The lock then becomes the guard the change above relies on. An advisory lock needs no schema change; a reservation table would |
Together they save 0.36 to 0.72 s. From the 2 to 2.5 s Railway estimate, version 2 by card lands at 1.3 to 2.1 s. From the 3.75 s method, about 3.0 to 3.4 s. If the higher figure holds, the time is somewhere we have not found yet, and the rerun with a saved server log comes first.
Version 2, added up
| Step, in dollars | Version 2, estimated |
|---|---|
| Ticket wait | 0, fetched when the till opens |
| Reads | 0 to 0.25 s: one round again if the till's last read is over 15 s old, or its prepared payment over 30 s |
| Sign | 0.04 s |
| To our server and on to Lit | About 0.1 s from a desktop talking to itself. From a phone on cell data, and from Railway to Lit, not measured |
| Lit starts up while the payment is sent and reaches both hosts | 0.45 to 1.0 s, whichever of the two is longer |
| Lit reads once at the payment's block, opens the card and answers | 0.3 to 0.6 s |
| Draw | 0.1 s |
| Tap to first barcode | About 1.4 to 2.0 s |
The steps add up to 1.0 to 2.1 s. The low end needs Lit as fast as its quickest runs on file (0.77 s, a warm release), not the 1.41 to 1.54 s of the ticket runs or the smoke's 1.22 s average. It also needs a phone and Railway as quick as a desktop talking to itself. Two separate research estimates put version 2 at 1.4 to 2.0 s and 1.5 to 1.8 s, so this page uses 1.4 to 2.0 s. Most of that range is Lit's start-up, which we have not measured.
In what order
- Measure. Run an empty Lit Action a few times, about a cent each, then a probe that records the time after each step. Time a call from Railway to Lit's API. Run both rails on Railway, 10 taps each. These numbers decide whether version 2 reaches 2 s, and whether version 3 is needed for that at all.
- The changes that need no new escrow: the ticket route's reads, warm connections and a warm Lit, the block feed, drawing on release, and the three card changes (the cached allowance list only with the lock). Then tickets when the till opens, once the extra balance reads at Best Buy and Nike are weighed.
- A trial Tempo node, logging how far it trails Tempo's own host, before it is written into Lit's code.
- The new escrow, with every change to Lit's code at once, shipped with the move to mainnet. If the finality proofs below are ready by then, they go in too, so sellers seal their cards again only once.
Version 3: below 2 s
Version 2 still pays for a fresh Lit worker on every run. To get well under 2 s, toward 1 s, the release has to come from something that is already running, and the chain check has to stop waiting for the slower of two hosts. Each of those is a trust decision as much as a speed one. All figures here are estimated, 2026-10-14.
The options
| Option | Tap to first barcode | Who you trust with the keys and the code | Who can switch it off |
|---|---|---|---|
| Lit's service (today, and version 2) | 1.4 to 2.0 s in version 2 | Lit's enclave, on Phala and Intel TDX. Code changes need Lit's multisig: 2 of 4, all Lit's, no timelock | Lit, which can cut off our API key, and Phala, which hosts Lit |
| Lit's code on our own hardware | 0.1 to 0.4 s quicker than Lit's service | The enclave's hardware maker, and whoever approves our releases | Us, and our hosting provider |
| Our own enclave service on Phala | 0.6 to 1.2 s | Intel's hardware, Phala's key service (governed by Phala's own 2 of 3 multisig), and our upgrade multisig, which needs a timelock and signers who are not us | Us, as the one operator, and Phala, which hosts it and governs its key service |
| Amazon's enclaves (AWS Nitro) | About the same as Phala | Amazon's hardware, Amazon's key service, our Amazon account | Amazon, and us |
| Oasis ROFL | About the same as Phala; not sure | Intel enclaves run by an open pool of operators, with keys from Oasis's key manager | Harder for any one party: its docs describe an open pool of node operators and deployment that cannot be censored. Less mature |
| Plus Tempo's finality proofs, on any of the above | 0.3 to 0.7 s less again | Tempo's validators' signature, instead of two hosts we name | Not the hosts: any host will do, and a host can hold data back but never lie. The proofs do nothing about Tempo's validators or the stablecoin's issuer (see "The trade, plainly") |
Our own enclave service with the proofs: about 0.5 to 0.8 s (estimated, not very sure). That is at or below the 0.64 s our own code takes offline today, so our own code has to get faster too, for example crypto done natively in the enclave instead of in the pinned JavaScript.
Our own enclave service
The best candidate. A small program of ours runs all the time inside an Intel TDX enclave, on Phala's dstack or on our own TDX machine. It gets its keys from Phala's key service, keeps warm connections to both hosts or a live block feed, and does its crypto natively. The phone calls it directly, over a connection that proves which code is running.
- Speed. A release takes 0.05 to 0.3 s once the second host has the block. Tap to first barcode: 0.6 to 1.2 s.
- Custody. The keys belong to our app's ID, not to a fingerprint of its code, so they survive upgrades. That is good: later speed changes do not make sellers seal their cards again. It is also the risk: whoever approves an upgrade could ship code that reads cards. So every upgrade waits behind a timelock, approved by signers we do not control alone, as Custody requires. Done that way, it is no weaker than Lit's service, whose multisig is Lit's alone, with no timelock. Done carelessly, it is a vault we can open alone, and that is custody.
- Censorship. One operator is one place for an issuer to push, and Phala, which hosts it and governs its key service, is a second. Phala's dstack can let our app run on other machines, but only ones that passed Phala's onboarding and are part of Phala Cloud's own fleet, and its docs do not say those copies get the same keys. Every copy would still take its keys from Phala's key service. So copies run by others, outside Phala, are not something Phala offers, as far as its docs say. Oasis ROFL is the option whose docs describe an open pool of operators.
- Cost. We become an operator. The phone has to check the enclave's proof of what it runs. Every listing is sealed again once, to the new keys.
Lit's code on our own hardware
Lit's service is open source, and runs on Phala, on bare-metal Intel TDX or AMD SEV-SNP, on Google's confidential machines and on Amazon's enclaves. Production setups are arranged with Lit. We would get our own region and our own outbound address, so other customers' traffic could no longer get us "too many requests". But every run would still start a fresh worker, so it saves maybe 0.1 to 0.4 s. It would almost certainly mean new keys, so every listing is sealed again (not confirmed). And we would be the operator, with the same upgrade rule as above. It is a way to keep going if Lit's service goes away, more than a speed path.
Proofs instead of two hosts agreeing
Today Lit trusts a read only when two hosts we name agree. Every Tempo block comes with a finality certificate: one group signature from the validators, checked against one network key. Lit, or our own service, could check that certificate plus a proof of the escrow's storage, both fetched from any one host.
- Speed. No wait for the slower host, one fewer read round, and no "too many requests" retries: 0.3 to 0.7 s.
- Safety. Better than today. A lying host would have to forge the validators' signature.
- Censorship. Much better against the hosts. No host is written into the code, so whichever host answers first will do, ours, a provider's or a mirror's, with no redeploy. It does nothing about which payments the validators let in, or about the token's issuer.
- Cost. Weeks of work. Tempo does not document the certificate format step by step, so its encoding has to be ported from the consensus library Tempo uses (Commonware). Tempo's docs say the methods that serve certificates run on validators and certified follower nodes, so public hosts may not offer them, and a plain node of our own may not either. GetBlock's docs list them (not tried). When the validators' key changes, the proof of that change has to be checked too. Checking the signature in JavaScript inside Lit takes tens of milliseconds.
- Where it goes. It works on Lit's service or on our own enclave. If it is ready before version 2's code is frozen, it belongs in that release, with the move to mainnet.
By card
The card rail's biggest lever is to place the hold when the till opens, the way a gas pump does, then capture part of it at Pay. When the estimate already covers the total, Pay needs no new authorization: 0.4 to 0.7 s saved. When it does not, raising the hold is itself an authorization, with its own wait.
The costs:
- Pending holds on visits that buy nothing tie up a debit card's money until the bank drops them.
- The card processor wants a genuine estimate of the amount, and allows at most 10 raises on one hold.
- It needs interchange-plus pricing, which the card processor has to approve.
- Once part of a hold captured in parts is taken, the card processor allows no further raise.
- More authorizations mean more fees.
One hold for the whole purchase, captured in parts, saves 0.1 to 0.4 s and removes the slow outlier. But the card processor's docs allow that mostly for online sales of goods shipped separately, so the deposit stays its own hold, and one dispute would cover the whole purchase.
Both add compliance risk to a business the card processor already restricts. Neither is worth it before version 2's numbers are in.
Other ideas, and why they are not first
- Threshold networks. Lit's own threshold network closed on 2026-04-01. TACo publishes no latency, and each of its nodes reads the chain itself: likely 1 s or more. Not a speed path.
- Payment channels. Tempo's channel reserve, added in its T5 upgrade, holds a payer's deposit and pays out signed running totals. A channel funded ahead of time could let Lit check a voucher with no block wait, saving 0.7 to 1.2 s. Unsolved: a channel has one payee, deposits used in part do not fit yet, and paying the same money twice has to be ruled out. Research only.
- Marlin Oyster. Amazon enclaves run by marketplace operators. Its cold start is not published. Unsure.
- Lit's ChainSecured mode. Our own multisig over our Lit account. It saves no time. It is a trust change, not a speed one.
- Another chain, or another dollar token. The only answer to censorship by validators or by the token's issuer (below). It is a separate decision from speed, and any move would need timing again: Tempo's 0.61 s final blocks are part of why 2 s is reachable at all.
The trade, plainly
Lit's service is slow mostly because each run starts in a fresh worker, and our pinned code reads the chain twice in a row inside it. Pinning the code by its fingerprint costs almost nothing: Lit hashes the code locally, about 0 to 20 ms. The fresh worker is also part of why it is hard for anyone to cheat: each run starts clean, and we hold none of its keys. A service of our own is fast because it stays running, and that puts our name in the answer to "who can change the code that holds the keys".
Gift card issuers may try to stop us. The places they can push:
- the host that runs the enclave (Phala, for Lit's service and for ours on Phala), and Lit, which holds our API key;
- the chain hosts the enclave reads;
- Tempo's validators, a permissioned set that decides which payments go into a block. Tempo was started by Stripe, our card processor, and Paradigm, and Stripe and Visa joined as validators (announced on 2026-04-14);
- the stablecoin's issuer, which can set transfer rules on its token, blacklists included (Tempo's TIP-403);
- the card processor, on the card rail;
- and us.
The proofs take the chain hosts off that list. An open pool of operators, as Oasis describes, would take a single enclave host off it; Phala's dstack, as documented, would not. Nothing on this page takes the validators or the token's issuer off it. So the dollars rail takes the card processor out of the payment itself, but not out of the picture: the same company helps run the chain.
Speed and decentralisation point the same way in one place only: the finality proofs, which make us both faster and harder to censor. Everything else trades one for the other, and a timelock with outside signers is what keeps that trade honest.
Not on the table
Each of these would be faster. Each breaks the custody rule.
- Showing the code before the chain shows the payment is final. The barcode is the card. The deposit covers only what the last card leaves ($5 to $10), not what was used. Payments fail in normal use: two shoppers on the same card means one payment reverts, and others fail for ordinary reasons. It would save 0.4 to 0.9 s.
- Releasing on Tempo's "notarized" signal. Notarized is not final.
- Opening a card number before its hold is in place.
What we have not measured yet
| What | Why it matters | How we will measure it |
|---|---|---|
| Version 1 in dollars, live | Every version 1 dollars number is an estimate | Rerun the browser run exactly as on 2026-10-14, and compare request by request |
| More than one tap | Every live tap on this page is one tap (n=1) | At least 10 taps per rail in each run. Report the median and the slowest |
| Railway | Queries drop from about 95 ms to 1 to 2 ms. The time from Railway to Tempo's hosts and to Lit is unknown, and the ticket route's and the release's own chain checks depend on it | A Railway preview on the test schema with a test card key. Card: pnpm till:timing --live --base-url <url> --cookie-file <file>. Dollars: the browser run pointed at it, with the server timing hooks |
| A phone on cell data | Every phone round trip gets longer, and a dollars tap makes at least five in a row | A real iPhone in Safari on an HTTPS deploy, with the per-step marks and Safari's Web Inspector. 10 taps on cell, 10 on wifi |
| A cold first tap | A shopper's first call to each host took 0.16 to 0.24 s, against 0.06 to 0.10 s warm (measured live, 2026-10-14). The first two Lit runs took about 0.7 s longer than the next two. The 1.9 s first calls were the seller's page, not a shopper's | The first tap of a session, on a phone. Then a till that warms Lit when it opens, against one that does not |
| Lit's start-up per run | It decides whether version 2 gets under 2 s | An empty Lit Action, about a cent a run, then a probe that records the time after each step |
| How much Lit varies | Each run's own time is on file for only 4 runs, 0.77 to 1.54 s. The smoke kept only a total for its 15 | Timed runs at several hours of the day, each one recorded. This spends Lit credit |
| A Lit run that waits for the block | Its polls fit about 3.4 s inside Lit's limit of 50 outside requests | Offline first, with a slow block and busy answers, before it is frozen into the new escrow |
| The card rail's counts after the fixes | No saved file holds them, and the two Railway estimates disagree by 1.3 s | Rerun the card browser run, and keep its server log, with each query's time, in its own file |
| Why shopper A's card price takes 1.5 s | B's, on the same route, took 0.57 s and 0.77 s. A cold first request is the likely cause, and it may slow A's Pay too | The same rerun, with one warm-up request first |
| Card holds on a live key | Every hold time so far is test mode | Only once the card processor has approved the business |
| Shopper B's leftover purchase in dollars | B never reached "Scan 1" on 2026-10-14. The new Done step is meant to fix the cause | Rerun, and check B |
| The new Done step in dollars | It posts a proof to our server, and is not timed | The same rerun |
| Our own Tempo node's lag | It decides the node's saving in version 2 | A trial node, logging how far it trails Tempo's own host |
| Mainnet | Block time, host lag, the sync send and Lit under real load. Our payment calls a contract, so it is not in Tempo's reserved lane for plain payments, and a busy chain squeezes it | First a read-only probe of mainnet hosts: block time, finality lag, round trips, nothing sent. Then a smoke with small real amounts, only once approved |
How we measure
The browser run. A headless Chromium on a desktop drives two shoppers through the real till: A pays, and B buys what A's last card left. The server is a local production build with the test database schema, the card processor in test mode, and Tempo's test network and Lit for real. The page logs the start and end of every request it makes. A timing patch on the server logs every database query and outside call. The tap is 0 s, and the end is the first barcode on screen. The scripts and the patch are not in the repo yet. They should be, under scripts/, so anyone can rerun them, and the server log should be kept per run instead of overwritten.
Its limits: one tap per run. A desktop, not a phone. A desktop server, not Railway. And every query crossing the internet to the database.
The timing script. pnpm till:timing times both rails step by step: the price, the tap to the first barcode, each Next, and Done. It is offline by default: our real code against stand-ins, with a chain inside the process running the real SalvoEscrowV2 behind two fake hosts, and the pinned Lit code on a fake Lit. That chain does not wait for Tempo's next block, so its send is quicker than live.
--rtt-msadds time to every request, and--lit-msto every Lit run.--tap-after-mstaps before the tickets are back, as a quick shopper does.--behindkeeps the second host a block behind.--live --base-url <server> --cookie-file <file>runs the card rail against a real server. Every run is a real purchase, so only on a test card key.
It cannot time a live dollars tap. That tap is signed by a key only the shopper's phone holds.
The per-step marks. In any browser, set salvo:till-timing to 1 in local storage. The console then prints each step of a dollars tap as it ends: ticket wait, reads, sign, send, included, second host, release, render. Names and milliseconds only, never a card, a key, an address or an amount. This is how a real phone gets timed.
The escrow smoke. pnpm escrow:smoke --version 2 --live, with its key files, runs whole sales on the test network and counts every Lit run and chain transaction, with their total time. It keeps totals only, not each run's time, which is why most Lit runs on file have no time of their own. It spends Lit credit and test funds.