DEV Community

GitSerge-crypto
GitSerge-crypto

Posted on

Two signatures are better than one — bilateral provenance for AI agents

Two signatures are better than one — bilateral provenance for AI agents

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

A week later, someone asks the obvious question: who proved the agent wrote the report?

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

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

The binding hash

The mechanism is a single hash:

binding_hash = sha256(work_hash + sig_A + agent_pubkey)
Enter fullscreen mode Exit fullscreen mode
  • work_hash — SHA-256 of the agent's output (32 bytes)
  • sig_A — agent's Ed25519 signature over work_hash, NEP-413 standard (64 bytes)
  • agent_pubkey — agent's Ed25519 public key (32 bytes)

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

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

What actually changed

Almost nothing structurally — and that's the point.

v0x03 (ordinary) v0x04 (bilateral)
Version byte 0x03 0x04
payload_hash sha256(work_result) binding hash
Agent signature not required Ed25519 over work_hash
Size 239 bytes 239 bytes
Price $0.01 $0.01

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

The notary auto-selects the version: if the request includes agent_sig + agent_pubkey → v0x04. If not → v0x03. No explicit version parameter.

The agent signs first

Before anything reaches the notary, the agent signs its own work hash:

import hashlib, struct, base64
from nacl.signing import SigningKey

# Agent's Ed25519 key
agent_key = SigningKey(bytes(32))  # use your real key
agent_pubkey_hex = agent_key.verify_key.encode().hex()

# Agent produced this output
report = b"Q3 revenue projection: $2.4M based on agent analysis..."
work_hash = hashlib.sha256(report).digest()

# NEP-413: Tag(u32 LE) + Len(u32 LE) + message — raw, no pre-hash
nep413_buffer = struct.pack("<II", 2147484061, len(work_hash)) + work_hash
agent_sig = agent_key.sign(nep413_buffer).signature
agent_sig_b64 = base64.b64encode(agent_sig).decode()
Enter fullscreen mode Exit fullscreen mode

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

Notarize with the agent signature

curl -s https://api.aotrust.link/v1/notarize \
  -H "Content-Type: application/json" \
  -H "X-Payment: <base64-encoded EIP-3009 payload>" \
  -d '{
    "work_hash": "a1b2c3d4...sha256_of_output...",
    "agent_sig": "BASE64_AGENT_ED25519_SIGNATURE",
    "agent_pubkey": "HEX_AGENT_ED25519_PUBLIC_KEY"
  }'
Enter fullscreen mode Exit fullscreen mode

Inside the notary:

  1. Verifies sig_A against agent_pubkey — before payment, before anything else
  2. Verifies EIP-3009 payment on Base (~2s)
  3. Computes binding_hash = sha256(work_hash + sig_A + agent_pubkey)
  4. Builds 239-byte PDR — version 0x04, binding hash in payload_hash
  5. Signs the 175-byte payload with notary Ed25519 key
  6. Returns base64 PDR
{
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "notarized",
  "pdr_b64": "BQFCAUJ...239_bytes_base64..."
}
Enter fullscreen mode Exit fullscreen mode

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

Two independent verification paths

This is the real payoff. After notarization, you have two separate cryptographic proofs — each verifiable independently, each requiring a different public key.

Path 1: Notary signature (the PDR itself)

curl -s "https://api.aotrust.link/v1/pdr/verify/BQFCAUJ...your_pdr_b64..."
Enter fullscreen mode Exit fullscreen mode
{
  "valid": true,
  "version": 4,
  "binding_hash": true,
  "payload_hash": "b3c4d5e6...",
  "timestamp_utc": 1751700000,
  "payment_anchor_type": 5,
  "anchor": { "near_tx": "H4MaR5...", "confirmed": true }
}
Enter fullscreen mode Exit fullscreen mode

Or fully offline — the parser is a single Python file, zero dependencies:

curl -sO https://raw.githubusercontent.com/GitSerge-crypto/aotrust-skills/main/pdr_parser.py

python3 pdr_parser.py --pdr "BQFCAUJ..." \
  --pubkey "490f51f23b993eacaff54fc977d9a7689ab7d4ae91504dc6cbdeadb2dbf1f462"
Enter fullscreen mode Exit fullscreen mode
PDR v2.4 — Valid ✓
  version:           4 (bilateral)
  payload_hash:      b3c4d5e6... (binding hash)
  signature:         VALID (Ed25519/NEP-413)
Enter fullscreen mode Exit fullscreen mode

This proves: the notary signed the record, the payment was verified, the timestamp is attested, and the record is anchored on NEAR.

Path 2: Agent signature (stored in ledger)

The 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:

import struct, base64
from nacl.signing import VerifyKey

agent_sig_b64 = "BASE64_AGENT_SIG"      # from notarize response / DB
agent_pubkey_hex = "HEX_AGENT_PUBKEY"
work_hash_hex = "a1b2c3d4...your_work_hash..."

work_hash = bytes.fromhex(work_hash_hex)
nep413_buffer = struct.pack("<II", 2147484061, len(work_hash)) + work_hash

vk = VerifyKey(bytes.fromhex(agent_pubkey_hex))
vk.verify(nep413_buffer, base64.b64decode(agent_sig_b64))
# → no exception = valid
Enter fullscreen mode Exit fullscreen mode

This proves: the agent's Ed25519 key signed this specific work_hash. The agent cannot deny authorship.

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.

When to use bilateral

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

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

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

Backward compatibility

The parser handles both versions transparently:

from pdr_parser import parse_external_pdr

parsed = parse_external_pdr(base64.b64decode(pdr_b64))

if parsed.version == 0x03:
    payload = "work_hash"
elif parsed.version == 0x04:
    payload = "binding hash"
Enter fullscreen mode Exit fullscreen mode

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

Spec and parser

  • PDR spec (v2.3 + v2.4): pdr-spec.md
  • Standalone parser: pdr_parser.py — MIT, zero dependencies, handles v0x02/v0x03/v0x04

Try it

# Health check (no auth)
curl -s https://api.aotrust.link/health

# Get a quote — see the 402 response
curl -s https://api.aotrust.link/v1/notarize/quote \
  -H "Content-Type: application/json" \
  -d '{"work_hash":"0000000000000000000000000000000000000000000000000000000000000000"}'

# MCP discovery (4 tools: notary_quote, notary_notarize, notary_verify, notary_notarize_paid)
curl -s https://api.aotrust.link/.well-known/mcp.json

# Verify page — paste any PDR base64
# https://verify.aotrust.link
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)