Notarize Integration Guide

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).

15-Minute Walkthrough β€” 5 Steps
  1. Install @fairseal/mcp-server (npm v0.2.1)
  2. Set up x402 payment wallet (USDC on Base mainnet)
  3. Call notarize ($0.02 USDC) β€” get a receipt
  4. Fetch receipt once anchored (~2 min, free)
  5. Verify: verify.fairseal.io browser UI or @fairseal/verify offline

Prerequisites: Node.js β‰₯ 18, a funded Base mainnet wallet (β‰₯ $0.05 USDC + small ETH for gas). No API key needed for the x402 path.

Contents

  1. Part A β€” MCP Server Path (Claude Desktop)
  2. Part B β€” Direct x402 Path (no MCP, no signup)
  3. Part C β€” AWS Bedrock AgentCore Integration
  4. Part D β€” Receipt Anatomy & Verification
  5. Part E β€” Pricing & Keys Reference
  6. Part F β€” Troubleshooting

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.

Part A β€” MCP Server Path (Claude Desktop)

Easiest path if you use Claude Desktop. No x402 wallet setup required β€” uses API key billing instead.

Step 1 β€” Install & configure the MCP server

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.

Step 2 β€” Hash your input and decision (local, zero network)

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.

Step 3 β€” Notarize

"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.

Step 4 β€” Fetch the receipt once anchored (free, ~2 min wait)

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.

Part B β€” Direct x402 Path (no MCP, no signup)

For developers not using an MCP client. Pay $0.02 USDC on Base mainnet per call, no API key required. All 5 walkthrough steps below are validated against live endpoints (2026-09-16).

Step 2 β€” Set up wallet & x402 client

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.

Get USDC on Base mainnet

Install

mkdir fairseal-notarize && cd fairseal-notarize
npm init -y
npm install @x402/fetch @x402/evm viem @fairseal/verify

Step 3 β€” Notarize ($0.02 USDC via x402)

Full example (notarize.mjs) β€” validated 2026-09-16

// 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

Live 402 Challenge (verified 2026-09-16)

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
  }]
}

Step 4 β€” Poll until anchored (free, ~2 min)

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);

Step 5 β€” Verify (offline with @fairseal/verify v0.4.0)

// ─── 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.

Alternative: Browser verify (no code)

Open verify.fairseal.io, scroll to the "Notarize Receipt Lookup" section, paste your receipt_id (e.g. nr_d9583861b8fb6ab620ce64bec8c14b73) and click "Look Up".

Alternative: curl (quick sanity check)

# 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",

Request body schema (reference)

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.

Part C β€” AWS Bedrock AgentCore Integration

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:

  1. Reads the PAYMENT-REQUIRED header (machine-readable JSON, x402Version 2)
  2. Signs the payment via the configured Coinbase CDP or Stripe Privy embedded wallet
  3. Retries the original request with the payment proof in the X-PAYMENT header
  4. Receives HTTP 201 with the notarize receipt β€” no custom middleware needed

AgentCore Resource Model

ResourceRole
PaymentCredentialProviderHolds wallet provider credentials (Coinbase or Stripe Privy)
Payment ManagerOrchestrates 402→sign→retry lifecycle
Payment ConnectorLinks manager to a credential provider
Payment InstrumentEMBEDDED_CRYPTO_WALLET β€” the wallet the agent spends from
Payment SessionBounded spend window: maxSpendAmount + expiry

End-to-end flow from an AgentCore agent

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.

Part D β€” Receipt Anatomy, Verification, and the Delivered-vs-Verified Distinction

Critical distinction: "Request delivered/settled" β‰  "Cryptographically verified"
These are two different states. Do not conflate them.

StateWhenWhat it meansWhat 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.

Receipt anatomy (GET /v1/notarize/:id once anchored)

// 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.

Offline verification with @fairseal/verify

// 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

Manual verification (no library)

The receipt includes a step-by-step guide. For a single-receipt batch (proof.merkle_path: []):

  1. Compute leaf = SHA256(receipt_id + "||" + payload_hash) β€” must match proof.leaf_hash
  2. Since merkle_path is empty, merkle_root = leaf_hash (trivially)
  3. Call getBatchRoot(proof.batch_id) on contract 0xEE09671… on Base β€” must equal 0x + proof.merkle_root
  4. Or check proof.anchor_tx calldata on basescan.org

What FairSeal does NOT verify

Part E β€” Pricing & Keys Reference

EndpointPriceAuthReceipt type
POST /v1/notarize$0.02 USDCAPI key (fsn_) OR x402 on BaseMerkle receipt (nr_…) β€” verifiable offline
GET /v1/notarize/{id}FreeNoneReceipt lookup (includes Merkle proof once anchored)
POST /v2/anchorPer tierAPI key (anck_) OR x402Anchor only
GET /v2/anchor/{id}FreeNoneAnchor status
POST /v1/pii/detect$0.001 USDCx402Scan result β€” not a Merkle receipt

API Keys

Environment variableKey prefixWhat it unlocks
FAIRSEAL_API_KEYanyUniversal fallback; used by MCP server if specific vars not set
FAIRSEAL_NOTARY_API_KEYfsn_Notarize service (/v1/notarize)
FAIRSEAL_ANCHOR_API_KEYanck_Anchor API v2 (/v2/anchor)

No key? Email hello@fairseal.io.

Hosts

HostPurpose
api.fairseal.ioKey-authenticated calls; free GET receipt lookups
x402.fairseal.iox402 keyless payment path; Anchor API v2
verify.fairseal.ioBrowser-based receipt verifier (no code required)

Part F β€” Troubleshooting

400 "Path contains template placeholder"

{
  "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.

401/403 on /v1/notarize

402 "keeps returning" after payment

Check the X-PAYMENT-RESPONSE header for the rejection reason.

Receipt stays "pending_anchor" for > 5 minutes

Normal batch time is ~2 minutes. If persisting: contact hello@fairseal.io with your receipt_id.

verifyAnchor returns valid:false with empty errors

MCP key not recognized

Ensure 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_.

"params can't be blank"

Ensure Content-Type: application/json is set and you're using the envelope format: {schema, payload_hash, metadata} β€” NOT flat fields.