A marketplace is a long project. A signed receipt is a string.
If you already hold a JSON receipt from an AIMarket hub, you can check the signature in your own stack this weekend. You do not need to reimplement the hub, stand up a second one, or read the Python reference. The bytes are written down. The files that decide whether you got them right are already in git.
Repo: alexar76/aimarket-protocol. Dual license, Apache-2.0 OR MIT. Spec section that matters: §7.3. Vectors: test-vectors/.
This is one slice of the protocol. Passing it does not make a hub, and it does not make anyone "certified." There is no conformance badge to collect. There is a yes/no on seven files.
What you are checking
Every signature in this protocol is Ed25519 over the UTF-8 of one canonical string. The result is standard base64, with padding. The string is name:value pairs joined by |, in a fixed order, with no spaces.
A v1 receipt signs exactly this:
nonce:{nonce}|product_id:{product_id}|capability_id:{capability_id}|price_usd:{price_usd}|timestamp:{timestamp}|success:{0|1}|latency_ms:{latency_ms}
The published vector receipt-signed.json signs this exact line:
nonce:rcpt_test001|product_id:prod-001|capability_id:translate.multi@v2|price_usd:0.4|timestamp:2026-08-28T09:00:50Z|success:1|latency_ms:8100
The JSON says "success": true. The signed bytes say success:1. true and false never appear in the string. A missing success is 0. A missing latency_ms is 0.
Build that string from the file. Verify it with the pinned public key below. If your string differs by one character, the signature is wrong, and that is the test working.
The key is not inside the receipt
Pin this public key. It is the test key, published so the vectors are reproducible. Do not use it in production. Do not copy the private key out of test-vectors/README.md into anything that ships.
11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
signature.public_key inside a receipt is a claim. A verifier that trusts it will accept a signature from whoever generated the file. The test key lives beside the vectors. A production key lives in your config, pinned by you.
Four ways a careful parser still fails
Numbers are tokens. price_usd and latency_ms are copied as the exact JSON number text. The v2 vector signs price_usd:0.0, not price_usd:0. Parse the number, print it from a float, and those two stop being the same string.
A rejection is a longer string. receipt-v2-signed.json carries "type": "rejection" and "signature.version": 2. Its canonical line is:
nonce:rcpt_test002|product_id:prod-001|capability_id:translate.multi@v2|price_usd:0.0|timestamp:2026-08-28T09:00:50Z|success:0|latency_ms:0|v:2|fields:62e8135ac924809d58b3b8d8e8e0ff9b3bdaaabca378881003524c36408a61aa
The fields digest is SHA-256, lowercase hex, over one JSON object. The keys are:
type, channel_id, category, plugin, reason, verify_score, delivery_reasons, trace_id, refunded
A key that is absent is JSON null, not omitted. The object is serialized with sorted keys, ensure_ascii off, and compact separators: a comma between members, a colon after a key. No spaces. Sorted, the hashed order is:
category, channel_id, delivery_reasons, plugin, reason, refunded, trace_id, type, verify_score
Manifest digests elsewhere in the same spec use a space after : and ,. Receipt fields do not. One helper for both will verify one of them and fail the other.
Read the version from signature.version. It is the JSON integer 2 on this file. Do not infer it from a header.
Non-ASCII stays non-ASCII. The digest emits characters as themselves. A serializer that writes \uXXXX produces a different hash, and every v2 signature fails.
The downgrade file is not a clean accept. receipt-v2-downgraded-to-v1.json carries v2 fields under a signature that does not cover them. A weekend verifier that does not need to speak to old peers should refuse it. If you accept the v1 half, you still have to say which of those fields are unsigned. A bare "ok" on that file is a fail.
The seven files
Accept:
test-vectors/receipt-signed.jsontest-vectors/receipt-v2-signed.json
Reject:
test-vectors/negative/receipt-flipped-success.jsontest-vectors/negative/receipt-tampered-latency.jsontest-vectors/negative/receipt-stale-timestamp.jsontest-vectors/negative/receipt-wrong-signer.jsontest-vectors/negative/receipt-v2-downgraded-to-v1.json
Language is yours. Go, Rust, C#, Kotlin, TypeScript, whatever you already run. The thing that does not count is a wrapper around aimarket-hub. Start from spec.md §7.3 and the vectors. Leave the hub repo closed.
Vectors are necessary. They are not the whole protocol. Invoke, federation, and payment channels are other slices. This weekend is the receipt.
What counts
An implementation counts here when three things are true:
- The author is not alexar76. A second copy written by the maintainer, in any language, does not add one.
- The code is not derived from the reference hub.
- The seven files above do what the table says, and the repo is public.
Shared governance of the spec, as written in GOVERNANCE.md, waits on three implementations the maintainer does not operate, at least two of them not grown from the reference. A receipt checker is one slice toward that, not the institution. No foundation, no badge, no grant program in this post.
When yours passes, open an issue on the protocol repo:
https://github.com/alexar76/aimarket-protocol/issues/new
Title: receipt: <language>
Language:
Repo:
I am not alexar76. This is not a wrapper of aimarket-hub.
Accepts receipt-signed.json and receipt-v2-signed.json.
Rejects the five negative receipt files listed in the challenge.
That issue is the count. A star on the spec repo is optional, and it is not a substitute for the link.
If your string matches the published canonical and the signature still fails, the bug is worth an issue too. Bring the string you signed. Do not bring a patch that makes the vector match the hub.


Top comments (0)