Context
We've built an independent merchant-side TAP verifier (AVA Pay) implementing all three layers to public-spec depth: the RFC 9421 message signature (agent-browser-auth / agent-payer-auth tags, ed25519 + rsa-pss-sha256), Consumer Recognition Object validation including the Visa-signed PS256 IdToken verified against the production JWKS at mcp.visa.com/.well-known/jwks, and Agentic Payment Container validation. Wire format is locked to this repo's sample messages, and our test suite runs real cryptography for every layer.
One construct blocked exact interoperability: the object signature base for the signed body objects.
The gap
For the object-level signatures (Consumer Recognition Object, Agentic Payment Container), the public spec's guidance is a single sentence — the base is "a canonical representation of all fields in the object in the order received", excluding the signature field. There is no worked example, no test vector, and no definition of "canonical representation" in either the developer.visa.com pages or this repository's samples (the samples show signed objects, but not the exact byte string that was signed).
That leaves an independent implementer to guess at least four things:
- Rendering — is each field rendered as raw JSON, a
"name": value line, or something else? Are string values quoted or raw?
- Nested values — compact JSON? Key order preserved? Whitespace rules?
- Joining — line separator (
\n? none? CRLF?), trailing separator or not?
- Field order — "order received" presumably means JSON insertion order, but that's worth confirming since some stacks don't preserve it.
Any two reasonable-but-different answers produce signers and verifiers that cannot interoperate, while each passes its own round-trip tests — the failure mode is silent until two independent implementations meet.
Our documented interpretation
Mirroring the RFC 9421 message-signature base style: one "name": value line per field in object insertion order, string values raw, non-string values as compact JSON, lines joined with \n, signature excluded. Implementation (signer and verifier share it):
https://github.com/AVA-PAY/ava-pay/blob/main/packages/agent-sdk/src/protocol/visa-tap/index.ts (buildTapObjectSignatureBase)
Ask
- A normative worked example in this repo: one sample Consumer Recognition Object plus the exact signature base string and signature for a published test key — that single artifact resolves all four ambiguities at once.
- Failing that, a sentence in the spec defining the rendering/joining/ordering rules.
If our interpretation matches Visa's intent, we're happy to contribute a PR adding test vectors (we already generate them in CI). If it doesn't, we'll change our implementation to match — one function, by design.
Thanks — the three-layer design is clean to implement against, and production-verifiable IdTokens via the public JWKS made independent validation possible. This is the one place we had to interpret rather than follow.
Context
We've built an independent merchant-side TAP verifier (AVA Pay) implementing all three layers to public-spec depth: the RFC 9421 message signature (
agent-browser-auth/agent-payer-authtags, ed25519 + rsa-pss-sha256), Consumer Recognition Object validation including the Visa-signed PS256 IdToken verified against the production JWKS atmcp.visa.com/.well-known/jwks, and Agentic Payment Container validation. Wire format is locked to this repo's sample messages, and our test suite runs real cryptography for every layer.One construct blocked exact interoperability: the object signature base for the signed body objects.
The gap
For the object-level signatures (Consumer Recognition Object, Agentic Payment Container), the public spec's guidance is a single sentence — the base is "a canonical representation of all fields in the object in the order received", excluding the signature field. There is no worked example, no test vector, and no definition of "canonical representation" in either the developer.visa.com pages or this repository's samples (the samples show signed objects, but not the exact byte string that was signed).
That leaves an independent implementer to guess at least four things:
"name": valueline, or something else? Are string values quoted or raw?\n? none? CRLF?), trailing separator or not?Any two reasonable-but-different answers produce signers and verifiers that cannot interoperate, while each passes its own round-trip tests — the failure mode is silent until two independent implementations meet.
Our documented interpretation
Mirroring the RFC 9421 message-signature base style: one
"name": valueline per field in object insertion order, string values raw, non-string values as compact JSON, lines joined with\n,signatureexcluded. Implementation (signer and verifier share it):https://github.com/AVA-PAY/ava-pay/blob/main/packages/agent-sdk/src/protocol/visa-tap/index.ts (
buildTapObjectSignatureBase)Ask
If our interpretation matches Visa's intent, we're happy to contribute a PR adding test vectors (we already generate them in CI). If it doesn't, we'll change our implementation to match — one function, by design.
Thanks — the three-layer design is clean to implement against, and production-verifiable IdTokens via the public JWKS made independent validation possible. This is the one place we had to interpret rather than follow.