BTC$68,420▲ 0.82% / ETH$3,841▼ 0.31% / LTC$94.05▲ 1.14% / SOL$221.60▲ 3.02% / USDT·TRC20$1.0000 / USDC·ERC20$0.9999 / XMR$168.42▼ 0.18% / TON$6.02▲ 0.47% / XRP$2.41▼ 0.09% / GAS·ETH18 gwei / Indicative prices · illustrative ticker / No-KYC · non-pooled · 0.4% flat / BTC$68,420▲ 0.82% / ETH$3,841▼ 0.31% / LTC$94.05▲ 1.14% / SOL$221.60▲ 3.02% / USDT·TRC20$1.0000 / USDC·ERC20$0.9999 / XMR$168.42▼ 0.18% / TON$6.02▲ 0.47% / XRP$2.41▼ 0.09% / GAS·ETH18 gwei / Indicative prices · illustrative ticker / No-KYC · non-pooled · 0.4% flat
OrbChain
UTC 18:47:02 Get started →
§ Engineering · 2026-04-19

Signed settlement receipts:
why Ed25519, why offline.

Every payment that reaches PAID state on OrbChain carries a cryptographically signed receipt. Merchants can verify those receipts without talking to us, using a 10-line function and our public key. This post explains what that buys you and why no other gateway in our category ships it.

The problem receipts solve

Most crypto gateways hand you a JSON payload and a webhook HMAC, and that's the end of their attestation surface. If the dispute is "the payment happened, you're just not showing it in your dashboard anymore," you end up emailing a CSV and hoping. If the gateway ever goes out of business, every historical record becomes non-verifiable. Crypto is supposed to fix exactly this — payments are public on-chain, attestations shouldn't depend on the gateway's survival.

A signed receipt is a specific artifact: a payload describing the payment (amount, address, tx hash, timestamps, merchant id) plus an Ed25519 signature from the gateway. The merchant — or an auditor, or a lawyer in 2040 — can take the receipt, our public key, and verify the signature locally. No network call to us. No dependency on us still existing.

Why Ed25519

Short version: it's the right cryptographic default in 2026. Signatures are 64 bytes, public keys are 32 bytes, signing is deterministic, verification is fast, and the algorithm has held up well against post-quantum concerns for what we need (receipts don't need to be secret or forward-secret — just verifiable).

ECDSA-secp256k1 would have been fine. Ed25519 is cleaner to implement correctly (no ephemeral-nonce footgun), and Node's built-in crypto module supports PEM-encoded Ed25519 keys out of the box:

import { createPrivateKey, createPublicKey, sign } from "crypto";

const key = createPrivateKey(process.env.RECEIPT_SIGNING_KEY);
const signature = sign(null, Buffer.from(canonical), key); // 64 bytes

Canonical JSON is the hard part

The naive approach — sign(JSON.stringify(receipt)) — breaks the moment two parties disagree on key ordering or number formatting. We stabilize the payload with a sorted-keys stringifier that also normalizes number output. That's in src/services/receipts.ts; it's ~20 lines and it's the only place the canonical format is defined.

The payload itself is deliberately flat — one level of keys, no nested objects, no optional-sometimes-present fields. Hard to get wrong at verify time.

The 10-line verifier

Here's the entire receiver side, in Node.js:

import { createPublicKey, verify } from "crypto";

async function verifyReceipt(receipt) {
  const pubPem = await (await fetch(
    "https://orbchain.io/v1/common/receipt-public-key"
  )).text();
  const pubKey = createPublicKey(pubPem);
  const canonical = canonicalize(receipt.body); // sorted-keys JSON
  const sig = Buffer.from(receipt.signature, "base64");
  return verify(null, Buffer.from(canonical), pubKey, sig);
}

Python is the same shape with cryptography; Go with crypto/ed25519; Rust with ed25519-dalek. The public key endpoint (/v1/common/receipt-public-key) is cacheable — the key rotates only on our say-so, and rotations publish a new endpoint version.

What we refused to ship

  • Nested receipt payloads. Some designs include the full webhook body, the full address row, and a snapshot of merchant settings. Every additional field is a surface for disagreement between signer and verifier. Flat is safer.
  • Custom signature envelopes (JWS, CMS). Both add encoder / decoder complexity that has no upside here. We sign raw canonical JSON; the signature travels as a sibling field, base64-encoded. That's it.
  • Ephemeral keys. Receipts need to verify in 2040. Ephemeral keys would require us to maintain a key-registry service in 2040. We use a single long-lived key, with a documented rotation procedure.

When we sign

Receipts are produced on demand — we don't store them. When a merchant requests GET /v1/payment/:track_id/receipt, the current signing key signs the current canonical body and returns. This keeps the database simple (no receipt table), makes key rotation trivial (re-sign any future request with the new key), and means every verifier gets a fresh signature they can trust.

The receipts survive us. That's the whole point.

§ Keep reading

Related posts.

Hand-picked
Same rails, next questions