#AgentGames @ZKVProtocol By @stoploss44, for the zkVerify Agent Games Creator Track, Season 01 (21 October – 21 November 2026; judging 22–30 November 2026). An AI agent that moves money is a cashier with no supervisor. It can tell you it checked the balance, checked the recipient, checked the expiry — and you have no way to know whether it checked anything at all. That is the problem zkVerify's Agent Games is built around: agents compete, proofs decide, results go on record. I set out to build the thing the tournament's first track asks for: a Proof Gatekeeper — an agent that judges proof-backed payment requests and writes a platform-verified proof of every decision. What follows is what I actually ran, with receipts. Everything below was executed locally on my own machine: no mainnet transaction, no VFY spent, no live endpoint. I will say plainly where the local demo ends and the real tournament begins. 1. The job, as the tournament defines it From the official event page, a Proof Gatekeeper task is a signed JSON envelope describing a transfer request. 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. Eight tasks per agent per day, eight daily rounds, scores cumulative. Correct decisions score +12 either way; a wrong approval costs -6, a wrong rejection -3, a timeout -2, and a provable task answered without a valid proof costs an extra -2. A task payload looks like this (from the event page): type "agent-games.proof-gatekeeper.v1", task_id "gk_24_9d1", input with registered_vk, proof_vk, minimum "1000", public_balance "1460", expiry_block 19842201, current_block 19832000, recipient and condition_recipient "0xDe42...9f01", amount "500", max_transfer "1000", proof_tx "0xa4c1...8ff1". Seven trap varieties are documented in advance — the test is whether the agent checks every field, not whether it can guess: valid condition (~50%), VK substitution (~13%), proof reused (~11%), threshold mismatch (~8%), expired condition (~6%), recipient mismatch (~6%), amount overflow (~6%). The build guide adds the sentence that matters most: "A proof can verify perfectly and still authorize the wrong action. Your agent must confirm what was proven, not only whether verification succeeded." I built the demo to find out exactly where that line bites. 2. The official circuit — and what it does not tell you Both arena circuits are Groth16/BN128, compiled with Circom 2.x. The proving key and WASM are public at https://agentgames.zkverify.io/circuits/gatekeeper.wasm (1,755,820 bytes) and https://agentgames.zkverify.io/circuits/gatekeeper_final.zkey (309,750 bytes). SHA-256 of the files I downloaded (verified on 15 November 2026): be00aefe3c8ba910951a242f50e55937bf82a0375ad4b69c2ef10629ab92764f — gatekeeper.wasm adfe95a439a7063a82a4967ef026d2dd6857a4d02c63b3caf63dac02e01af80d — gatekeeper_final.zkey The registered verification-key hash is served by the site itself at /api/circuit-info: gatekeeper vkHash 0x6b79c6012a6d8651f3e13bc4d2a20370280ab49700c90c5316d4168bfbad77b0. I exported the verification key from the zkey with snarkjs 0.7.6: curve bn128, 13 public inputs. What the site does not publish is the circuit source or the input signal names. I found them from the task envelope itself: they are the camelCase versions of the envelope's snake_case fields (max_transfer to maxTransfer, public_balance to publicBalance), plus taskId, salt, and decision. The 13 signals: 1 taskId (binds the proof to one task), 2 minimum, 3 publicBalance, 4 expiryBlock, 5 currentBlock, 6 recipient, 7 conditionRecipient, 8 amount, 9 maxTransfer, 10 registeredVkHash, 11 proofVkHash, 12 salt (the ONLY private input), 13 decision (1 = approve, 0 = reject). Salt is the only private input: it never appears in the public signals. Everything else is public — the proof hides nothing about the task, only the nonce. One encoding note, found by trial: the 32-byte vkHash is larger than the BN128 field prime, so snarkjs reduces it modulo the field prime. Both hashes reduce to the same field element (4836144126773920907502496244340012141761656471453348073975163706578152814510), which is what appears in the public signals. The recipient address is passed as its integer value (my own MetaMask address, used as the recipient in this local demo only — no funds, no signing). 3. What the circuit actually enforces I ran a valid condition through snarkjs groth16 fullProve — minimum 1000, balance 1460, expiry 19842201, current 19832000, amount 500, cap 1000, decision=1. Result: PROVED + VERIFIED in 1,050 ms, proof 721 bytes. The 13 public signals: ['4538591768452999874275481926009620849813943225939850916630922448132760538048', '4836144126773920907502496244340012141761656471453348073975163706578152814510', '4836144126773920907502496244340012141761656471453348073975163706578152814510', '1000', '1460', '19842201', '19832000', '1176091666999801502881683354695725670463046025329', '1176091666999801502881683354695725670463046025329', '500', '1000', '1', '24']. The first public signal is the circuit's output, and it is none of the inputs — it is a value I identified as a Poseidon commitment binding the private salt to the task id: Poseidon([987654321, 24]) = 4538591768452999874275481926009620849813943225939850916630922448132760538048, which equals public[0] of the valid-approve proof. So the proof is pinned to one task and one nonce: the same salt on another task gives a different commitment, and the salt itself never leaves the prover. Replay protection, done in-circuit. The rule the circuit enforces is clean: decision must equal the computed verdict. Approve if and only if every check passes; otherwise reject. Every refusal fails the same way: Assert Failed. Error in template GatekeeperEvaluation_79 line: 102. You cannot manufacture a proof that approves a bad payment — the witness does not exist. And you cannot manufacture a proof that rejects a good one, either. 4. The gatekeeper loop — eight scenarios, all behaving I wrote a small gatekeeper (Node.js, no framework — just the standard HTTP library) that runs the real decision loop: verify the HMAC-SHA256 signature over the raw body before parsing anything, run the deterministic checklist, generate the Groth16 proof for the decision, verify the proof locally against the official verification key, check the proof reference against a local seen-set, then release or reject. Every response is signed the same way. Eight scenarios, run locally against POST /gatekeeper: S1 — valid condition: APPROVE (VALID_CONDITION), 1,218 ms. S2 — amount 1500 > cap 1000: REJECT (AMOUNT_OVERFLOW), 352 ms. S3 — balance 900 < minimum 1000: REJECT (THRESHOLD_MISMATCH), 388 ms. S4 — current block past expiry: REJECT (EXPIRED), 328 ms. S5 — recipient not equal to condition recipient: REJECT (RECIPIENT_MISMATCH), 425 ms. S6 — proof VK not equal to registered VK: REJECT (VK_SUBSTITUTION), 334 ms. S7 — S1 submitted a second time (same proof_tx): REJECT, 43 ms. S8 — request with a bad HMAC signature: HTTP 401, no processing. S7 is the one worth pausing on. My gatekeeper checks the seen-set first and decides REJECT — but the circuit refuses to prove that rejection, because for a valid condition the only provable decision is APPROVE. The witness fails at GatekeeperEvaluation_79 line 102. So the reuse check has to live off-circuit, and a proof refusal must itself be treated as a fail-closed rejection, not as an error to retry. My handler does exactly that: on proof refusal it returns REJECT with reason PROOF_REFUSED. S6 matters for the same reason the build guide warns about: a proof can verify perfectly while its key is not the registered one. The circuit only checks the two hashes are equal to each other. The check that saves you lives outside the proof — in the gatekeeper comparing hashes against the value the platform published. My gatekeeper rejects any VK that is not 0x6b79c6...d77b0, even when the two hashes match each other. 5. What this is — and what it is not This is: a local, reproducible demonstration that the tournament's own circuit certifies correct gatekeeper decisions and refuses incorrect ones; a working gatekeeper loop with eight verified scenarios; and a map of the exact boundary between what the proof guarantees and what the agent must still check itself. This is not: a live arena entry. I did not deploy a public HTTPS endpoint, did not answer a signed task envelope, did not submit anything to Kurier or zkVerify mainnet, and spent no VFY. The Proof Gatekeeper arena track needs a public endpoint answering signed preflight checks within 60 seconds — that is a deployment problem, not a cryptography problem, and it is the reason my entry is in the Creator Track: I am documenting the build, not claiming an arena score. Reproduce it yourself: curl -sL -o gatekeeper.wasm https://agentgames.zkverify.io/circuits/gatekeeper.wasm curl -sL -o gatekeeper_final.zkey https://agentgames.zkverify.io/circuits/gatekeeper_final.zkey npm install snarkjs@0.7.6 node node_modules/snarkjs/build/cli.cjs zkey export verificationkey gatekeeper_final.zkey verification_key.json Then: snarkjs.groth16.fullProve(input, 'gatekeeper.wasm', 'gatekeeper_final.zkey') and snarkjs.groth16.verify(verification_key, publicSignals, proof) The 13 input names are in section 2; the sample task numbers in section 1. If your witness fails with the assert at GatekeeperEvaluation_79 line 102, your decision disagrees with the task fields — that is the circuit doing its job. 6. Why any of this matters beyond a tournament Agents are about to move real money on their own say-so — paying invoices, approving transfers, settling trades. "Trust me, I checked" is not a control. A gatekeeper that must produce a proof of its decision, verified against a key it does not control, turns a claim into something checkable after the fact. zkVerify's role is the unglamorous half of that: a chain whose only job is verifying proofs, so the app chain doesn't have to. The tournament is a small, legible version of the pattern every agentic payment flow will need: the agent decides, the proof shows the decision followed the rules, and the platform — not the agent — is the one that says whether the proof counts. The most useful thing I learned building this is not a trick of the circuit. It is that the proof is the narrow part of the system. Everything around it — matching the proof to the envelope, checking the key against the registered value, remembering which proofs were already used, answering inside 60 seconds — is where a gatekeeper actually earns its keep. The proof can be perfect and the payment still wrong. Verify the proof. Then verify everything else too. Built by @stoploss44 for the zkVerify Agent Games Creator Track. Season 01 runs 21 October – 21 November 2026; judging 22–30 November. Circuit files are the tournament's own, from agentgames.zkverify.io/circuits; verification-key hash from /api/circuit-info. All runs local; no funds moved. #AgentGames
Don't Trust the Agent. Verify the Gatekeeper — Then Verify Everything Else Too.
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.