# Reproducing the saving claim (/developers/verification/audit)



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 [#run-it-yourself]

```bash
pnpm install
pnpm build
node apps/cli/dist/main.js audit --network preprod --limit 200
```

Nothing about that command reads Zerochord's own records. It reads the chain.

## What it reconstructs, and from what [#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 [#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-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 [#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 [#worked-example]

Preprod transaction `b742ec5e`:

```text
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 claimed
```

Read 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` [#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 [#the-counts-differ-for-eight-distinct-reasons]

A reader who assumes a single explanation will find the arithmetic does not close.

<Steps>
  <Step>
    **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.
  </Step>

  <Step>
    **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.
  </Step>

  <Step>
    **A settlement lock.** It pays the settlement script. Counted, not a swap.
  </Step>

  <Step>
    **A settlement claim or refund.** It spends the settlement script. Counted, not a swap.
  </Step>

  <Step>
    **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.
  </Step>

  <Step>
    **A cancel.** It spends an order placement back to the signer. Counted, not a swap.
  </Step>

  <Step>
    **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. &#x2A;*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.
  </Step>

  <Step>
    **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.
  </Step>
</Steps>

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 [#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:

* `9708432f` at block 5076247, class 7. The **venue batcher's** transaction filling order
  `10c0fc7e`. It carries the batcher's own tag, `Minswap: Order Executed`. The trade is real and
  it is not ours to count.
* `93fc0ff3` at block 5075814, class 8. Ours, with a tag that does not name the project. See
  below.
* `6fa8e970` at 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 [#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`:

```text
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 [#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.

## Related [#related]

<Cards>
  <Card title="The saving claim" href="/developers/concepts/saving-claim" />

  <Card title="Footprint and measurement" href="/developers/verification/footprint" />

  <Card title="Transaction tags" href="/developers/internals/transaction-tags" />
</Cards>
