Trust Nothing, Prove Everything: Building a Proof Gatekeeper for zkVerify Agent Games
Written for the zkVerify Agent Games Creator Track (#AgentGames). Season 01 runs 21 October – 21 November 2026, with 400,000 VFY in prizes across four tracks. This article documents work I actually did — and is honest ab
Written for the zkVerify Agent Games Creator Track (#AgentGames). Season 01 runs 21 October – 21 November 2026, with 400,000 VFY in prizes across four tracks. This article documents work I actually did — and is honest about what I didn't.
My agent is noven.ai. It runs on a Binance Agentic Wallet, it signs transactions, mints NFTs, and holds assets on my behalf. Every day I trust it with real decisions about real money.
That is exactly the problem.
An agent can tell you it checked the balance, verified the recipient, and approved a payment for the right reasons. But a claim is not evidence. If the agent is wrong — or lying, or hacked — its word is worth nothing. The crypto world learned this about money a long time ago: don't trust, verify. zkVerify's Agent Games applies the same principle to agents themselves.
So I had my agent build the thing the tournament's first track asks for: a Proof Gatekeeper — an agent that judges proof-backed payment requests and proves every decision with a zero-knowledge proof. Then I tested it, locally, against the tournament's own circuit. This is what happened.
The job: a bouncer with a 60-second clock
From the official field guide, a Proof Gatekeeper task is a signed JSON envelope describing a transfer request. The platform POSTs it to your agent's public endpoint. The agent has 60 seconds to answer with one deterministic decision — APPROVE or REJECT — plus a Groth16 proof of that decision, which the platform re-verifies against its own registered verification key.
The scoring is deliberately asymmetric: a correct, verified decision is +12. A wrong REJECT is −3. A wrong APPROVE — letting a bad payment through — is −6. A timeout or a missing proof is −2. The message is clear: being careless is worse than being cautious, and being slow is its own failure.
The tasks are not all honest. The field guide documents seven trap varieties: a valid condition (about half the tasks), plus VK substitution, threshold mismatch, proof reuse, expired condition, recipient mismatch, and amount overflow. A gatekeeper that only checks "is this proof valid?" will approve traps all day — because a proof can be perfectly valid and still authorize the wrong thing. The guide says it plainly: verification is necessary, not sufficient.
What I built
I built the gatekeeper as a single Node.js file with no framework, running locally. It does five things, in order:
1. Verify the signature before parsing anything. Every incoming request carries an x-agent-games-signature header — an HMAC-SHA256 over the raw request body. The server checks it against the raw bytes first, with a constant-time comparison. A bad signature gets a 401 and no work is done at all:
// 1. Verify HMAC against the RAW body before parsing anything.
const sig = req.headers["x-agent-games-signature"];
if (!verifySignature(rawBody, sig)) {
res.writeHead(401, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "bad_signature" }));
return;
}
2. Decide, fail closed. The decision logic walks the trap encyclopedia in order: malformed task, proof VK different from the registered VK, proof already used, balance below the minimum, expired condition, recipient mismatch, amount above the cap. Any failure means REJECT. Only a task that survives every check gets APPROVE. If anything is uncertain, the answer is REJECT — never approve by default.
3. Prove the decision. For every provable decision, the server generates a Groth16 proof with the tournament's own gatekeeper circuit — gatekeeper.wasm and gatekeeper_final.zkey, downloaded from the official site — using snarkjs 0.7.6. APPROVE becomes decision 1 in the circuit; REJECT becomes decision 0. If proof generation fails on an APPROVE, the server fails closed to REJECT instead of returning an approval it cannot prove. (Reused-proof tasks get no proof at all, per the rules: there is nothing new to prove.)
4. Stay idempotent. The platform may retry a task after a network failure, so every response is cached by task ID. A repeated task ID returns the byte-identical cached body.
5. Sign the response. Every response carries its own x-agent-games-signature over the raw response body, and the server never hangs: an internal error returns a 500 rather than a timeout.
One honest note: the HMAC secret in this build is a local test secret. The real platform secret is generated at registration, and this build never touches it.
The proof, verified
I ran the full loop locally on the event page's own sample scenario: public balance 1460 against a minimum of 1000, an amount of 500 against a cap of 1000, a recipient that matches the condition, an expiry block of 8,430,000 ahead of the current block 8,420,000, and a proof VK equal to the registered VK. The gatekeeper returned APPROVE — and a Groth16 proof that verified against the verification key exported from the official proving key.
Real output from the test run:
a) valid task -> APPROVE — status=200 decision=APPROVE reason=VALID_CONDITION totalMs=863 proveMs=843
a) Groth16 proof verifies against exported vk — verify=true
b) fullProve(decision=1) on trap inputs FAILS — Error: Assert Failed. Error in template GatekeeperEvaluation_79 line: 102
Proof generation took 843 ms — against a 60,000 ms deadline. The verification was confirmed twice: snarkjs.groth16.verify returned true in-process, and an independent CLI run printed OK! with exit code 0. I also decoded the proof's 13 public signals and confirmed they encode what they should: the registered verification-key hash, the recipient address, and decision 1.
In total, 12 out of 12 local checks passed: the valid approval, all six trap rejections with the correct reason codes, the bad-HMAC 401, the byte-identical replay, the reused-proof rejection without a proof, and the malformed-task rejection.
The check I found most satisfying was the one designed to fail. When I fed the circuit trap inputs and asked it to prove decision 1 — approve anyway — it refused: Assert Failed at line 102 of the gatekeeper template. The circuit will not let you prove an approval that isn't true. That is the whole point of the exercise, and it works.
The honest miss: the verification-key hash
Not everything matched. The site publishes a registered verification-key hash for the gatekeeper circuit (0x6b79c6…d77b0). I exported the verification key from the official proving key and tried to reproduce that hash — 12 serialization variants: keccak256 and SHA-256 over compact, indented, and sorted JSON, curve-point fields only, flattened commitment data, wrapped and unwrapped forms. None of them matched.
The most likely explanation is that zkVerify (or its Kurier service) computes the registered hash over its own canonical serialization of the verification key, which I did not reproduce. This does not affect the local build: proofs generated with the downloaded circuit files verify against the exported key, so the artifacts are self-consistent. But I am not going to claim the local hash matches the registered one. It was checked, and it did not match with the methods I tried. If I take this build to the arena, resolving that gap is the first thing to do — because the platform re-verifies proofs against its registered key, not mine.
What I did not do
This article documents a build, not an arena score. To be precise about scope:
-
No public endpoint. The server ran only on
127.0.0.1:8787for these tests and is stopped now. No tunnel, no hosting account. - No arena entry. No practice task, no scored round, no points.
- No submission to Kurier or zkVerify mainnet. No verification-key registration, no proof reference, no on-chain transaction of any kind.
- No funds moved. No VFY spent, no gas paid. Total cost: $0.
I am entering the Creator Track with this record because the work is real and the numbers are checkable — not because I am claiming a leaderboard position I don't have.
Why this matters beyond the tournament
My agent moves money for me. Every agent that does that — approving payments, signing contracts, managing positions — is a trust problem waiting to happen. Today we mostly solve it with reputation: the agent behaved well yesterday, so we assume it behaves well today. That is not verification. That is hope.
The gatekeeper pattern is a better answer, and it has two halves. The first half is the proof: a decision accompanied by cryptographic evidence that anyone can check. The second half is the part the field guide keeps repeating — verification is necessary, not sufficient. A valid proof of the wrong statement is still wrong. The agent has to check what was proven: the right key, the right threshold, the right recipient, the right expiry, a proof that hasn't been spent already. The cryptography enforces the rules; the agent still has to know the rules.
That is what I built here, in miniature: a gatekeeper that checks first, proves second, and refuses to approve what it cannot prove. The proof took 843 milliseconds. The lesson took a little longer — but it's the one I'll keep.
AgentGames
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.