Receipts
Before an agent signs, tide dry-runs the exact bytes and returns a receipt it signs: what the transaction does, what Sui enforces, and whether it matches the agent's intent. After it runs, tide compares the chain with the receipt.
Make a receipt
Send the full unsigned TransactionData (the bytes the wallet signs) and, optionally, the intent. Every receipt gets a page at tide.ad/proof/<id> to show the human.
curl -X POST "https://api.tide.ad/v1/tx/receipt" \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"tx_bytes": "AAAC...",
"intent": { "action": "swap", "max_spend": { "USDC": "5" }, "min_receive": { "SUI": "4.9" } }
}'tx.build (and the MCP tool build_transaction) returns a receipt with the bytes it builds. In MCP the tools are receipt_for_transaction, verify_receipt and confirm_execution.
What a receipt holds
| Field | Meaning |
|---|---|
| intent | What the agent said it means to do, as sent. |
| tx.digest | The blake2b digest of the bytes: the digest Sui will give the transaction. |
| tx.bytes_base64 | The exact bytes, so anyone can run them again. |
| simulation | The checkpoint of the dry run and its effects: status, every balance change, objects created, changed, moved and deleted, events and gas. |
| guards | What Sui itself enforces when the bytes run, in plain words: the swap minimum, and under an Agent Pass its caps, pools, price floor and budget. |
| checks | tide's checks of the intent: spend at most, receive at least, nothing else leaves the wallet, deadline, the pass's rules, the pre-sign check. |
| verdict | pass when every check passes, otherwise fail with the reason. |
Verify it yourself
The signature is ed25519 over the UTF-8 bytes of tide-receipt-v1, a newline, and the receipt as canonical JSON (object keys sorted at every level, no whitespace). The public key is at api.tide.ad/.well-known/tide-attest.json. This script checks the signature, checks that the digest is the digest of the bytes, and runs the same bytes on a public Sui full node. A dry run at the same state gives the same effects; a later one can differ where prices or balances have moved since.
import { createPublicKey, verify } from "node:crypto";
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { TransactionDataBuilder } from "@mysten/sui/transactions";
const canonical = (v: unknown): string =>
v === null || typeof v !== "object" ? JSON.stringify(v ?? null)
: Array.isArray(v) ? "[" + v.map((x) => canonical(x ?? null)).join(",") + "]"
: "{" + Object.keys(v).filter((k) => (v as any)[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical((v as any)[k])).join(",") + "}";
const BASE = "https://api.tide.ad";
const RECEIPT_ID = "YOUR_RECEIPT_ID";
const { receipt, signature } = await (await fetch(`${BASE}/v1/receipts/${RECEIPT_ID}`)).json();
const { keys } = await (await fetch(`${BASE}/.well-known/tide-attest.json`)).json();
const key = keys.find((k: any) => k.key_id === signature.key_id);
// 1. tide signed this receipt
const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(key.public_key_base64, "base64")]);
const ok = verify(null, Buffer.from("tide-receipt-v1\n" + canonical(receipt)), createPublicKey({ key: spki, format: "der", type: "spki" }), Buffer.from(signature.signature_base64, "base64"));
// 2. the digest is the digest of the bytes in the receipt
const bytes = Buffer.from(receipt.tx.bytes_base64, "base64");
const digestOk = TransactionDataBuilder.getDigestFromBytes(bytes) === receipt.tx.digest;
// 3. any full node gives the same dry run
const sui = new SuiGrpcClient({ network: "mainnet", baseUrl: "https://fullnode.mainnet.sui.io:443" });
const { response } = await sui.transactionExecutionService.simulateTransaction({
transaction: { bcs: { value: bytes } },
readMask: { paths: ["transaction.effects.status", "transaction.balance_changes"] },
});
const changes = response.transaction?.balanceChanges?.map((b) => `${b.address} ${b.coinType} ${b.amount}`);
console.log({ signedByTide: ok, digestMatchesBytes: digestOk, success: response.transaction?.effects?.status?.success, changes });
After execution
GET /v1/tx/{digest}/receipt-check?receipt=<id> reads the executed transaction and compares status, balance changes, objects, events and gas with the receipt. The answer is match, differs with one plain sentence per difference, not_executed, or other_transaction when the signed bytes were not the receipt's.
Agent Pass
When the bytes act under an Agent Pass, the receipt names the pass and checks the action against its rules as they stand: active, signed by its agent, the rule for the pair, per-trade and daily limits, listed pools, price floor and budget left. GET /v1/agent-pass/report?owner=0x... is a plain account of an owner's passes and everything signed under them.
Methods
| Method | What it does | Credits |
|---|---|---|
| tx.receiptPOST /v1/tx/receipt | Signed receipt for unsigned bytes: simulated effects, Agent Pass policy check, intent checks and a verdict, signed by tide. | 10 |
| receipts.getGET /v1/receipts/:id | A signed receipt by id. | 1 |
| receipts.verifyPOST /v1/receipts/verify | Check a receipt's signature and that its digest is the digest of its bytes. | 1 |
| tx.receiptCheckGET /v1/tx/:digest/receipt-check | After execution: compare the transaction's actual effects with its receipt. | 2 |
| tx.receiptCheckPostPOST /v1/tx/:digest/receipt-check | After execution: compare actual effects with a receipt you send in the body. | 2 |
| meta.attestKeyGET /.well-known/tide-attest.json | tide's attestation public key (ed25519) for receipts. | Free |
| agentPass.reportGET /v1/agent-pass/report | Agent Pass report: an owner's (or agent's) passes, their rules in plain words, spend against caps and every action, for a human. | 5 |
| agentPass.precheckPOST /v1/agent-pass/precheck | Will this Agent Pass accept the action? Each rule checked before anything is signed. | 2 |