Documentation menu · receipts

Receipts

Every billed or failed request gets a receipt signed with the exchange's Ed25519 key — so the record of what you bought doesn't depend on trusting a dashboard.

Fetching a receipt

The receipt id comes back as thin.receipt and on the request log.

GET/v1/receipts/{id}
response
{
  "receipt": { … },
  "signature": "<base64url>",
  "kid": "…"
}

Public keys

GET/.well-known/thin-receipts.json
JWKS
{ "keys": [ { "kid": "…", "kty": "OKP", "crv": "Ed25519", "x": "…" } ] }

Verifying it yourself

The signature covers the UTF-8 bytes of JSON.stringify(receipt) exactly as returned. In Node 20+ or any modern browser:

verify.mjs
const BASE = "http://localhost:8787";

const b64url = (s) =>
  Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));

export async function verifyReceipt({ receipt, signature, kid }) {
  const { keys } = await (await fetch(`${BASE}/.well-known/thin-receipts.json`)).json();
  const jwk = keys.find((k) => k.kid === kid);
  if (!jwk) return false;
  const key = await crypto.subtle.importKey("jwk", jwk, { name: "Ed25519" }, false, ["verify"]);
  const bytes = new TextEncoder().encode(JSON.stringify(receipt));
  return crypto.subtle.verify({ name: "Ed25519" }, key, b64url(signature), bytes);
}

Or let the exchange check it — handy as a sanity test:

POST/v1/receipts/verify
→
{ "valid": true }