{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreih3hfu37kpqvkjfxd574pbnlwdmoabrvi5b6zgez6dwdnakjqfxie",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpv4dvvx75y2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiavdkuvh3g7poxgc6furc3tfdzbhzl2nj3ear3k75wmernblf43bi"
},
"mimeType": "image/webp",
"size": 65268
},
"path": "/gitsergecrypto/two-signatures-are-better-than-one-bilateral-provenance-for-ai-agents-3668",
"publishedAt": "2026-07-05T07:44:52.000Z",
"site": "https://dev.to",
"tags": [
"web3",
"base",
"ai",
"cryptography",
"pdr-spec.md",
"pdr_parser.py",
"api.aotrust.link",
"docs.aotrust.link",
"api.aotrust.link/mcp",
"github.com/GitSerge-crypto/aotrust-skills"
],
"textContent": "# Two signatures are better than one — bilateral provenance for AI agents\n\nAn AI agent produces a financial report. You notarize it — a 239-byte cryptographic record, signed by an independent notary, anchored on NEAR. The record proves: _this hash existed at this timestamp, and $0.01 USDC was paid for the attestation._\n\nA week later, someone asks the obvious question: **who proved the agent wrote the report?**\n\nNobody. The notary signed a hash the client submitted. Anyone could have submitted that hash. The PDR proves the hash existed — not that the agent authored the content behind it.\n\nThis is the gap **Bilateral Signature (v0x04)** closes. The agent signs its own work hash with its Ed25519 key. That signature gets fused into the PDR. Now the record carries two independent signatures — agent and notary — and neither party can repudiate.\n\n## The binding hash\n\nThe mechanism is a single hash:\n\n\n\n binding_hash = sha256(work_hash + sig_A + agent_pubkey)\n\n\n * `work_hash` — SHA-256 of the agent's output (32 bytes)\n * `sig_A` — agent's Ed25519 signature over `work_hash`, NEP-413 standard (64 bytes)\n * `agent_pubkey` — agent's Ed25519 public key (32 bytes)\n\n\n\nThis binding hash replaces `work_hash` in the PDR's `payload_hash` field. The notary then signs the full 175-byte payload — which already contains the binding hash. So the notary's signature covers a record that embeds the agent's signature inside it.\n\nTo forge a v0x04 PDR, you need **two** private keys: the notary's Ed25519 seed and the agent's Ed25519 key. With the ordinary v0x03, you only need the notary's. Bilateral doubles the compromise requirement.\n\n## What actually changed\n\nAlmost nothing structurally — and that's the point.\n\n| v0x03 (ordinary) | v0x04 (bilateral)\n---|---|---\nVersion byte | `0x03` | `0x04`\n`payload_hash` | `sha256(work_result)` | binding hash\nAgent signature | not required | Ed25519 over work_hash\nSize | 239 bytes | 239 bytes\nPrice | $0.01 | $0.01\n\nSame binary layout. Same 239 bytes. Same NEP-413 notary signature. The parser handles both by checking the version byte. All 9 existing mainnet PDRs (v0x03) remain valid — no migration, no fork.\n\nThe notary auto-selects the version: if the request includes `agent_sig` + `agent_pubkey` → v0x04. If not → v0x03. No explicit version parameter.\n\n## The agent signs first\n\nBefore anything reaches the notary, the agent signs its own work hash:\n\n\n\n import hashlib, struct, base64\n from nacl.signing import SigningKey\n\n # Agent's Ed25519 key\n agent_key = SigningKey(bytes(32)) # use your real key\n agent_pubkey_hex = agent_key.verify_key.encode().hex()\n\n # Agent produced this output\n report = b\"Q3 revenue projection: $2.4M based on agent analysis...\"\n work_hash = hashlib.sha256(report).digest()\n\n # NEP-413: Tag(u32 LE) + Len(u32 LE) + message — raw, no pre-hash\n nep413_buffer = struct.pack(\"<II\", 2147484061, len(work_hash)) + work_hash\n agent_sig = agent_key.sign(nep413_buffer).signature\n agent_sig_b64 = base64.b64encode(agent_sig).decode()\n\n\nNEP-413 signs the **raw** buffer — no SHA-256 pre-hash. The signature covers `Tag + Len + work_hash_bytes` directly. Same standard NEAR uses for transaction signing.\n\n## Notarize with the agent signature\n\n\n curl -s https://api.aotrust.link/v1/notarize \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Payment: <base64-encoded EIP-3009 payload>\" \\\n -d '{\n \"work_hash\": \"a1b2c3d4...sha256_of_output...\",\n \"agent_sig\": \"BASE64_AGENT_ED25519_SIGNATURE\",\n \"agent_pubkey\": \"HEX_AGENT_ED25519_PUBLIC_KEY\"\n }'\n\n\nInside the notary:\n\n 1. **Verifies`sig_A` against `agent_pubkey`** — before payment, before anything else\n 2. Verifies EIP-3009 payment on Base (~2s)\n 3. Computes `binding_hash = sha256(work_hash + sig_A + agent_pubkey)`\n 4. Builds 239-byte PDR — version `0x04`, binding hash in `payload_hash`\n 5. Signs the 175-byte payload with notary Ed25519 key\n 6. Returns base64 PDR\n\n\n\n\n {\n \"job_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"status\": \"notarized\",\n \"pdr_b64\": \"BQFCAUJ...239_bytes_base64...\"\n }\n\n\nThe PDR starts with `BQ` — base64 for `0x04 0x01` (version=bilateral, scheme=Ed25519). An ordinary PDR starts with `Aw` (`0x03 0x01`). You can tell which version at a glance.\n\n## Two independent verification paths\n\nThis is the real payoff. After notarization, you have **two separate cryptographic proofs** — each verifiable independently, each requiring a different public key.\n\n### Path 1: Notary signature (the PDR itself)\n\n\n curl -s \"https://api.aotrust.link/v1/pdr/verify/BQFCAUJ...your_pdr_b64...\"\n\n\n\n {\n \"valid\": true,\n \"version\": 4,\n \"binding_hash\": true,\n \"payload_hash\": \"b3c4d5e6...\",\n \"timestamp_utc\": 1751700000,\n \"payment_anchor_type\": 5,\n \"anchor\": { \"near_tx\": \"H4MaR5...\", \"confirmed\": true }\n }\n\n\nOr fully offline — the parser is a single Python file, zero dependencies:\n\n\n\n curl -sO https://raw.githubusercontent.com/GitSerge-crypto/aotrust-skills/main/pdr_parser.py\n\n python3 pdr_parser.py --pdr \"BQFCAUJ...\" \\\n --pubkey \"490f51f23b993eacaff54fc977d9a7689ab7d4ae91504dc6cbdeadb2dbf1f462\"\n\n\n\n PDR v2.4 — Valid ✓\n version: 4 (bilateral)\n payload_hash: b3c4d5e6... (binding hash)\n signature: VALID (Ed25519/NEP-413)\n\n\nThis proves: the notary signed the record, the payment was verified, the timestamp is attested, and the record is anchored on NEAR.\n\n### Path 2: Agent signature (stored in ledger)\n\nThe agent's `sig_A` isn't in the PDR binary — it's in the notary ledger (the PDR contains the binding hash, not the raw signature). To verify agent authorship:\n\n\n\n import struct, base64\n from nacl.signing import VerifyKey\n\n agent_sig_b64 = \"BASE64_AGENT_SIG\" # from notarize response / DB\n agent_pubkey_hex = \"HEX_AGENT_PUBKEY\"\n work_hash_hex = \"a1b2c3d4...your_work_hash...\"\n\n work_hash = bytes.fromhex(work_hash_hex)\n nep413_buffer = struct.pack(\"<II\", 2147484061, len(work_hash)) + work_hash\n\n vk = VerifyKey(bytes.fromhex(agent_pubkey_hex))\n vk.verify(nep413_buffer, base64.b64decode(agent_sig_b64))\n # → no exception = valid\n\n\nThis proves: the agent's Ed25519 key signed this specific `work_hash`. The agent cannot deny authorship.\n\n**Two signatures. Two keys. Two independent proofs.** Forged by compromising either party alone? No. The binding hash ties them together — the notary's signature covers a payload that contains the agent's signature fused into the hash.\n\n## When to use bilateral\n\nIf the agent has an Ed25519 key — use v0x04. The cost is identical ($0.01), the size is identical (239 bytes), and you get non-repudiation for free.\n\nIf the agent has no key (third-party notarizing someone else's work, bulk timestamping) — v0x03 is fine. It's proof-of-existence without identity binding.\n\nThe interesting case is **multi-agent pipelines**. Agent A produces output, agent B refines it, agent C compiles the final report. Each stage can be notarized with a bilateral PDR signed by that stage's agent key. You get a chain of custody where every link has its own Ed25519 signature bound into the record — and the notary attests each link independently.\n\n## Backward compatibility\n\nThe parser handles both versions transparently:\n\n\n\n from pdr_parser import parse_external_pdr\n\n parsed = parse_external_pdr(base64.b64decode(pdr_b64))\n\n if parsed.version == 0x03:\n payload = \"work_hash\"\n elif parsed.version == 0x04:\n payload = \"binding hash\"\n\n\n9 existing mainnet PDRs are v0x03. They stay v0x03. New bilateral PDRs are v0x04. No migration, no re-issuance. The version byte is the only thing the parser branches on.\n\n## Spec and parser\n\n * **PDR spec (v2.3 + v2.4):** pdr-spec.md\n * **Standalone parser:** pdr_parser.py — MIT, zero dependencies, handles v0x02/v0x03/v0x04\n\n\n\n## Try it\n\n\n # Health check (no auth)\n curl -s https://api.aotrust.link/health\n\n # Get a quote — see the 402 response\n curl -s https://api.aotrust.link/v1/notarize/quote \\\n -H \"Content-Type: application/json\" \\\n -d '{\"work_hash\":\"0000000000000000000000000000000000000000000000000000000000000000\"}'\n\n # MCP discovery (4 tools: notary_quote, notary_notarize, notary_verify, notary_notarize_paid)\n curl -s https://api.aotrust.link/.well-known/mcp.json\n\n # Verify page — paste any PDR base64\n # https://verify.aotrust.link\n\n\n * **API:** api.aotrust.link\n * **Docs:** docs.aotrust.link\n * **MCP:** api.aotrust.link/mcp\n * **Source:** github.com/GitSerge-crypto/aotrust-skills\n\n\n\n_The service is live on mainnet. Bilateral PDRs (v0x04) are supported as of July 2026 — same price, same size, same verification flow. The agent proves it wrote the work. The notary proves it was notarized. Both are math._",
"title": "Two signatures are better than one — bilateral provenance for AI agents"
}