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 }