# Transaction tags (/developers/internals/transaction-tags)



Every transaction Zerochord emits carries a CIP-20 transaction message. That message is the
only public attribution there is: an auditor with nothing but chain data identifies a
Zerochord transaction by the project name in its metadata, and by nothing else.

## CIP-20 in brief [#cip-20-in-brief]

[CIP-20](https://cips.cardano.org/cip/CIP-20) is Active. The schema is:

```json
{ "674": { "msg": ["...", "..."] } }
```

`msg` is an array of at least one string, and each string is at most **64 bytes** when encoded
as UTF-8. Sixty four bytes, not characters: a multi byte character costs more than one.

The label is a configuration value under `tagging.label` rather than a constant in code.

## The lines Zerochord writes [#the-lines-zerochord-writes]

`buildSwapTagLines` produces three lines:

```text
<projectName> <action>
<assetIn>-><assetOut>
route:<routeId>
```

The actions in use are `swap`, `order`, `cancel`, and the settlement actions. The route
identifier ties a transaction back to the quote that produced it, and the applications build it
from the block height the quote was read at and the minimum output it carried.

Every line is length checked, and a line over the limit throws rather than being truncated. A
truncated message on chain is invisible; a failed build is not.

`splitIntoCip20Lines` exists for a long message, and splits on UTF-8 boundaries so it never
cuts a character in half.

## Two rules, enforced where every builder passes [#two-rules-enforced-where-every-builder-passes]

`attachProductTag` in `packages/tx/src/tagging.ts` is the only path a builder uses to attach
metadata. It enforces both rules:

<Steps>
  <Step>
    ### The message is valid CIP-20 [#the-message-is-valid-cip-20]

    At least one line, and every line at most 64 UTF-8 bytes. `buildCip20Metadata` throws
    `Cip20Error` otherwise.
  </Step>

  <Step>
    ### Some line carries the project name [#some-line-carries-the-project-name]

    ```ts
    if (!tagLines.some((line) => line.startsWith(projectName))) {
      throw new ProductTagError(
        `no CIP-20 tag line starts with the project name ...`,
      );
    }
    ```

    A tag without the project name is attached, valid, and useless. An earlier build omitted the
    name and the audit found zero swaps over a chain that held several. The check lives here, where
    every builder passes, rather than at each call site.
  </Step>
</Steps>

## Reading a tag back [#reading-a-tag-back]

The tag is only worth anything if it survives serialisation, so it is read back out of the
finished transaction bytes rather than trusted from the builder.

```ts
readProductTagFromTxCbor(cborHex, label): string[] | null
requireProductTag(cborHex, network): string[]
```

`readProductTagFromTxCbor` decodes the transaction's auxiliary data and returns the message
lines, or null when there is none under the label. It is the same thing an auditor does from
the chain, run against the transaction before it is signed.

`requireProductTag` asserts and throws. Call it on anything about to be signed: it is the last
point at which an untagged transaction can still be stopped. The command line prints its result
next to the built transaction.

## The project name is immutable on chain [#the-project-name-is-immutable-on-chain]

`tagging.projectName` is `cardano-swap-router` in both shipped configurations.

Every transaction already recorded carries that exact string. `cardano-swap audit`, the
footprint declaration and the Dune queries all select on it, so changing it would orphan every
transaction the project has made.

## One historical transaction proves why the rule exists [#one-historical-transaction-proves-why-the-rule-exists]

Preprod transaction `93fc0ff3ca526534d355950c21e33cbedc7229e75f76c9d05a525c1143a88173` carries
label 674 with the message `["fUSDM->fUSDA", "min:5906126"]` and **no line naming the project**.

It is a real product swap: it spends a UTxO at the declared 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, because attribution from public data is the project name and nothing else.

A settled transaction's metadata is immutable, so it cannot be repaired. It cannot recur,
because `attachProductTag` now refuses a tag whose lines do not name the project.

`cardano-swap audit` reports it in a section of its own, under `tagged, not attributable`, rather
than dropping it. See [Reproducing the saving claim](/developers/verification/audit).

## Surfacing tagged activity [#surfacing-tagged-activity]

`dune/` holds SQL queries over `cardano.transaction_metadata` and related tables that surface the
tagged activity. Their totals are gross.

Dune publishes a single `cardano` schema and it is mainnet, so those queries return rows for
mainnet activity only.

## Related [#related]

<Cards>
  <Card title="Reproducing the saving claim" href="/developers/verification/audit" />

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

  <Card title="Configuration reference" href="/developers/reference/configuration" />
</Cards>
