Cryptographic receipts for AI agent decisions β MCP server, x402 keyless path, AWS Bedrock AgentCore, and receipt verification.
E2E validated 2026-09-16 (receipt nr_d9583861b8fb6ab620ce64bec8c14b73, Base block 51367733).
@fairseal/mcp-server (npm v0.2.1)verify.fairseal.io browser UI or @fairseal/verify offlinePrerequisites: Node.js β₯ 18, a funded Base mainnet wallet (β₯ $0.05 USDC + small ETH for gas). No API key needed for the x402 path.
What exists today: @fairseal/mcp-server v0.2.1 ships a working, end-to-end notarize path. The three-tool flow β fairseal_hash_content β fairseal_notarize_decision β fairseal_notarize_status β is live and independently verifiable. E2E acceptance: receipt nr_d9583861b8fb6ab620ce64bec8c14b73, Base tx 0x63695b8dab26debeβ¦, block 51367733 (2026-09-16).
This guide adds what the README doesn't cover: the direct x402/keyless path with working code, verified receipt field names, AWS AgentCore integration, and the critical delivered-vs-verified distinction.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"fairseal": {
"command": "npx",
"args": ["-y", "@fairseal/mcp-server"],
"env": {
"FAIRSEAL_API_KEY": "fsn_your_key_here"
}
}
}
}
Save and restart Claude Desktop. To verify the server starts correctly, you can run it from your terminal:
# Expected output: [fairseal-mcp-server] ready on stdio β upstream https://api.fairseal.io
npx @fairseal/mcp-server
Press Ctrl+C to exit. In Claude Desktop, the MCP tools will appear in the tool list after restart.
No key yet? Email hello@fairseal.io or see fairseal.io/llms.txt. Key prefix: fsn_. Alternatively, use the x402 path in Part B β no key required.
Ask Claude:
"Hash this text with fairseal_hash_content: [the input your agent acted on]"
Note the returned sha256 β this is your input_hash.
"Hash this text with fairseal_hash_content: [the decision the agent made]"
Note that sha256 β this is your decision_hash.
Why hash locally? fairseal_hash_content is a pure local function β SHA-256 in the Node.js process. Your raw content never leaves your machine. Only the hash goes to FairSeal.
"Call fairseal_notarize_decision with:
- agent_id:my-agent-v1
- input_hash:<from step 2>
- decision_hash:<from step 2>
- description:Approved loan application #4829"
Expected response (HTTP 201, status: "pending_anchor"):
{
"receipt_id": "nr_d9583861b8fb6ab620ce64bec8c14b73",
"leaf_hash": "94daa4c3250b46f2...",
"verify_url": "https://api.fairseal.io/v1/notarize/nr_d9583861b8fb6ab620ce64bec8c14b73",
"status": "pending_anchor",
"created_at": "2026-09-16T02:12:51.970Z"
}
"pending_anchor" means: Your request was received, accepted, and queued into the next Merkle batch. The batch anchors to Base mainnet within ~2 minutes. This is not yet cryptographic verification β see Part D.
Wait ~2 minutes, then ask:
"Call fairseal_notarize_status with receipt_id: nr_d9583861b8fb6ab620ce64bec8c14b73"
Once status is anchored, you receive the Merkle proof and Base tx hash. See Part D for offline verification.
Security: The private key below is your wallet's spending key. Store it securely (env var, secret manager). Never commit it to version control. You need β₯ $0.05 USDC and a small amount of ETH for gas on Base mainnet.
mkdir fairseal-notarize && cd fairseal-notarize
npm init -y
npm install @x402/fetch @x402/evm viem @fairseal/verify
// notarize.mjs
import { createHash } from "node:crypto";
import { createWalletClient, createPublicClient, http } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { toClientEvmSigner } from "@x402/evm";
// βββ Wallet setup ββββββββββββββββββββββββββββββββββββββββββββ
const key = process.env.BUYER_KEY; // 0x<64-hex-private-key>
if (!key) { console.error("Set BUYER_KEY env var"); process.exit(1); }
const account = privateKeyToAccount(key);
const rpcUrl = "https://mainnet.base.org";
const pub = createPublicClient({ chain: base, transport: http(rpcUrl) });
const wal = createWalletClient({ account, chain: base, transport: http(rpcUrl) });
const signer = toClientEvmSigner({
address: account.address,
signTypedData: wal.signTypedData.bind(wal),
readContract: pub.readContract.bind(pub),
});
// βββ x402 auto-pay client ββββββββββββββββββββββββββββββββββββ
const client = new x402Client().register("eip155:8453", new ExactEvmScheme(signer));
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
// βββ Hash content locally (raw content never sent) ββββββββββ
const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
const agentInput = "Should we approve loan application #4829?";
const agentDecision = "Approve. Debt-to-income ratio within policy limits.";
const input_hash = sha256(agentInput);
const decision_hash = sha256(agentDecision);
// βββ POST to x402 endpoint β auto-pays $0.02 USDC βββββββββββ
const body = {
schema: "agent_decision",
payload_hash: decision_hash,
metadata: {
agent_id: "my-agent-v1",
decided_at: new Date().toISOString(),
input_hash,
decision_hash,
description: "Loan approval decision", // optional
session_id: "session-abc123", // optional
model: "claude-fable-5", // optional
operator: "FinAdvisorCo", // optional
}
};
console.log("Sending notarize request (will auto-pay $0.02 USDC on 402)...");
const response = await fetchWithPayment(
"https://x402.fairseal.io/v1/notarize",
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
);
if (response.status !== 201) {
console.error("Notarize failed:", response.status, await response.text());
process.exit(1);
}
const receipt = await response.json();
console.log("β receipt_id:", receipt.receipt_id);
console.log(" status: ", receipt.status); // pending_anchor
console.log(" verify_url:", receipt.verify_url);
export BUYER_KEY=0x<your-64-hex-private-key>
node notarize.mjs
The server returns this when you POST without auth. @x402/fetch parses and handles it automatically:
{
"x402Version": 2,
"error": "Payment required",
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"amount": "20000", // $0.02 USDC (6-decimal)
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base mainnet
"payTo": "0xBd5eB5559f5Ac14475AC0C81b6735A4d9106ce28",
"maxTimeoutSeconds": 300
}]
}
Add this after the notarize call in your script:
// βββ Poll until anchored (~2 min) βββββββββββββββββββββββββββ
const receiptId = receipt.receipt_id;
let anchored = null;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 15_000)); // wait 15s
const r = await fetch(`https://api.fairseal.io/v1/notarize/${receiptId}`);
anchored = await r.json();
console.log(`poll ${i+1}: status=${anchored.status}`);
if (anchored.status === "anchored") break;
}
if (anchored?.status !== "anchored") {
console.error("Timeout: receipt did not anchor within 6 min. Try again later.");
process.exit(1);
}
console.log("β Anchored at Base block", anchored.proof.anchor_block);
console.log(" tx:", anchored.proof.anchor_tx);
// βββ Offline verification (add to same script after polling) β
import { verifyMerklePath, verifyAnchor } from "@fairseal/verify";
// Step 5a: verify Merkle inclusion (pure local math, no network)
const proof = anchored.proof;
const leafOk = verifyMerklePath(proof.leaf_hash, proof.merkle_path, proof.merkle_root);
console.log("Merkle inclusion:", leafOk ? "β PASS" : "β FAIL");
// Step 5b: verify anchor is on Base mainnet (one RPC call to a node YOU trust)
// v0.4.0 accepts the raw receipt directly β no manual field remapping
const result = await verifyAnchor(anchored, { rpcUrl: "https://mainnet.base.org" });
console.log("On-chain anchor:", result.valid ? "β PASS" : "β FAIL", result.errors);
v0.4.0 raw-receipt adapter: From @fairseal/verify v0.4.0, verifyAnchor accepts the raw response from GET /v1/notarize/:id directly β no manual proof.* field remapping needed. Install: npm install @fairseal/verify@latest. The adapter auto-detects the agent_decision schema and maps proof.anchor_tx, proof.merkle_root, etc. automatically.
rpcUrl is required: Pass a Base mainnet RPC you trust. Using "https://mainnet.base.org" (Coinbase public RPC) is fine for testing. For production, use a dedicated RPC like Alchemy or Infura.
Open verify.fairseal.io, scroll to the "Notarize Receipt Lookup" section, paste your receipt_id (e.g. nr_d9583861b8fb6ab620ce64bec8c14b73) and click "Look Up".
# No auth required β free public lookup
curl -s "https://api.fairseal.io/v1/notarize/nr_d9583861b8fb6ab620ce64bec8c14b73" \
| python3 -m json.tool | grep -E '"status"|"anchor_block"|"anchor_tx"'
"status": "anchored",
"anchor_block": 51367733,
"anchor_tx": "0x63695b8dab26debe215f3ea49c805cbabaf3355c5b495f01afd2a397cc1fd91f",
POST https://x402.fairseal.io/v1/notarize // x402 / keyless path
POST https://api.fairseal.io/v1/notarize // API key path
Content-Type: application/json
Authorization: Bearer fsn_<key> // key path only; omit for x402
{
"schema": "agent_decision", // required β literal string
"payload_hash": "<64-char lowercase sha256>", // required
"metadata": {
"agent_id": "<string>", // required
"decided_at": "<ISO-8601 UTC>", // required
"input_hash": "<64-char lowercase sha256>",// required
"decision_hash": "<64-char lowercase sha256>",// required
"session_id": "<string>", // optional
"model": "<string>", // optional
"description": "<string>", // optional
"operator": "<string>" // optional
}
}
All *_hash fields: 64-char lowercase hex (SHA-256). Unknown fields are rejected. Dates must be ISO-8601 UTC.
Status: AWS Bedrock AgentCore Payments is in preview (announced May 7, 2026). Configuration keys and API shapes cited below are per AWS documentation as of 2026-09-16. Verify against current AWS docs before production use: docs.aws.amazon.com/bedrock
AgentCore Payments implements the x402 protocol natively. When an AgentCore agent calls https://x402.fairseal.io/v1/notarize and receives HTTP 402, AgentCore:
PAYMENT-REQUIRED header (machine-readable JSON, x402Version 2)X-PAYMENT header| Resource | Role |
|---|---|
PaymentCredentialProvider | Holds wallet provider credentials (Coinbase or Stripe Privy) |
Payment Manager | Orchestrates 402βsignβretry lifecycle |
Payment Connector | Links manager to a credential provider |
Payment Instrument | EMBEDDED_CRYPTO_WALLET β the wallet the agent spends from |
Payment Session | Bounded spend window: maxSpendAmount + expiry |
Agent β POST https://x402.fairseal.io/v1/notarize {schema, payload_hash, metadata}
Server β 402 PAYMENT-REQUIRED: {x402Version:2, amount:20000, network:"eip155:8453", ...}
AgentCore β signs EIP-3009 USDC authorization via Coinbase wallet
Agent β POST (retry) + X-PAYMENT header
Server β 201 { receipt_id: "nr_...", status: "pending_anchor", verify_url: "..." }
Zero custom code needed β AgentCore handles the 402βsignβretry loop. The agent receives the 201 JSON as if it were any ordinary HTTP API.
Critical distinction: "Request delivered/settled" β "Cryptographically verified"
These are two different states. Do not conflate them.
| State | When | What it means | What it does NOT mean |
|---|---|---|---|
| Delivered | HTTP 201 returned | Request received, accepted, queued into next Merkle batch | The receipt is on-chain. You have not verified anything cryptographically. |
| Pending anchor | status: "pending_anchor" |
Batch is building; anchors within ~2 min | Not yet verifiable offline |
| Anchored | status: "anchored" |
Merkle root is written to the MerkleAnchor contract on Base | FairSeal's servers haven't been audited; verify the proof yourself |
| Cryptographically verified | You run verifyAnchor() locally |
Merkle proof mathematically links your receipt to the on-chain root | Correctness of the decision β FairSeal proves provenance, not truth |
Rule: Never call a receipt "verified" based only on HTTP 201. A receipt proves: this exact hash existed at this time, and it was included in a Merkle batch anchored to Base block N.
// GET https://api.fairseal.io/v1/notarize/nr_d9583861b8fb6ab620ce64bec8c14b73
{
"receipt_id": "nr_d9583861b8fb6ab620ce64bec8c14b73",
"schema": "agent_decision",
"payload_hash": "49fe7a1ba52f86cd917b0732e319e8f4649e7456e51907fa996459e465549c4a",
"metadata": {
"agent_id": "omi-r22-walkthrough",
"decided_at": "2026-09-16T02:12:51.715Z",
"input_hash": "c6ba86dbb8b2ecb6671a572cab7f50bf9be23e3813d243f32e49e0e24cf285bf",
"decision_hash": "49fe7a1ba52f86cd917b0732e319e8f4649e7456e51907fa996459e465549c4a"
},
"created_at": "2026-09-16T02:12:51.970Z",
"status": "anchored",
"proof": { // β "proof", not "anchor"
"leaf_hash": "94daa4c3250b46f2...", // SHA256(receipt_id + "||" + payload_hash)
"leaf_formula": "SHA256(receipt_id + '||' + payload_hash) β hex-as-text",
"merkle_path": [], // empty = single-receipt batch
"merkle_root": "94daa4c3250b46f2...", // equals leaf_hash for single-item batch
"batch_id": "fairseal-notary-batch-...",
"anchor_tx": "0x63695b8dab26debe...",
"anchor_block": 51367733,
"anchor_chain_id": 8453,
"anchor_contract": "0xEE09671Bbc6B932BF1461CAff360bfDD9bEA55ca"
},
"verification": {
"leaf_recomputed_ok": true,
"merkle_path_valid": true,
"anchored_onchain": true,
"verified": true,
"how_to_verify_independently": [...]
}
}
Field name: Proof data is under receipt.proof, not receipt.anchor. The verification object is the server's self-report β run verifyAnchor() independently to confirm.
// verify.mjs β install: npm install @fairseal/verify@latest
import { verifyMerklePath, verifyAnchor } from "@fairseal/verify";
// Fetch the anchored receipt
const receipt = await fetch(
"https://api.fairseal.io/v1/notarize/nr_d9583861b8fb6ab620ce64bec8c14b73"
).then(r => r.json());
const proof = receipt.proof; // β always receipt.proof
// Step 1 (offline, no network): Merkle inclusion check
// Note: proof.merkle_path is a string[] of sibling hashes (pair-sorted).
// For single-receipt batches this is [] and verifyMerklePath returns true trivially.
const inclusion = verifyMerklePath(proof.leaf_hash, proof.merkle_path, proof.merkle_root);
console.log("Merkle inclusion:", inclusion ? "β PASS" : "β FAIL");
// Step 2 (one RPC call to a Base node YOU trust): on-chain anchor confirmation
// v0.4.0: pass the raw receipt directly β adapter auto-detects agent_decision schema
// REQUIRED: provide rpcUrl pointing to a Base mainnet RPC you trust
const anchor = await verifyAnchor(receipt, { rpcUrl: "https://mainnet.base.org" });
console.log("On-chain anchor:", anchor.valid ? "β PASS" : "β FAIL");
// anchor.onChain.blockNumber, anchor.onChain.merkleRoot β on-chain values for audit
The receipt includes a step-by-step guide. For a single-receipt batch (proof.merkle_path: []):
leaf = SHA256(receipt_id + "||" + payload_hash) β must match proof.leaf_hashmerkle_path is empty, merkle_root = leaf_hash (trivially)getBatchRoot(proof.batch_id) on contract 0xEE09671β¦ on Base β must equal 0x + proof.merkle_rootproof.anchor_tx calldata on basescan.orginput_hash is whatever you submitted/v1/pii/detect responses are NOT Merkle-anchored. Do not use @fairseal/verify on PII responses.| Endpoint | Price | Auth | Receipt type |
|---|---|---|---|
POST /v1/notarize | $0.02 USDC | API key (fsn_) OR x402 on Base | Merkle receipt (nr_β¦) β verifiable offline |
GET /v1/notarize/{id} | Free | None | Receipt lookup (includes Merkle proof once anchored) |
POST /v2/anchor | Per tier | API key (anck_) OR x402 | Anchor only |
GET /v2/anchor/{id} | Free | None | Anchor status |
POST /v1/pii/detect | $0.001 USDC | x402 | Scan result β not a Merkle receipt |
| Environment variable | Key prefix | What it unlocks |
|---|---|---|
FAIRSEAL_API_KEY | any | Universal fallback; used by MCP server if specific vars not set |
FAIRSEAL_NOTARY_API_KEY | fsn_ | Notarize service (/v1/notarize) |
FAIRSEAL_ANCHOR_API_KEY | anck_ | Anchor API v2 (/v2/anchor) |
No key? Email hello@fairseal.io.
| Host | Purpose |
|---|---|
api.fairseal.io | Key-authenticated calls; free GET receipt lookups |
x402.fairseal.io | x402 keyless payment path; Anchor API v2 |
verify.fairseal.io | Browser-based receipt verifier (no code required) |
{
"error": "invalid_path",
"self_correction": {
"step1": "Compute SHA-256 of your payload first",
"step2": "Submit: POST /v1/notarize with {schema, payload_hash, metadata}",
"step3": "Poll: GET /v1/notarize/{actual_receipt_id}" // β use the nr_... value, not a template
}
}
Cause: The agent used a literal {receipt_id} template string in the URL. Capture receipt.receipt_id from the POST 201 response and pass that value.
api.fairseal.io/v1/notarize + Authorization: Bearer fsn_β¦x402.fairseal.io/v1/notarize β no auth header, let @x402/fetch pay automaticallymaxTimeoutSeconds (300s) exceeded β regenerate payment and retryCheck the X-PAYMENT-RESPONSE header for the rejection reason.
Normal batch time is ~2 minutes. If persisting: contact hello@fairseal.io with your receipt_id.
{ rpcUrl: "https://mainnet.base.org" } as the second argument@fairseal/verify@latest (v0.4.0) β earlier versions require manual field remapping"status": "anchored" before calling verifyAnchorEnsure FAIRSEAL_API_KEY or FAIRSEAL_NOTARY_API_KEY is in the MCP server env block (not in your shell β MCP servers run in a subprocess). Key prefix: fsn_.
Ensure Content-Type: application/json is set and you're using the envelope format: {schema, payload_hash, metadata} β NOT flat fields.