Which AI Output Did the Human Actually Approve? Co-Signing AI Results on Base, $0.01 per Check

A

Artur Baranouski

Guest
TL;DR. BuildProof is an open-source registry on Base mainnet where several parties co-sign the SHA-256 of a file. One of the file types is an AI-output manifest: the hashes of the prompt, the input documents and the model's answer, bundled into one canonical JSON. The AI operator signs it, a human reviewer signs or rejects it, and anyone can later check a file against the chain for free, or let an agent pay $0.01 in USDC per lookup over x402. Code, contract and endpoints are live. It has no real users yet; everything on mainnet is demo data.

The problem: AI output has no paper trail​


I direct a construction and real-estate development company. Every month the same paperwork moves between the client, the general contractor, subcontractors and technical supervision: acceptance acts, invoices, supplementary agreements that change a quantity or a unit rate. It travels as PDFs over e-mail and messengers. Two arguments come up again and again: which version was signed, and does this act actually match the contract and its amendments.

Now add AI to that flow. A model summarizes an NDA, flags risks in a contract, checks the arithmetic in an act. A lawyer or an engineer reads the result and approves it. A few weeks later someone disputes it, and the questions are the old ones with a new twist:

  • "That's not what the model said." The output file was edited after review, by the operator, by the reviewer, or by whoever forwarded it. Nothing inside a .md file records what it said yesterday.
  • "That's not what I approved." The reviewer says they saw a different version. Without a shared record, it is one mailbox against another.
  • "The model saw a different contract." The answer may be correct for an input that was swapped. If the inputs are not pinned, the output is not either.
  • Agents trusting agents. An agent that is about to pay an invoice or accept a contract version on the strength of another agent's report has no way to ask whether a human ever signed off on that exact report.

None of this needs a sophisticated attacker. One changed number and two people with different files are enough.

The approach: pin the bytes, not the content​


The idea is deliberately boring. Hash the files locally. Put only the hash on-chain, together with the list of wallets that must sign. The record becomes ATTESTED only when every listed wallet has signed. A required signer can instead reject, and the rejection carries the SHA-256 of the reason (for example a review report), so the reason text stays off-chain but is pinned too. When an amendment to an attested document is attested, the original is marked SUPERSEDED.

For AI output, the "file" is a manifest. The CLI builds it from four things:

Code:
node cli/buildproof.mjs manifest --model gpt-4o \
  --prompt examples/legal/ai_review_prompt.txt \
  --input examples/legal/nda_mutual.pdf \
  --output examples/legal/ai_review_output.md \
  --created 2026-10-09 > examples/legal/ai_review_manifest.json

and the result in the repository is a single canonical JSON line (keys in fixed alphabetical order, no trailing newline, so the file hashes the same on every machine):

Code:
{"created":"2026-10-09","input_documents_sha256":["0xd4caea07…8b3a"],"model":"gpt-4o",
 "output_sha256":"0x92edd666…2d36","prompt_sha256":"0x0216b2a6…1c3f",
 "reviewed_by":"human reviewer (wallet signs below)","schema":"buildproof.ai-output/v1"}

(Hashes shortened here; the full file is examples/legal/ai_review_manifest.json.) The manifest is then registered with two signers, the AI operator and the human reviewer. This exact example is live on Base mainnet: attestation 0x87580bfb…26ad12e, registered in 0x38c7eef6…, signed by the AI operator's key offline (EIP-712) and relayed in 0x8662ee13…, then signed by the human reviewer in 0xf779a3e5…. Status: ATTESTED. Change one byte of the prompt, of any input, or of the answer, and the manifest hash changes, so the lookup returns NOT_FOUND.

Be precise about what this proves. It proves which exact bytes each wallet approved, and when. It does not prove that the named model produced the output: model is a string the operator writes. Proving model execution is a different problem (TEEs, zkML) and out of scope here.

Architecture​


BuildProof: what lives where


Four pieces:

  1. BuildProofRegistry (Solidity, 228 lines) on Base mainnet at 0xF818e4A95BBA02c822bCa8A0CFB50a2Ae4B8eE94. No owner, no constructor arguments, no upgrade path. Source verified on Sourcify (exact match). A record holds the hash, a document type, a project reference, an optional link to the document it amends, and up to 16 required signers.
  2. CLI (Node): hash, manifest, register, sign, sign-offline, relay, reject, status, verify.
  3. Verify page on GitHub Pages. It hashes the file in the browser with WebCrypto and reads the chain through a public RPC. The file never leaves the machine.
  4. Paid lookup API (Cloudflare Worker): GET /v1/attestation/{sha256} returns a verdict (ATTESTED, SUPERSEDED, PENDING, REJECTED, NOT_FOUND, NO_MATCHING_SIGNERS), the signers and the dates, for $0.01 USDC over x402. The same lookup code answers on the NEAR AI Agent Market and in the OpenServ x402 catalogue.

An honest footnote on decentralization: the registry is permissionless and readable for free through any RPC; the Worker is a convenience I run on Cloudflare. If I shut it down tomorrow, every record stays readable on Base.

The contract, in two fragments​


Signers who have no gas can still sign. They produce an EIP-712 signature over the attestation id and the document hash, and anyone relays it:

Code:
function signBySig(bytes32 id, address signer, bytes calldata signature) external {
    Attestation storage a = _att[id];
    if (a.status == Status.None) revert UnknownAttestation(id);
    bytes32 digest = signDigest(id);
    if (_recover(digest, signature) != signer) revert BadSignature();
    _sign(id, signer, true);
}

The domain separator includes block.chainid and the contract address, _recover rejects high-s (malleable) signatures and address(0), and the id embeds the document hash, so a signature cannot be replayed onto another document, contract or chain.

The second fragment came from a review. Two amendments to the same attested contract could both be pending; whichever completed last would silently overwrite the first. The fix is one line in _sign:

Code:
if (a.signedCount == a.signerCount) {
    a.status = Status.Attested;
    a.closedAt = uint64(block.timestamp);
    emit Attested(id, a.docHash);
    bytes32 old = a.supersedes;
    if (old != bytes32(0)) {
        if (supersededBy[old] != bytes32(0)) revert BadSupersedes(old);
        supersededBy[old] = id;
        emit Superseded(old, id);
    }
}

Anyone can register any hash with any signer list, so a stranger can front-run a registration with fake signers. That does not block the real record: the id is keccak256(docHash, docType, projectRef, supersedes, signers, msg.sender), so the fake one gets a different id. Verifiers pass the expected parties (?signers=0xA,0xB), and the verdict only counts records whose signer set matches exactly.

The Foundry suite has 21 tests, two of them fuzz tests, covering replay, malleability, griefing registrations, double signing and the supersession race.

Paying for a check: x402​


One AI output, two signatures, any number of checks


The interesting user of a verify endpoint is not a person with a browser. It is an agent in the middle of a task: "before I pay this invoice, was this act co-signed by both parties?" or "before I forward this AI review to the client, did a human sign it?" Agents do not fill in sign-up forms, so the API has no accounts and no keys. Payment is the authentication. The route config is small:

Code:
paymentMiddleware({
  [ROUTE]: {
    accepts: { scheme: "exact", price: cfg.PRICE, network: cfg.X402_NETWORK,
               payTo: cfg.PAY_TO, maxTimeoutSeconds: 120 },
    serviceName: "BuildProof",
  },
}, resourceServer)

An unpaid request gets 402 Payment Required with a PAYMENT-REQUIRED header. Decoded, the live one quotes scheme exact, network eip155:8453 (Base), amount 10000 (USDC has 6 decimals, so $0.01), asset 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. The buyer signs an EIP-3009 USDC authorization, a facilitator (PayAI, no API key) settles it, and the buyer retries with the payment header. The Worker never holds a private key and never sends a transaction. A malformed hash returns 400 before any price is quoted, and a failed chain read returns 502, which the middleware does not settle, so the buyer is not charged for my outage.

On the NEAR AI Agent Market the marketplace itself is the A2A server and takes the payment ($0.01 USDC on NEAR). My Worker receives assignments by webhook, checks an HMAC-SHA256 over "<timestamp>.<raw body>" with a 5-minute window, and a cron trigger polls every minute for missed ones. A brief without a hash is declined as rejected, so the caller is not charged. On OpenServ the listing is a paid workflow in the public x402-services catalogue; the platform calls the Worker as an external agent endpoint, with no LLM in the loop.

Check it yourself​


Everything below runs without an account.

Code:
git clone https://github.com/ttimesai-star/buildproof && cd buildproof && npm install

# A demo acceptance act registered on Base mainnet, two signers, one of them via a relayed EIP-712 signature
BP_RPC=https://mainnet.base.org node cli/buildproof.mjs verify examples/pdf/act_01.pdf
#   status ATTESTED, registered 2026-10-09T13:18:07Z, closed 2026-10-09T13:18:19Z, 2 signers signed

# The AI-output review manifest, co-signed by the AI operator and the human reviewer
BP_RPC=https://mainnet.base.org node cli/buildproof.mjs verify examples/legal/ai_review_manifest.json
#   status ATTESTED, type ai-output, registered 2026-10-10T00:45:03Z, closed 2026-10-10T00:45:13Z, 2 signers signed

# The x402 quote (expect HTTP 402 and a PAYMENT-REQUIRED header)
curl -i https://buildproof-x402.zbignevich.workers.dev/v1/attestation/$(node cli/buildproof.mjs hash examples/pdf/act_01.pdf)

# Free service info and OpenAPI spec
curl https://buildproof-x402.zbignevich.workers.dev/
curl https://buildproof-x402.zbignevich.workers.dev/openapi.json

Links:


The whole demo story (contract, amendment, a clean act, an act with planted errors that the client rejects with the hash of the check report) runs on a local anvil chain with bash scripts/demo.sh. CI runs it, plus the Foundry, Python and Node suites, on every push; the latest commit is green.

What it costs, what it is not​


Deploying the registry and running the mainnet demo cost 0.0000096 ETH in gas, about $0.024. Registering and co-signing the AI-output manifest (three transactions) cost 0.0000022 ETH, about half a cent. That is the economic argument for an L2: co-signing a document costs less than printing it.

The limits, stated plainly:

  • No users yet. Every record on mainnet is demo data with synthetic parties. No paid x402 call has been made on mainnet so far. The AI-output manifest flow is covered by tests on a local chain, and the example manifest is registered and co-signed on mainnet, but both signers are my own demo wallets, not an independent operator and reviewer.
  • A wallet signature is not a qualified electronic signature under eIDAS or ESIGN. BuildProof is an evidence layer next to whatever signature the law requires, not a replacement for it.
  • Hashes of short documents can be guessed. A one-page template with a few variable fields can be brute-forced from a dictionary of likely values. A real contract with dates, bank details and amounts is a much harder target, but salted hashes for low-entropy forms are still on the roadmap, not in the code.
  • The manifest names the model; it does not prove the model ran. It proves what was approved, by whom and when.
  • The paid API is a single Worker. The data is decentralized; my convenience layer is not. Anyone can run node api/server.mjs against the same registry.

What's next​


An Ethereum Attestation Service adapter, salted hashes for templates, and an agent-side helper that builds the manifest the moment the model answers, so pinning the output is a side effect of generating it. And the part that matters most: a first real counterparty who signs a real acceptance act this way. That decides whether this is a tool or a demo.

How I used AI​


The code, the tests and the draft of this article were produced by AI agents (mainly Claude by Anthropic, with a second-opinion review by Google's Jules agent), working on goals and constraints I set. The problem and the rules for checking acceptance acts come from my own work in construction. The agents proposed the architecture and wrote the code; every claim in this text was then verified against the repository and the live endpoints: the contract address and its Sourcify match, the mainnet attestation status via the CLI, the live 402 price quote, the test count, and the CI status. Where something has not been done, such as a paid mainnet call or real users, the article says so. The optional field extraction in the cross-checker can call an NVIDIA-hosted Nemotron model; in that path the model only extracts numbers, and deterministic Python decides every finding.

Artur Baranouski
 

Thread statistics

Created
Artur Baranouski,
Replies
0
Views
4
Back
Top