# Receipts and verification

> Read your bot's execution receipts, and check a published receipt yourself: its Ed25519 signature, then the chain.

Canonical page: https://nevarn.com/docs/receipts
Updated: 2026-11-02

Every executed trade has one receipt linking the Decionis ruling to the order, the fill transaction, settlement, the ledger entries and the balance after. Your bot reads its own with a read key. Every receipt in the public feed, and on the trading record page, is also signed with Nevarn's receipt key, and every step that happened on a chain can be read back from the chain: neither check needs an account, Nevarn's cooperation or trust in whoever handed you the receipt.

## Your bot's receipts

Once a Production execution is confirmed, its receipt_id is set. GET /machine/receipts/{id}, with a read key and the execution's id, returns the receipt: the ruling, the order, the fill transaction, settlement, the ledger entries and the balance after, each with its status. The SDKs call it getReceipt and the MCP server get_receipt.

```bash
curl -s https://api.nevarn.com/machine/receipts/<execution id> -H "Authorization: Bearer $NEVARN_API_KEY"
```

Shadow intents never execute, so they never have a receipt, and neither does a paper fill in Sandbox: a receipt proves what happened on a chain. An id with no receipt on your account answers 404.

## Published receipts

> The receipt feed is switched on per deployment. Until it is published, both addresses below answer 404.

## Where the receipts and the key are

| What | Where |
| --- | --- |
| The feed: the platform trading wallet's receipts, newest first | https://api.nevarn.com/public/receipts |
| The key set that signs it (JSON Web Keys, public keys only) | https://api.nevarn.com/.well-known/nevarn-receipt-keys.json |

A published receipt holds the trading wallet's address, chain data (the order hash its fills emit, the fill transactions and blocks, the balance after the trade), the Decionis decision and dossier identifiers with the policy that ruled, ledger postings with internal identifiers removed, and the platform's own vocabulary. It holds no token, key, signature or credential that could move funds or open an account, no account or tenant identifier, and nothing from the Decionis dossier itself.

## 1. Check the signature

1. Fetch the key set. Each key is an Ed25519 public key whose kid is its RFC 7638 thumbprint, so a key set cannot relabel a key. The signing key comes first; retired keys stay listed so a receipt signed before a rotation still verifies.
2. Take the object that carries the proof: one receipt, or the whole feed. Remove its proof member and write the rest as canonical JSON (RFC 8785: object keys sorted, no whitespace; receipts hold only strings, integers, booleans and null).
3. Verify proof.signature (base64url) over those bytes with the key whose kid is proof.kid. The algorithm is EdDSA.
4. Each receipt's proof covers that receipt alone, so it can be checked after it is copied out of the feed. The feed's proof covers the wallet, the issue time and the full list, so nothing can be added to it or dropped from it unnoticed.

```python
import base64, hashlib, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

def b64(text):
    return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))

def canonical(value):
    # RFC 8785 for the values a receipt holds: sorted keys, no whitespace.
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()

def verified(signed, keys):
    body = {name: value for name, value in signed.items() if name != "proof"}
    proof = signed["proof"]
    for key in keys["keys"]:
        members = json.dumps({"crv": key["crv"], "kty": key["kty"], "x": key["x"]}, separators=(",", ":"))
        kid = base64.urlsafe_b64encode(hashlib.sha256(members.encode()).digest()).decode().rstrip("=")
        if key["kid"] == proof["kid"] == kid and proof["alg"] == "EdDSA":
            try:
                Ed25519PublicKey.from_public_bytes(b64(key["x"])).verify(b64(proof["signature"]), canonical(body))
                return True
            except Exception:
                return False
    return False
```

```js
import { createHash, createPublicKey, verify } from "node:crypto";

// RFC 8785 for the values a receipt holds: sorted keys, no whitespace.
function canonical(value) {
  if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
  if (value !== null && typeof value === "object") {
    return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
  }
  return JSON.stringify(value);
}

export function verified(signed, keys) {
  const { proof, ...body } = signed;
  const key = keys.keys.find((candidate) => candidate.kid === proof.kid);
  if (!key || proof.alg !== "EdDSA") return false;
  const kid = createHash("sha256").update(JSON.stringify({ crv: key.crv, kty: key.kty, x: key.x })).digest("base64url");
  if (kid !== key.kid) return false;
  const publicKey = createPublicKey({ key: { kty: key.kty, crv: key.crv, x: key.x }, format: "jwk" });
  return verify(null, Buffer.from(canonical(body)), publicKey, Buffer.from(proof.signature, "base64url"));
}
```

## 2. Check it against the chain

1. For each fill transaction, read its receipt from any Polygon node (chain_id 137): the status is 1, and the block number and block hash are the receipt's.
2. In that transaction's logs, the exchange named in order.exchange emitted OrderFilled(bytes32,address,address,uint8,uint256,uint256,uint256,uint256,bytes32,bytes32). Its first indexed topic is the receipt's id (the order hash) and its second is the trading wallet, the maker. The side and token id decode from its data and match the order.
3. Add up the filled amounts across the receipt's fill transactions: they equal the settlement's paid, received and fee figures.
4. Read balanceOf(trading wallet) on balance.token_address at balance.block_number: it equals balance.balance_minor, and that block is at or after the last fill. Historical reads need an archive node.
5. Each ledger entry's postings sum to zero in every asset: what left one account arrived in another.
6. The ruling is Decionis's. Its decision and dossier identifiers are the ones Decionis issued for this exact order, and Decionis, not Nevarn, verifies its own decision record.

```bash
RPC=https://polygon-bor-rpc.publicnode.com   # or any Polygon JSON-RPC endpoint
# The fill: status, block number and block hash.
curl -s "$RPC" -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<fills.transactions[0].transaction_hash>"]}'
# The balance after the trade: balanceOf(wallet) at the receipt's block, the block number in hex.
curl -s "$RPC" -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"<balance.token_address>","data":"0x70a08231000000000000000000000000<wallet without 0x>"},"<0x block number>"]}'
```

The feed is signed again each time it is served and cached for five minutes, so issued_at says when this copy was made. A receipt's own signature changes only when the receipt does: a link that was missing, such as the balance after the trade, appears once the records back it.
