linkex.cclinkex.cc
The Open Tally Protocol

Receipts

The atom of the ledger — canonical serialization, the signed receipt envelope, failure receipts, sections with selective disclosure, and section generations.

Canonical serialization: the foundation under every hash

The first vulnerability of any signing/hashing scheme is "the same content has more than one byte representation." Open Tally's canonical form is RFC 8785 (JSON Canonicalization Scheme) with one additional constraint: the document must not contain a JSON number.

Why numbers are banned

RFC 8785 serializes numbers as IEEE-754 doubles. Two consequences make that unusable for a financial record: any integer above 2⁵³ is not exactly representable (9007199254740993 round-trips as …992), and decimal fractions are approximated differently across producers. Either one lets two implementations that are both correct compute different hashes for the same receipt. Banning numbers removes the failure mode rather than asking implementors to avoid it.

All numerics are therefore decimal strings. Monetary amounts are int64 micro-USD (1 USD = 1,000,000) end to end — no floating point exists anywhere in the pipeline. Integer strings have exactly one legal spelling (^(0|[1-9][0-9]*)$): 007 and 7 are the same number but different strings, and it is the string that gets hashed.

The ban is a validation rule, not a deviation from RFC 8785 — the output is still plain JCS, so an off-the-shelf JCS library reproduces the bytes exactly.

Rules that require inspecting the raw bytes

Four validation rules cannot be checked after parsing, because mainstream JSON parsers silently resolve them. An implementation that validates the parsed value accepts all four:

RuleWhy it matters
Duplicate object keys are rejected{"amount":"1","amount":"1000000"} is read one way by a person and collapsed another way (last-wins) by a parser — the document a reader sees and the document that was hashed can differ.
Unpaired surrogate escapes are rejectedGo and JavaScript both replace an unpaired surrogate with U+FFFD, so "\ud800" and a literal U+FFFD would canonicalize identically and share a signature.
Malformed UTF-8 is rejected on the raw bytesDifferent languages "repair" invalid bytes differently — two conforming-looking implementations would emit different canonical bytes for one input. Validity must be decided on the raw bytes before any decoder runs.
Trailing data is rejectedExactly one JSON value per document.

These rules bind the issuer as well as the verifier — and the issuer cannot discharge them with a byte check, because serializers substitute U+FFFD while serializing. An issuer must validate string values before serialization; section payloads carry strings the issuer does not control, such as a user-chosen token name or a model name echoed from an upstream provider.

Every rule an implementation can plausibly get wrong has a conformance vector — the exact input bytes, expected output bytes, and their SHA-256 — and the definition of done was two independent implementations (Go and JavaScript) producing identical hashes for every accepted input. See building your own verifier.

The receipt: one per billable event

Every event that consumes a sequence number is issued exactly one signed receipt. A receipt is an envelope plus one or more sections.

The envelope

The envelope is small, fixed, and is what the hash chain and the signature commit to. This is a real one, from the linkex.ai production ledger (fetch it yourself: /api/receipt/bundle?stream=ch:10&seq=2):

{
  "spec": "linkex.usage-receipt",
  "spec_version": "1",
  "issuer": "linkex.ai",
  "stream": "ch:10",
  "seq": "2",
  "issued_at_ms": "1788570354585",
  "key_id": "ed25519:3d1aa5da8d326147165e10d4aa00cf8ef1daf690f708741fe7d4719dda3064e3",
  "sections": {
    "usage.customer.v1": "sha256:9e84ce945594491036f5fb9938a2923fd4bcc87bec8c5814005e9cba8b6fb925",
    "usage.supplier.v4": "sha256:8bcd11cde66e315c1aa919fdb1c0bdf50e28a232b9978043d7ad0cf95528a28c"
  },
  "prev_hash": "sha256:421a7c8ed1e9a9b1d3639bef02b41de634196424f82e1ebc9747bf24cd30503e"
}
FieldMeaning
issuerthe signing entity — inside the signed bytes, so a signature cannot be replayed as another issuer's
streamthe hash chain this receipt belongs to (one per upstream channel)
seqmonotonic within the stream, no gaps permitted
issued_at_mswhen the receipt was signed — a mechanical fact, distinct from when the request ran
key_ided25519: + SHA-256 of the raw 32-byte public key — a fingerprint, not a label
sectionssection name → content hash of the section payload
prev_hashthe previous receipt's self-hash on this stream (all zeros for a stream's first receipt)

Two of these choices carry more weight than they look:

  • key_id is a derived fingerprint. A verifier fetches the key from the published key history, recomputes sha256(raw public key), and confirms the key received is the key the receipt names. An opaque label (key-3) could be silently re-pointed at a different key; a fingerprint cannot.
  • There is deliberately no accounting-period field in the envelope. Which period a request is attributed to is a business rule (a call spanning a period boundary belongs wherever the published attribution rule says); it lives in a section, and it is a pure function of a timestamp that is inside the signature — so an attributed period altered after the fact disagrees with recomputation. Keeping business rules out of the cryptographic layer means the commitment structure never needs reissuing when an attribution rule changes.

Failures are signed too

Failed, cancelled, and zero-rated calls also occupy a sequence number and receive a receipt, with a billing status marking them as such. This is not tidiness — it is the precondition of the completeness argument: if every event must have a receipt, a hole in the sequence numbers has no innocent explanation. If failures were simply not receipted, the sequence would be full of legitimate gaps, and a withheld record would be indistinguishable from a service failure — the whole mechanism would become unfalsifiable.

(A consequence for implementers: a sequence number can only be allocated once the outcome is known, because the receipt records that outcome.)

Envelope + sections: selective disclosure

Business data lives in sections committed by content hash — in the linkex.ai instance, a supplier-side section (what was consumed, at what upstream cost) and a customer-side section (what the end customer was charged).

The signature covers the section hashes, not the payloads directly. Two properties follow, and both are requirements rather than conveniences:

  • Redaction that is still provable. The operator can disclose the supplier section to a supplier and the customer section to a customer. The undisclosed side does not impair verification of what was disclosed — and the recipient can still see that the withheld section was fixed at issue time and has not been swapped since. Neither party sees the operator's full margin; each can verify everything they received. What you receive, you can verify; what you don't receive, you don't have to take on faith.
  • Extensibility without invalidating history. New fields go into a new section version; receipts signed under the old version keep verifying under the old version's rules forever.

Within a section payload, every field defined by that section version is always present — a field is never omitted because its value is empty. Absence has exactly one meaning: the payload is not of the version it claims, and is rejected. Without this rule, a verifier could not distinguish "empty value" from "this receipt predates the field."

Section generations: evolving formulas without touching history

Section versions are grouped into generations. The linkex.ai instance is on its fifth:

GenerationWhat it changed, and why
1Initial envelope + sections.
2Added the recomputation inputs — per-direction unit prices, currency, tier coefficient, price-book id. Before it, deterministic recomputation (check 6) could not pass for token-billed receipts at all, and the report said so rather than pretending.
3Signs how the numbers were produced, not just what they were: tokenizer identity and version, count mode, the gateway's own independent token counts beside the upstream's, priced cache quantities, which upstream endpoint served the call, and why a receipted request was not billed. A figure nobody can dispute is not evidence — disclosing the inputs is what makes the number disputable, and therefore meaningful.
4Corrected the token formula (a double-count of reasoning tokens, a double-charge of cached tokens, and a double rounding) and dropped a field that was one number printed twice under a claim of independence.
5Adds an itemized customer-side breakdown, so a customer can verify not just that their amount was signed but that it multiplies out line by line (check 7). The supplier side is unchanged.

The protocol rule is absolute:

Signed history is never rewritten — not one byte. Verification and recomputation dispatch on the generation the receipt itself declares.

A published section version is frozen: no field may be added, removed, or renamed within it. Fixing a formula defect means minting a new generation beside the old one — generation-3 receipts recompute under the generation-3 formula forever, because that is the arithmetic they committed to. Receipts on either side of a generation cutover form one unbroken chain, since chain verification reads only the envelope and never section content.

What a receipt records for recomputation

Amounts must be deterministically recomputable from the receipt alone (check 6). A receipt therefore records, inside signed content: the unit prices and price-book version applied, the metering inputs and their provenance (tokenizer id/version, count mode — see instance guarantees), the time-band coefficient actually applied, and the usage-count convention the upstream follows (whether reported prompt tokens already include cached tokens — with no default: an absent convention means the verifier reports "cannot recompute" rather than guessing).

The coefficient is read from the receipt, not re-derived from today's configuration — otherwise a routine pricing change would make every historical receipt fail recomputation with nothing tampered. What check 6 establishes is deliberately narrow: the amount follows from the inputs that were signed. Whether those inputs were the contractually correct ones is what price books and reconciliation settle.

Disclosure follows a per-field whitelist audited field by field, so counterparties know exactly which fields they will always receive.

On this page