An onchain agent that verifies its own execution against the chain.
Built for the KeeperHub Agents Onchain Hackathon.
cd agent && pnpm install && pnpm demoOn 2026-07-27 this agent executed a transaction through KeeperHub. It succeeded: Sepolia block 11362630, status 1, gas sponsored, zero retries.
Our integration reported failure. Thirty status polls, thirty 404s, then
no transaction hash yet — while the transaction sat mined on the chain.
Three traps, all in one execution:
- the response field is
executionId, notid - direct executions and workflow executions use different status endpoints; the wrong one 404s silently
- the transaction is a meta-transaction —
fromis a relayer,tois a forwarder, and the agent's wallet appears only inside the calldata
None of these are KeeperHub failing. All of them produce an agent that is confidently wrong about what it just did.
An agent can execute onchain far more reliably than it can tell you whether it did. Reckoner closes that gap.
KeeperHub's submission layer is trustworthy. The gap is on the integration side.
We tried to disprove that and failed. Attempting to induce a false positive — a
USDC transferFrom with no allowance — KeeperHub simulated it, returned
ERC20: transfer amount exceeds allowance, and refused to broadcast. No
transaction, no hash, nothing mined.
So false-positive and phantom remain defence-in-depth for execution layers
that don't simulate, and are labelled SYNTHETIC in the demo. Everything
claimed as live is live.
Every chain read goes to a public RPC with no relationship to KeeperHub. Asking the system that just told you an execution succeeded whether it succeeded is not verification — it is repetition.
| Verdict | Meaning | Evidence |
|---|---|---|
confirmed |
Reported success, chain agrees, attributable | live |
false-negative |
Reported failure — the chain says it succeeded | live |
unverifiable |
No hash to check. Reported honestly, not assumed good | live |
unattributable |
Succeeded onchain, but nothing ties it to this agent | synthetic |
phantom |
Reported success — no onchain record exists, either because the hash is unknown to the chain or because no transaction was ever produced | synthetic |
false-positive |
Reported success — the chain says it reverted | synthetic |
pending |
Not mined yet. Early, not wrong | synthetic |
Eienel raised the inverse of our own case
(#1): on KeeperHub an action
node missing actionType is accepted at creation, silently skipped at
execution, and the run reports status: success with the node absent from
executionTrace. A green run for an automation that never ran. The incentive is
backwards, too — adding actionType makes creation reject you for a missing
abi, so the less complete definition is the one that passes and does
nothing.
Reckoner did not catch it. Any record without a transaction hash returned
unverifiable regardless of what had been claimed, and unverifiable is not a
divergence — so a run claiming success while never touching the chain rendered
as a grey "undetermined", excluded from the divergence rate and
indistinguishable from an honest "nothing to check".
It is a phantom now: reported success, no transaction ever produced. A
reported failure with no hash is still unverifiable, because that is
coherent — nothing was claimed and nothing happened.
KeeperHub sponsors gas by relaying. A relayer pays, a forwarder contract is the
to, and the agent's wallet appears only as a calldata parameter. The obvious
check — is receipt.from my agent? — answers no for a transaction the
agent absolutely caused.
A verifier that gets this wrong marks every sponsored execution
unattributable, which is worse than having no verifier. Reckoner attributes
three ways: direct-sender, relayed-calldata, relayed-logs. Every sponsored
execution in the record resolves via calldata.
| What | Tx | Verdict |
|---|---|---|
| First execution — dust self-transfer | 0x34d520a8…4428 |
false-negative caught — succeeded in block 11362630 |
| Control — dust self-transfer | 0x3e5aec78…2718 |
confirmed, block 11363031, 47,693 gas |
| Induced failure — USDC transferFrom, no allowance | — | refused before broadcast |
Full record in tx-log.md.
69 executions in total, 58 with a transaction hash and 11 refused before
broadcast. A verification tool means nothing at n=1: the signal is the
divergence rate (1.7% of 58 verifiable), the verdict mix, and the
attribution mix across a population. Gas is sponsored, so volume costs nothing
but time — DRY_RUN=0 pnpm populate --execute --count 45 produces more.
The model proposes; policy.ts disposes. It is short
enough to read before trusting the agent: address validation, amount validation,
per-action ceiling. Nothing reaches the chain without passing it.
Two reasoning layers sit behind one enforcement point — the Anthropic API, and Claude Code headless (which costs no API credits). Claude Code has the KeeperHub MCP server configured, so a careless invocation could call execution tools directly and skip the gate; the CLI brain is therefore restricted to read-only tools and must return a structured proposal. Reasoning is delegated; authority is not.
| Path | What |
|---|---|
| agent/ | The agent. See agent/README.md for internals. |
| agent/src/reconcile/ | Ground truth and verdict logic. |
| web/ | The dashboard. Next.js 16, reads the same record the agent writes. |
| tx-log.md | Every execution, with explorer links. |
| friction-log.md | Onboarding friction, logged as it happened. |
| BOUNTY.md | Eight findings, submitted for the onboarding bounty. |
| docs/DEMO.md | Shot list for the demo video. |
A dark operator UI — the scene is an engineer opening this after an unattended run, usually because something looks off. Its signature element is the execution field: one column per execution, in submission order, each column's bars drawn from that execution's transaction hash, hex digit by hex digit. Verdict sets how it reads — confirmed full-height in teal, undetermined at 40% height in indigo so a gap in knowledge is drawn as a gap, and the one execution where report and chain disagree in amber.
Colour encodes verdict and nothing else. Navigation, links and headings stay neutral, so a hue on this page always means a state — and amber is reserved for divergence, the only state allowed to fill a region. A palette where the alarm colour also decorates the header cannot raise an alarm, and a Playwright spec enforces it.
The palette is computed rather than eyeballed: worst-pair ΔE 14.8 under simulated protanopia and 21.0 under normal vision, against a hard floor of 15. The first cut scored 13.0 and had to be re-stepped.
cd web && pnpm install && pnpm dev # http://localhost:3000
pnpm exec playwright test # 19 specs: axe, layout, palette, reconciliationMeasured on the production build, throttled mobile (4× CPU, Slow 4G): LCP
0.9–1.2 s, CLS 0.00, Lighthouse accessibility 100 / best-practices 100 /
SEO 100 — 52 audits passed, 0 failed. Runtime dependencies are next,
react, react-dom and nothing else.
cd agent
pnpm install
cp .env.example .env # add your kh_ organization key
pnpm verify # pre-flight: key, wallet funding, chain, brain
pnpm demo # reconciliations against live chain data
pnpm loop:once # one reckoning cycleDRY_RUN=1 and BRAIN=cli are the defaults: no transactions submitted, no API
credits spent.
