Skip to content
Open format v0.1

Price receipts

Every price the Agorantis agent service shows comes with a signed receipt: the store, the product, the price and when it was read, signed with our key. Anyone can check one, here in your browser or in ten lines of code.

Check a receipt

Paste a whole answer from the agent service, MCP or HTTP, or one priced offer with its receipt. Each price is checked against our published keys.

Checked in this browser. Nothing you paste leaves it; the page only fetches our public keys.

The format

A receipt sits beside each price's proof. It holds no account, key, email, address or anything else about who asked, so it can be handed to anyone.

"receipt": {
  "v": "0.1",
  "key_id": "21fe31dfa154a261",
  "issued_at": "2026-11-01T12:00:00.000Z",
  "read": "page",
  "sig": "kOiq03shA3BWL9XCt0m26hHYS68zfzVI5YyHpJXLHXUS3mSA86vP_vaTX1dx9W-oTCfm85zsof5aKoBqLw5tBA"
}
FieldMeaning
vThe format version, "0.1".
key_idWhich of our keys signed it: the first 16 hex characters of SHA-256 of the raw 32-byte public key.
issued_atWhen the answer was made and signed (ISO 8601, UTC).
readHow the price was read: "page", the shop's own page (every v0.1 price). Later also "feed" or "store".
sigThe Ed25519 signature, base64url without padding.

What is signed

One claim: the receipt's v, key_id, issued_at and read, plus these values exactly as the answer shows them. A field marked "left out" is not in the claim at all, never null. Shipping is shown beside the price but is not signed in v0.1.

Claim fieldAn offerA history pointA lowest price
storeshop.domainthe point's shop.domainlowest.shop
listingproof.listingproof.listingproof.listing
productthe answer's product.name (a scan offer's own product.name)the answer's product.namethat product's name
price_minorprice.minorprice.minoramount times 100, rounded to a whole number
currencyprice.currencyprice.currencycurrency
stockstockleft outleft out
checked_atchecked_atchecked_atchecked_at
proofproof.fingerprintproof.fingerprintproof.fingerprint

An offer is a price from price_at, cheapest, live_check or market_scan; a history point comes from price_history; a lowest price from find with a country.

Canonical JSON and Ed25519

The claim is written as canonical JSON (RFC 8785): keys sorted, no spaces, strings and whole numbers written as JSON.stringify writes them. The signature is Ed25519 over those UTF-8 bytes. The claim behind the receipt above:

{"checked_at":"2026-11-01T11:30:00.000Z","currency":"EGP","issued_at":"2026-11-01T12:00:00.000Z","key_id":"21fe31dfa154a261","listing":"https://shop-a.example/coffee","price_minor":9500,"product":"Sunny Morning Coffee 200g","proof":"fp-1-1","read":"page","stock":"in_stock","store":"shop-a.example","v":"0.1"}

Our public keys

mcp.agorantis.com/.well-known/agorantis-receipt-keys.json lists every key that ever signed a receipt: its key_id, the public key as a JWK (kty OKP, crv Ed25519, x), whether it is active or retired, and its dates. A retired key stays listed forever, so an old receipt still checks.

Check it yourself

Node 22 or later, or any current browser:

const canon = (v) => Array.isArray(v) ? `[${v.map(canon)}]` : v && typeof v === "object"
  ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`)}}` : JSON.stringify(v);
const b64 = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
async function check(offer, product, keysUrl) {        // one priced offer and its answer's product
  const r = offer.receipt, { keys } = await (await fetch(keysUrl)).json();
  const key = await crypto.subtle.importKey("jwk", keys.find((k) => k.key_id === r.key_id).jwk, { name: "Ed25519" }, false, ["verify"]);
  const claim = { v: r.v, key_id: r.key_id, issued_at: r.issued_at, read: r.read, store: offer.shop.domain, listing: offer.proof.listing, product: product.name,
    price_minor: offer.price.minor, currency: offer.price.currency, stock: offer.stock, checked_at: offer.checked_at, proof: offer.proof.fingerprint };
  return crypto.subtle.verify({ name: "Ed25519" }, key, b64(r.sig), new TextEncoder().encode(canon(claim)));
}

// check(answer.results[0], answer.product, "https://mcp.agorantis.com/.well-known/agorantis-receipt-keys.json")

check gives true only when every value matches what was signed. Change one character of any of them and it gives false.

Rules

  • A receipt is made when the answer is made, over the exact values shown. Nothing about who asked is ever in it.
  • With no working key there are no receipts: the answer carries no receipt field. A receipt is never made up.
  • While receipts are on, a price that cannot be signed is not shown, the same way a price without its proof is not shown.
  • The private key exists only inside our signing service. No person ever sees it.