Several methods are in use for building tamper-evident records of agentic payments. This repository puts the common ones side by side and lets you decide with reproducible bytes, not opinion.
It compares those methods against a named reference: the AlgoVoi JCS
(RFC 8785) Substrate, whose load-bearing properties are pinned by conformance
vectors you can run yourself - action_ref_exactly_once_v1 (exactly-once) and
adversarial_isolation_v1 (rejects malformed input) - and cross-validated across
8 independent implementations (880/880 byte-for-byte). The reference is named; the
alternative methods are compared by technique (no competitor named). Every
verdict below is produced by a script in this repo that you can run offline, with
nothing but Python, algovoi-substrate, and SHA-256. If you disagree, run the
demo and read the bytes. A full failure map with the exact hashes is in
RESULTS.md.
What
action_refmeans here. Throughout,action_refis the AlgoVoi implementation of action_ref, adapted to AlgoVoi's own design:SHA-256(JCS(RFC 8785)({agent_id, action_type, scope, timestamp_ms}))with an integer-millisecondtimestamp_ms, canon versionjcs-rfc8785-v1. The bare nameaction_refis used across the ecosystem in mutually incompatible forms (string timestamps, bare concatenation); this comparison's named reference is specifically the AlgoVoi construction, and every reference value is produced by thealgovoi-substratepackage itself (action_ref,sha256_jcs,settlement_action_binding).
Every cell below is computed live from real bytes by coverage.py
(hash a real record, read the result), not entered by hand. The reference holds
every property; each alternative technique fails at least one.
| Technique | Exactly-once | Byte-reproducible | Offline-verify | Adversarial-safe |
|---|---|---|---|---|
| AlgoVoi JCS (RFC 8785) Substrate (action_ref) | yes | yes | yes | yes |
| second-precision timestamp | no | yes | yes | yes |
| RFC 3339 string timestamp | yes | no | yes | yes |
| bare concatenation | no | yes | yes | no |
| naive key-order serialization | yes | no | yes | yes |
| camelCase field naming | yes | no | yes | yes |
| forward-id / operator-report binding | yes | yes | yes | no |
| operator-attestation (no content-address) | yes | yes | no | yes |
| amount as JSON number (float64) | no | yes | yes | yes |
| ad-hoc number serialization (1.0 vs 1) | yes | no | yes | yes |
Run python coverage.py to regenerate this table from real bytes.
Two bindings sit on a separate axis from the four properties above - they ask
whether a later tamper or a silent policy change is detectable from the record
alone. The content-addressed bindings catch it; an operator-assigned id or a
version label does not (coverage.py computes these from real bytes too):
| Binding technique | Change detected from the record alone |
|---|---|
| AlgoVoi settlement_action_binding (action swap) | yes |
| forward-id / operator-report (action swap) | no |
| AlgoVoi policy_bound_ref (silent policy rotation) | yes |
| policy id/version label (or operator attestation) | no |
At realistic agentic-payment rates the cost of one of those failures (exactly-once under a coarse timestamp) is most of the payments (counted from real hashes; the full table is under At scale):
| Payments | second-precision dropped | integer epoch-ms dropped |
|---|---|---|
| 100 in 1s | 98.0% | 0.0% |
| 1,000 in 1s | 99.8% | 0.0% |
| 10,000 over 10s | 99.9% | 0.0% |
A comparison is only as good as the vectors behind it, and a vector set is only trustworthy if independent implementations agree on it byte-for-byte. The AlgoVoi JCS (RFC 8785) Substrate conformance corpus is published and independently cross-validated; the common alternative is a single-implementation vector set with no independent agreement.
| Vector set | Independent implementations | Byte-for-byte agreement | Sets / vectors |
|---|---|---|---|
| AlgoVoi JCS (RFC 8785) Substrate | 8 (8 languages, incl. the RFC 8785 author's Java) | 880/880 | 30 / 245 |
| single-implementation vector set | 1 | none reported | varies |
Source: the public corpus
chopmob-cloud/algovoi-jcs-conformance-vectors
(manifest pins the sets, vectors, and the cross-validation runs). Run them yourself.
Rejecting a malformed input is a property of the validator, not of the bytes. A single-implementation conformance set can show its own reference implementation rejecting an attack. It cannot show that independent implementations reject it the same way.
The AlgoVoi adversarial set is cross-validated across all eight implementations: every one accepts the valid control and rejects each of the eleven isolated attacks (a non-integer timestamp, a non-hex action reference, an empty state, a broken chain link, a stale content hash, and so on). Eight implementations times twelve vectors is 96 fail-closed verdicts, all green.
| Property | single-implementation set | AlgoVoi (8 implementations) |
|---|---|---|
| Input bytes agree across implementations | not shown | yes |
| Every implementation rejects each attack identically | not shown (one impl) | 96/96 |
Reference-implementation rejection runs here in
methods/adversarial_rejection.py. The eight
independent implementations all failing closed on the same attacks, with its
attestation, is in the corpus at composition/adversarial_gauntlet/ (one command:
bash run_gauntlet.sh).
Regenerate everything (methods + results + graphs) from one command:
python run_all.py.
A 2026-06-21 survey of the independent agentic-payment canonicalization
implementations in this space (each read from its own primary source) found that the
field largely converges on a single canonicalization identifier. Of the
implementations surveyed, six declare the jcs-rfc8785-v1 canonicalization identifier
in their own specs or code (as preimage_format, canon_version, or a
canonicalization constant). Further implementations use the RFC 8785 JCS discipline
under their own label, and one cross-validates against this corpus directly. The
implementations that adopt neither use naive serialization (sorted-key
JSON.stringify, or a delimiter-joined string) or no canonicalization at all.
Compared by technique, the alternatives differ from this substrate on the axes that decide byte-portability and durability:
| Capability | This substrate | Alternatives (surveyed) |
|---|---|---|
| Canonicalization | real RFC 8785 JCS | RFC 8785 (most), naive or none (some) |
| Timestamp form | integer epoch-ms | integer-ms or RFC-3339 string (split) |
| Independent implementations, byte-for-byte | 8 languages, 880/880 | 1 to 8 (one reports 192/192) |
| Conformance vectors | 30 sets / 245 vectors | 0 to ~53 |
| Adversarial fail-closed, cross-implementation | 96/96 (8 implementations) | 0 to a few, mostly single-implementation |
| Signing | post-quantum (Falcon-1024 + ML-DSA) | classical (Ed25519 / ES256); one post-quantum (STARK) |
| Specification | sole-authored, published IETF I-Ds | mix of unpublished and published drafts |
jcs-rfc8785-v1 is the registered canonicalization identifier, first published
2026-05-22. Every figure above is recomputable from the public corpus; run it yourself.
The comparison is anchored on two properties the published substrate proves, both reproducible from a single command:
- Exactly-once (
action_ref_exactly_once_v1) - a genuine retry collapses to the same identity (skipped, no double spend); a distinct action stays distinct; a non-committed state cannot pass as settled. Demo:methods/secondary_attempts.py. - Adversarial rejection (
adversarial_isolation_v1) - malformed inputs (an RFC 3339 timestamp, a negative, a boolean, a non-hex or short identifier) are rejected at the validation layer, not hashed into a clean-looking identity. Demo:methods/adversarial_rejection.py.
Every method below is measured against these two properties, plus byte reproducibility and offline verifiability.
A method is judged only against the three properties that actually matter for a regulated agentic payment record:
- Exactly-once identity - two operationally distinct actions must get two distinct identifiers. If two real payments can collapse to one identifier, the second is indistinguishable from a retry and can be silently dropped.
- Cross-implementation byte-reproducibility - the same logical record must hash to the same bytes in every language and on every platform, or independent parties cannot verify each other.
- Offline verifiability - a holder of the record can verify it with a hash function and a JSON parser, without calling back to the issuer.
| Method | Exactly-once | Byte-reproducible | Offline | Demo |
|---|---|---|---|---|
| integer epoch-millisecond | yes | yes | yes | methods/timestamp_encoding.py |
| second precision | no | yes | yes | same |
| RFC 3339 string | yes | no (string vs number diverges) | yes | same |
methods/timestamp_encoding.py shows it directly: under second precision, two
payments 753 ms apart collapse to one identifier (exactly-once breaks); under an
RFC 3339 string the same moment produces a different identifier than the integer
form, so the two cannot cross-verify.
The single-collision case is not an edge case; it is the common case at volume.
methods/scale.py computes the REAL action_ref of every
payment in a burst and counts how many collide (a collision is a real payment
dropped, because it is indistinguishable from a retry):
| Payments | integer epoch-millisecond dropped | second precision dropped |
|---|---|---|
| 100 in 1 second | 0% | 99.0% |
| 1,000 in 1 second | 0% | 99.9% |
| 10,000 over 10 seconds | 0% | 99.9% |
| 60,000 over 60 seconds | 0% | 99.9% |
| 100,000 over 100 seconds | 0% | 99.9% (only 100 of 100,000 survive) |
At any realistic agentic payment rate, second precision loses more than 99% of payments; integer epoch-millisecond loses none. To be precise rather than absolute: integer-millisecond also has a resolution limit (one identity per millisecond, so ~1000 payments/second), but that ceiling is 1000x higher than second precision and sits far above any real workload. These are counted from real hashes, not modelled.
Measured in a clean container. A fresh python:3.12-slim install of the
published packages (algovoi-substrate, algovoi-policy-binding,
algovoi-compliance-gate-lite from PyPI) reproduces 1,000,000 distinct action_ref
values with zero collisions, and the second-precision drop above (99.8 to 99.9
percent) exactly. Collision freedom is a property of the construction and is the
same on any host. Throughput is the host's: on a single core the million completed
in about 8.5 seconds (roughly 118,000 per second), with the full policy_ref plus
policy_bound_ref plus gate_ref chain at about 43,000 per second. Both scale with
cores. A blank node:20-slim box reproduces the same policy_bound_ref and
gate_ref values byte for byte from the npm packages.
Pinning the timestamp to an RFC 3339 millisecond string does not make it byte-stable. One instant has several valid encodings, so honest producers still diverge.
| Technique | Byte-reproducible across producers | Demo |
|---|---|---|
| AlgoVoi integer epoch-millisecond | yes (one representation) | methods/rfc3339_grammar.py |
| RFC 3339 millisecond string | no (.123Z, .123+00:00, .123000Z are one instant, three byte sequences) |
same |
methods/rfc3339_grammar.py hashes three valid RFC 3339 encodings of a single
instant and shows they produce three different identities, while the integer
epoch-millisecond form has exactly one.
The reason timestamp precision matters is what happens on a secondary attempt.
A content-addressed exactly-once method uses the action identity itself to tell
a genuine retry from a distinct action. methods/secondary_attempts.py
demonstrates all four behaviours:
| Secondary attempt | Behaviour |
|---|---|
| identical action re-presented (genuine retry) | same identity -> absorbed, SKIPPED, no double spend |
| distinct action 1 ms later | different identity -> processed as new, not skipped |
| a non-committed (PENDING) state | different hash -> cannot pass as settled |
| a different party, identical parameters | different identity -> cannot impersonate another's action |
This is why a coarser timestamp is not a free simplification: it removes the method's ability to distinguish a distinct action from a retry, which is the exact failure quantified in the scale table above. Precision and correct secondary-attempt handling are the same property viewed from two sides.
| Method | Byte-reproducible across implementations | Demo |
|---|---|---|
| JCS (RFC 8785) | yes | methods/canonicalization.py |
| ad-hoc / key-order-dependent serialization | no | same |
methods/canonicalization.py shows that JCS hashes the same logical object to
identical bytes regardless of input key order, while a naive serialization
produces different bytes for the same object, so two implementations disagree.
RFC 8785 performs no Unicode normalization and emits printable non-ASCII as
literal UTF-8. An implementation that normalizes a string, or escapes non-ASCII
as \uXXXX, changes the bytes and the identity.
| Technique | Byte-reproducible across implementations | Demo |
|---|---|---|
| hash the signed bytes as received (RFC 8785, literal UTF-8) | yes | methods/unicode_normalization.py |
normalize NFC/NFD, or \u-escape non-ASCII, before hashing |
no | same |
methods/unicode_normalization.py shows NFC and NFD of the same glyph hash to
different identities, and that a \u-escaping serializer diverges from the
canonical literal-UTF-8 bytes.
A common shortcut derives the action identity by joining the fields into one
string and hashing it: SHA-256(agent_id : action_type : scope : timestamp_ms).
The delimiter can also appear inside a field value, so the field boundaries are
not recoverable and two operationally distinct actions can produce the identical
joined string -- and therefore the identical identity.
| Method | Exactly-once | Adversarial-safe | Demo |
|---|---|---|---|
structured object under JCS (action_ref) |
yes | yes | methods/concatenation.py |
| delimiter-joined concatenation | no | no (forgeable collision) | same |
methods/concatenation.py shows it with real
hashes: two distinct actions (action_type="screen", scope="acme:order-42" vs
action_type="screen:acme", scope="order-42") collapse to one concatenation
identity, while the structured action_ref keeps them distinct. A coalition that
standardises on a delimiter-joined action_ref inherits this collision; the
structured form does not have it. It is also an attack surface: an actor who
controls one field can re-target another action's identity.
JCS fixes key order and value encoding, but it canonicalises whatever field names it is given. The field set must be pinned, or two implementations diverge.
| Technique | Byte-reproducible across implementations | Demo |
|---|---|---|
AlgoVoi snake_case field set (agent_id, action_type, scope, timestamp_ms) |
yes | methods/field_naming.py |
camelCase variant (agentId, actionType, scope, timestampMs) |
no | same |
methods/field_naming.py hashes the same logical action under both conventions and
shows the identities never match.
| Technique | Tamper-evident (action swap detected) | Demo |
|---|---|---|
AlgoVoi content-addressed binding (substrate settlement_action_binding -> binding_ref) |
yes | methods/settlement_binding.py |
| forward-id / operator-report (settlement carries an assigned receipt id) | no | same |
methods/settlement_binding.py swaps the action and shows the content-addressed
binding breaks (caught), while the forward id is unchanged (not caught) -- so a
forward id does not actually bind.
A record should be able to prove which policy was in force when it was sealed,
and a later change to that policy should be detectable from the record alone. The
hard case is a silent rotation: the operator keeps the same policy_id and
version label but edits a rule. A content hash of the policy catches it; a
label or an "operator applied policy X" attestation does not.
| Technique | Silent rotation detected (same label, edited rule) | Demo |
|---|---|---|
AlgoVoi content-addressed binding (policy_bound_ref over the policy bytes, bound to a frozen subject) |
yes | methods/policy_binding.py |
| policy id/version label carried in the record | no | same |
| operator attestation ("policy X was applied") | no | same |
methods/policy_binding.py seals a record under policy P, then recomputes under
an edited P' that keeps the identical policy_id/version label: the
policy_bound_ref changes (rotation caught), while the label and the attestation
are unchanged (rotation invisible). policy_ref / policy_bound_ref are the
algovoi-policy-binding package (Apache-2.0), additive over the frozen substrate.
This proves detectability - version-provable and rotation-detectable, offline, from bytes. Acting on a detected mismatch (rejecting the record) is a runtime verifier decision, not a property of the construction; it is listed here as the detectability demo, not an enforcement claim.
A content-addressed identity is derived from content alone, so two distinct executions with identical content share one identity, and a replay cannot be told from a real second execution. Binding the action to its settlement instance keeps them apart.
| Technique | Distinct executions stay distinct | Demo |
|---|---|---|
| AlgoVoi binding_ref (action_ref + settlement payment hash) | yes | methods/replay_resistance.py |
| content-address only (identity from content) | no (identical content gives one identity) | same |
methods/replay_resistance.py shows two identical-content executions collapse to
one content-address, while the binding_ref over each settlement instance keeps
them distinct. The substrate is instance-bound, not only content-bound.
| Technique | Verifiable from bytes alone (no issuer call) | Demo |
|---|---|---|
| AlgoVoi content-addressed identity | yes | methods/offline_verification.py |
| operator-attestation (operator-assigned id, operator-signed) | no (needs issuer key/endpoint; proves assertion, not truth) | same |
A signed receipt can be content-addressed over the JWS compact form or over the JCS-canonical claims object. The compact form is not unique: any intermediary that re-serializes or re-signs changes it, so a hash over the envelope breaks on re-encoding.
| Technique | Survives re-encoding by an intermediary | Demo |
|---|---|---|
| hash the JCS-canonical claims object | yes | methods/jws_reencoding.py |
| hash the JWS compact form (header.payload.signature) | no (re-encode or re-sign gives different bytes) | same |
methods/jws_reencoding.py carries the same claims through two producers and
shows the compact-form hash diverges while the JCS claims-object hash is
identical.
| Technique | Exactly-once | Demo |
|---|---|---|
| AlgoVoi atomic amount as a string | yes | methods/amount_precision.py |
| amount as a JSON number (float64) | no (rounds past 2^53; two amounts -> one identity) | same |
Atomic on-chain amounts exceed JSON's safe-integer range; a JSON-number amount silently rounds, collapsing distinct payments. AlgoVoi encodes the atomic amount as a string (and strict RFC 8785 rejects the unsafe integer outright).
| Technique | Byte-reproducible across implementations | Demo |
|---|---|---|
| AlgoVoi JCS (RFC 8785) number form | yes (1.0 -> 1, 1e3 -> 1000) |
methods/number_canonicalization.py |
| ad-hoc serialization (preserves spelling) | no (1.0 != 1) |
same |
The AlgoVoi action_ref carries no settlement rail, so the same action has one
identity across Base, Solana, Hedera, and every other rail, and a verifier
correlates and de-duplicates it across rails. A rail-coupled identity changes per
rail, so the same action on two rails looks like two unrelated actions.
| Technique | Correlatable across rails | Demo |
|---|---|---|
| AlgoVoi action_ref (no rail in the identity) | yes | methods/rail_agnostic.py |
| rail-coupled identity (rail folded into the hash) | no | same |
The AlgoVoi substrate natively signs and verifies the canonical record with post-quantum algorithms, over the same JCS (RFC 8785) bytes as the classical path. A long-retention payment record signed only with a classical scheme loses its non-repudiation once those signatures become forgeable by a quantum computer.
| Signing | Quantum-resistant | Demo |
|---|---|---|
| AlgoVoi native PQC: Falcon-1024 (FIPS 206, L5) + ML-DSA-65 (FIPS 204, L3) | yes | methods/pqc_signatures.py |
| classical-only: Ed25519 / ES256 | no | same |
methods/pqc_signatures.py produces real Falcon-1024 and ML-DSA-65 signatures over an
action_ref record and verifies them with the substrate's verify_artefact (requires
algovoi-substrate-pqc). The algorithm-family classification is the substrate's own
registry; the PQC signatures cover the same expected_jcs_bytes_b64 as the classical path.
Reconciliation is when two or more independent parties to the same payment each compute the record's identity from their own copy of the data, then check that they arrived at the same answer. It is how a payer, a payee, and an auditor confirm they are describing the same event - without trusting any one party's word for it, and without a central server to ask.
For that to work, every party must derive the same bytes from the same record. So reconciliation is only possible on a shared canonical form: if two parties encode the record differently, they compute different identities and can never prove they are talking about the same payment.
A regulated payment has several such parties - payer, payee, auditor,
facilitator - so this is the real test of a substrate, not a nicety.
methods/reconciliation.py runs four parties over
one payment:
| Party | Encoding | Reconciles |
|---|---|---|
| 1 | integer-ms + JCS | yes |
| 2 | integer-ms + JCS, fields in a different key order | yes |
| 3 | RFC 3339 string timestamp | no |
| 4 | second precision | no |
The two canonical parties agree byte-for-byte even though one ordered its fields differently - JCS absorbs that. The two non-canonical parties each land on a different identity, so they cannot reconcile with the canonical parties or with each other. Interoperability is not a feature added later; it is a property of the form. Only the canonical form produces an identity independent parties can share.
| Method | Offline verifiable | Note |
|---|---|---|
| offline content-addressed | yes | holder verifies with SHA-256 + JSON parser; no issuer contact |
| issuer callback / read-at-decision-time | no | verification requires querying a live issuer endpoint |
This row is a property of the design, not something a single script proves; it is listed for completeness and marked as such.
pip install algovoi-substrate algovoi-policy-binding
python methods/secondary_attempts.py # reference: exactly-once
python methods/adversarial_rejection.py # reference: rejects malformed input
python methods/timestamp_encoding.py
python methods/scale.py
python methods/canonicalization.py
python methods/settlement_binding.py # binding survives an action swap?
python methods/policy_binding.py # silent policy rotation detected?
python methods/reconciliation.py # do independent parties agree?Each prints its comparison and exits 0 when the demonstrated properties hold.
Choosing a substrate method is a one-way door: records made under one method do not interoperate with another, and some methods quietly fail properties you only notice in production. The intent here is to make that choice on the merits, before the door closes, with evidence anyone can reproduce.
Apache License, Version 2.0. Copyright 2026 Christopher Hopley / AlgoVoi
(chopmob-cloud). The reference substrate used by the demos is published as
algovoi-substrate (PyPI) under the same license.