Reproducing the saving claim
Rebuild every swap from public chain data with one command, and check the saving yourself.
Zerochord claims a saving against the direct route on every swap. That claim is only worth something if somebody else can check it. This page is how.
Run it yourself
pnpm install
pnpm build
node apps/cli/dist/main.js audit --network preprod --limit 200Nothing about that command reads Zerochord's own records. It reads the chain.
What it reconstructs, and from what
A Kernel class swap spends the venue's pool UTxOs directly in the user's own transaction. That makes the transaction self evidencing.
The pool states at the quoting block are the transaction's own resolved inputs. They are permanent and public.
What the user paid and received is the net flow across every pool the transaction touched: what the pools paid out minus what they took in.
Net flow is used rather than reading the legs in order, because a transaction's inputs are ordered by the ledger, not by the route. A two hop route can present its second hop first. An intermediate asset nets to zero, which is what makes a chained hop a chain.
The sum runs over every asset the pools hold, not only the pair they trade. A Dano Finance pool charges a flat ADA fee on each swap, including a pool whose pair is two tokens, and that ADA comes from the signer. Counting only the traded pair reported one two hop transaction as paying 20 100 000 lovelace when the signer actually parted with 20 200 000. The error flattered the product, which is the direction that matters.
Who paid the fee is the set of input addresses that are not the venue's pool script. This is how the fee is shown to be the signer's own.
The route is the list of pools spent, each with its own amount in and out. It is printed in output reference order, which is stable, and deliberately not in route order. Reading a leg list as if it were the route is the mistake that produced a wrong answer on a real transaction.
What it cannot reconstruct
The baseline is the best single pool for the same pair at the same block. A pool the route did not touch does not appear in the transaction, and Koios serves current UTxO state rather than the state at an arbitrary past block.
So:
- Where the direct pool is among the pools the transaction spent, the baseline is exact and the saving is computed.
- Where it is not, the audit prints baseline not reconstructible and claims no saving. It does not substitute today's pool state for a past one, because that would produce a number that looks like evidence and is not.
The summary reports both counts, so the proportion of swaps with an unverifiable baseline is visible rather than buried.
The count that must never be hidden
The summary reports how many swaps did not beat the direct route, next to how many did.
For a pair with a deep direct pool that number is often most of them, and saying so is the point. Suppressing it would make the published saving figure dishonest, which is the one failure this whole exercise exists to avoid.
Determinism, and how it is checked
Two runs against the same chain state produce byte identical output.
The test does not run the audit twice and hope. The second run is served the same real responses in a different order: the rows of a transaction batch, the inputs and outputs inside each row, and the head of the credential listing, all reversed. Every one of those orders is a legal way for a provider to answer, and Koios promises none of them. Nothing is invented, the same bytes arrive in a different sequence.
The test was proved able to fail by breaking the audit five different ways and confirming each break is caught.
| Break | Caught by |
|---|---|
| Remove the sort that orders swaps by block and hash | The permuted run, and two plain live runs. This is how the sort came to exist: the provider does not always return a batch in the same order, so the audit was not in fact deterministic before. |
| Remove the sort that orders the legs | The permuted run: leg lines change places. |
| Read the legs in order instead of by net flow | The two hop transaction reports different numbers under the reversal. |
| Widen the untagged list to every transaction carrying the label | The count jumps from 1 to 180, which is the venue's own front end. |
| Put a wall clock in the report | See below. |
The clock is the interesting one. It was caught by only one of the two comparisons, and the one that missed it missed it for a reason that would have made it decoration: it rendered both sides in the same millisecond, so a clock matched itself. The test now renders the live run's report at the moment the run finishes and compares it against a fresh render tens of seconds later, so anything in the output that moves with time fails deterministically rather than by luck.
A separate test recomputes what each swap paid and received from the signer's side, by summing the paying addresses' own inputs and outputs in the same transaction, and requires it to equal what the audit computed from the pool side, to the unit, allowing only for the ledger fee. That is the check that found the missing flat ADA fee described above.
Worked example
Preprod transaction b742ec5e:
transaction b742ec5ed9d8d0dc06e641e6d4507272de218715bf99d75f494f03ab7b8d6c33
block 5075848
tag cardano-swap-router swap | fUSDA->fUSDM | route:5075847-3621971
paid in 3000000 of 9a614be3...6655534441 (fUSDA)
received 3640172 of 834a1510...665553444d (fUSDM)
pools spent 2
746cb488...#1 3000000 in -> 11487192 out
ae845e06...#9 11687192 in -> 3640172 out
fee paid by addr_test1qz8mr9arc...
fee 601501 lovelace
baseline not reconstructible: no pool this transaction spent holds both
assets, so the best single pool at that block cannot be recovered
from the transaction alone, and no saving is claimed
saving not claimedRead it as: the user paid 3 000 000 fUSDA and received 3 640 172 fUSDM, routed through ADA across two pools in one transaction. The intermediate ADA nets to the 200 000 lovelace of protocol fees the two pools charged. No direct fUSDA to fUSDM pool was touched, so no saving is claimed for this one.
Reconciling audit with measure
cardano-swap measure and cardano-swap audit report different counts of the same chain. They
are supposed to. This section exists so a reader who runs both does not have to guess which one to
believe.
They start from different places and ask different questions.
measure | audit | |
|---|---|---|
| Starts from | The credentials the declared footprint names: the settlement script, the operator wallet, and the routed venue scripts | One credential: the Kernel venue's pool script |
| Window | Every transaction at the project's own credentials, plus a bounded newest-first window per routed venue | The newest --limit transactions at the pool script, default 200 |
| Counts a transaction when | Its CIP-20 message under the declared label carries the project name | The same, and it spent a pool of that venue, so a swap can be rebuilt from it |
| Reports | Transactions, fees, payers, concentration | Input, output, route, baseline, saving |
So measure counts every kind of product transaction, and audit counts only the ones that are a
Kernel class swap.
The counts differ for eight distinct reasons
A reader who assumes a single explanation will find the arithmetic does not close.
A Kernel class swap. It spends the venue's pool UTxOs in the user's own transaction. Both tools see it. This is the only class the audit counts, and it does not matter which front end built it: a swap the web application signed is the same evidence as one the command line signed.
A deploy. It puts a validator on chain as a reference script. It carries the tag, so measure
counts it. It spends no pool, so there is nothing for the audit to reconstruct.
A settlement lock. It pays the settlement script. Counted, not a swap.
A settlement claim or refund. It spends the settlement script. Counted, not a swap.
A Commitment class order placement. It pays the venue's order script and binds a minimum. The trade happens later in the batcher's transaction, so there is no pool state here to read and no output to compare. Counted, not a swap.
An order that never fills stays in this class forever, and that is the honest treatment: it is a real product transaction that paid a real fee, and it produced no swap.
A cancel. It spends an order placement back to the signer. Counted, not a swap.
A fill by the venue's own batcher. It executes the order, but the product did not build it and did not pay for it, and it carries the batcher's own tag. Neither tool counts it. This one is easy to misread as a missing swap: the trade did happen, and no tool here claims it, because the transaction is not ours.
A transaction whose tag does not name the project. Neither tool counts it, because the tag is the only public attribution and it is silent. The audit lists it separately. See below.
Two further reasons are not about kinds of transaction at all.
The windows differ. The rules nest, the scans do not. Each tool bounds its own scan and prints its bound. A transaction old enough to fall outside one window and not the other is counted by one tool only.
The two commands run at different moments. A test network keeps moving. A transaction that lands between the two runs is in the later one only.
A dated reading
The numbers below are a snapshot taken on 2026-08-20 with preprod at block 5 076 337. New transactions land, so the numbers move. The method is what reconciles, not the totals.
At that reading, measure counted 15 and audit reconstructed 6. The class column refers
to the list above.
| Block | Transaction | Tag | Class | In audit? |
|---|---|---|---|---|
| 5075651 | 3566cf94 | swap ADA->fUSDM | 1 | Yes, one pool |
| 5075665 | 787d3439 | swap ADA->fUSDM | 1 | Yes, split over two pools |
| 5075675 | 82e4f191 | swap fUSDM->fUSDA | 1 | Yes, one pool |
| 5075677 | f1aff8a6 | swap ADA->fUSDA | 1 | Yes, two hops through fUSDM |
| 5075848 | b742ec5e | swap fUSDA->fUSDM | 1 | Yes, two hops through ADA |
| 5076337 | 6a3109e2 | swap fUSDM->fUSDA | 1 | Yes, and the web application built this one |
| 5075873 | 8e066c7d | deploy settlement | 2 | No, it spends no pool |
| 5075957 | db06bc3f | settle lock | 3 | No, it pays the settlement script |
| 5076159 | e94b0a35 | settle lock | 3 | No, it pays the settlement script |
| 5076069 | 2d6de21a | settle claim | 4 | No, it spends the settlement script |
| 5076079 | dd9db10c | settle refund | 4 | No, it spends the settlement script |
| 5075921 | ccfc5883 | swap ADA->tiUSD | 5 | No, an order placement, cancelled at 5076295 |
| 5076246 | 10c0fc7e | order ADA->tiUSD | 5 | No, an order placement. The batcher filled it at 5076247 |
| 5076248 | 3ab08c68 | order ADA->tiUSD | 5 | No, an order placement. Still unspent: the batcher cannot fill it without breaking its bound, so it rests |
| 5076295 | 9f9038f7 | cancel order->refund | 6 | No, it cancels ccfc5883 |
Six swaps, nine transactions that are not swaps, fifteen counted.
Three further transactions sit at the project's own credentials and are counted by neither tool:
9708432fat block 5076247, class 7. The venue batcher's transaction filling order10c0fc7e. It carries the batcher's own tag,Minswap: Order Executed. The trade is real and it is not ours to count.93fc0ff3at block 5075814, class 8. Ours, with a tag that does not name the project. See below.6fa8e970at block 5075567. The faucet funding the operator wallet. It carries no metadata at all, so it is not a product transaction by any rule.
The one transaction neither tool counts
Preprod transaction 93fc0ff3ca526534d355950c21e33cbedc7229e75f76c9d05a525c1143a88173, block
5 075 814, carries label 674 with the message ["fUSDM->fUSDA","min:5906126"] and no line
naming the project.
It is a product swap. It spends a UTxO at the declared Dano Finance pool script and its other input and its change belong to the declared operator wallet. So it is tagged, the tag is valid CIP-20, and the tag is useless: attribution from public data is the project name in the message and nothing else.
measure does not count it. audit does not count it as a swap. Instead the audit reports it, in
a section of its own and in a summary line reading tagged, not attributable:
summary
...
tagged, not attributable 1
tagged under this product's label, no line naming the project
Each of these spent a pool of the same venue and was paid for by an
address that also paid for a swap above, so it looks like ours. The tag
does not say so, and the tag is the only public attribution there is, so
none of them is counted as a swap. Listed here rather than dropped.
93fc0ff3ca526534d355950c21e33cbedc7229e75f76c9d05a525c1143a88173 block 5075814 fUSDM->fUSDA | min:5906126
paid by addr_test1qz8mr9arc...Why it is reported rather than counted. Counting it would mean claiming a trade the chain does not attribute to this project, which is the same class of error as suppressing a swap that lost to the direct route. Dropping it silently would leave a reader comparing two counts with an unexplained difference. So it is listed, with its hash and its message, and left out of every saving number.
Why the rule that finds it is narrow. "Carries label 674 without the project name" on its own describes most of the window: the venue's own front end tags every trade it builds under the same label, around 180 transactions in the default window. Listing those would be noise dressed as disclosure. The audit lists only a transaction that also shares a paying address with a swap the tag did attribute to the project in the same run. That evidence comes out of the run itself, from public data, and it is what makes a listed transaction a near miss rather than a stranger's trade.
It cannot be repaired and it cannot recur. A settled transaction's metadata is immutable.
attachProductTag in packages/tx/src/tagging.ts refuses a tag whose lines do not include the
project name, so no builder can emit another one.
Surfacing tagged activity
dune/ in the repository holds SQL over cardano.transaction_metadata and related tables that
surface the tagged activity, not the saving, because a saving is a comparison against a route that
did not happen.
Dune publishes a single cardano schema and it is mainnet, so those queries report mainnet
activity.