Originally published at avalayer.com/writing, Field Notes 001.
Verification is a strange business. What you're selling, when you get down to it, is certainty about other people's promises. So when we built a merchant-side verification layer for AI shopping agents, we made ourselves a rule: implement every protocol to its public-spec depth, validate against live production key material, and wherever we couldn't be certain, say so out loud. This essay is about the one place that rule got interesting.
Some background. Our API accepts a signed request from any AI agent and returns a verdict: trusted or not, with a typed reason. To say that honestly we implemented four protocols, including Visa's Trusted Agent Protocol. Most of that work went the way implementation work should go. The spec says a thing, you build the thing, the tests pass, and cryptography does what cryptography does. Cryptography is the reliable half of this industry. The unreliable half is agreement. A specification is an agreement about reality between people who have never met, and like most such agreements, it holds right up until someone reads it carefully.
Three layers, two of them clean
Visa TAP is a three-layer protocol. The outer layer is an RFC 9421 HTTP message signature binding the agent's identity and intent to a specific merchant and path. The middle layer is a Consumer Recognition Object carried in the body, including a Visa-signed identity token you can verify against Visa's published JWKS at mcp.visa.com. The inner layer is an Agentic Payment Container, also signed.
Layers one and two were a pleasure. RFC 9421 is a mature spec with a defined signature base, worked examples, and years of implementation scar tissue behind it. The identity token verifies against a live public key set; we pulled Visa's production JWKS, resolved their real RSA key, and verified real signatures. When a spec gives you that much to push against, independent validation is possible, and independent validation is the whole game.
Layer three is where we met the sentence.
One sentence, four questions
The signed body objects need a signature base: the exact byte string that gets signed and verified. The public spec describes it in a single sentence, saying the base is "a canonical representation of all fields in the object in the order received," excluding the signature field itself.
That sentence is doing the work of a chapter, and it answers none of the questions an implementer actually has:
- Rendering. Is each field raw JSON? A name and value pair? Are strings quoted or bare?
- Nested values. Compact or pretty printed? Keys reordered or preserved?
- Joining. Newlines between fields? A trailing one or not?
- Ordering. "Order received" presumably means insertion order, but not every language keeps its promises about that.
Here is the trap. The sentence isn't wrong. That's the problem. Wrong would be visible. Any reasonable answer to those four questions produces a working implementation. Sign with your answer, verify with your answer, and every test passes. Your implementation is correct. So is everyone else's. Everyone is correct alone, the way two people can both be on time for a meeting scheduled in different time zones.
The signature base never travels on the wire. It exists briefly in the signer's memory and the verifier's memory, and if those two constructions differ, no error message will ever say so. What you get instead is cryptography's entire emotional range: invalid signature. When the mismatch finally surfaces, months from now, between two teams who each shipped a perfectly self-consistent product, it will look like a bug. It won't be a bug. It will be a disagreement that nobody knew they were having.
We looked for the artifact that would settle it. The developer documentation has no worked example. The sample repository shows signed objects but never the bytes that were signed. No test vector exists. As far as we can tell, there is no public object in the world against which two independent TAP implementations can prove they agree.
What we did about it
Three things, in increasing order of usefulness to everyone else.
First, we picked an interpretation and labeled it as one. Our reading mirrors the RFC 9421 base style: one name and value line per field in insertion order, strings bare, everything else compact JSON, newline joined. It lives in a single function shared by our signer and our verifier, so our round-trip is exact, and if Visa ever publishes a normative answer that differs, the fix is one function. Quarantine your guesses. Give each one an address.
Second, we wrote the uncertainty into our public docs instead of around it. Our README says the construct is under-specified and points at the function holding our interpretation. The alternative, quietly guessing while selling certainty, is the kind of contradiction customers eventually find.
Third, we filed a public issue on Visa's repository asking for the one artifact that resolves all four questions at once: a sample object, its exact signature base string, and a valid signature under a published test key. We offered to contribute test vectors ourselves if our interpretation matches their intent. That issue lives at github.com/visa/trusted-agent-protocol/issues/23, and if you're implementing TAP, you're welcome in it.
The general lesson
Test vectors are as normative as prose. More, honestly, because prose can be ambiguous in ways nobody notices until it's expensive, and a byte string cannot. Prose is where a spec makes its promises. Test vectors are where it keeps them.
Ambiguity in a spec is a loan, and the interest is paid by whoever implements second. Right now the agentic commerce space is taking out a lot of these loans: Visa's TAP, Google's AP2, the IETF's Web Bot Auth drafts, EMVCo's credential schemas, all being written in public, at speed, mostly before any two independent implementations have met. That meeting is where specs are actually tested, and almost none of them have happened yet. When we submitted comments on EMVCo's draft schema framework this month, our first request was the same one we made to Visa: ship the worked example with version one, not after the divergence.
If you're writing a spec in this space: publish one complete sample, the exact bytes that get signed, and a signature under a test key. It's an afternoon of work that forecloses years of silent incompatibility. If you're implementing one: when you find a sentence doing the work of a chapter, don't just pick an answer. Isolate the guess, name it a guess, and say so where the next implementer will trip over it. Being loud about ambiguity is cheap exactly once, at the beginning. We're at the beginning.
That's the discipline we're trying to hold ourselves to, anyway. Verification is a trust business, and a trust business that rounds up isn't one. Where the spec is precise, we're precise. Where it isn't, we say so, in public, and we ask.
Trust is not built where everyone agrees. It's built where someone admits they aren't sure, and shows their work.
– N.M.
AVA Payâ„¢ is a merchant-side trust gateway for AI commerce: one API that verifies the signatures AI shopping agents already carry (IETF Web Bot Auth, Visa TAP, Google AP2) and lets merchants set policy for what verified identity earns. The verifier, SDK, and all 240 real-cryptography tests are open source at github.com/AVA-PAY/ava-pay. More essays at avalayer.com/writing.
Top comments (0)