Search projects, hackathons, sponsors and tech

    ← All projects

    Reckon

    An agent manages a treasury under rules it can't forge. Refusing it is the whole point.

    Demo

    About

    If you hand money to an agent, you're trusting it not to do something dumb while you're asleep. Right now that trust sits in the agent's own code — an if-statement it can talk itself out of. reckon replaces that with a document. The owner writes a mandate in plain terms, signs it, and the agent holds a copy it can't alter. Six clauses: keep 60% in stablecoins, never more than 15% in one asset, max $500 a trade, only tokens older than 90 days with real liquidity, an hour between trades, and Uniswap only. What gets signed is the readable text plus a hash of the exact numbers, so every decision the agent makes can point back to the rules that were live at the time. Every clause is then enforced in three independent places. The evaluator checks the portfolio-wide limits against live data. Those same clauses compile into a Privy wallet policy, so the agent physically can't sign a transaction outside them — that refusal happens in Privy's signing enclave, not in our code. And anything the mandate merely bounds, rather than forbids, escalates to the owner, who signs an EIP-712 override good for one action, one token, one hour. Clause four is the one a price feed can't answer, so it comes from two Graph products composed together. The Uniswap v3 subgraph gives token age and the *trough* of daily TVL across thirty days — not today's number, because a pool that dropped to $40k last Tuesday isn't liquid however healthy it looks now. The Token API gives holder concentration. That second one taught us something: the top ten holders of WETH on Base own 52% of supply, but nine of the ten are bridges and protocol contracts, and real wallets hold 0.93%. The obvious metric rejects WETH for a reason that isn't real, so §4 counts wallets and the receipt records both figures. Execution is Uniswap v3 through SwapRouter02. The subgraph ranks fee tiers by TVL but doesn't get to set the price: we quote all four through QuoterV2 and take the best fill, which is about 0.6% better on WETH/USDC, with the winner flipping from 0.01% to 0.05% around $5,000. Four decisions in the demo, all against a fork of Base mainnet — real pools, real prices, real Privy signatures, fake funds. A $5 WETH rebalance clears every clause and executes. $5 of DEGEN is refused: trough liquidity was $493,000 against a $2M floor. A $12 WETH buy would put 34% into one asset against a 15% ceiling, so it stops and waits. Then the owner signs, the policy is widened for that single action, it executes, and the original policy goes back. Every decision appends a receipt: what it saw and where the data came from, what it wanted to do and why, which clauses it cleared or broke with the observed values and the limits, who authorised it, what it did on chain, and the hash of the mandate in force. The dashboard is that ledger and nothing else — no charts, no dials. One thing to be straight about: the planner that decides *what* to propose is written but not wired in, and has never been run — we had no API key. The four proposals in the demo are fixtures we wrote. So what exists is the layer that constrains an agent, with the decision-maker sketched in but not plugged in. It's called out in the README rather than left for someone to find.

    How it's made

    TypeScript throughout, Next.js 16 for the dashboard, viem for anything touching a chain, zod for validating the mandate before it's ever hashed. The mandate is an EIP-712 document. What gets signed is a readable sentence per clause plus a keccak of the canonicalised clause values — keys sorted recursively, so the same rules always hash the same way. Every receipt cites that hash. Change one number and the hash changes, which is the whole point. **Privy** is where the mandate stops being a promise. We compile the clauses into wallet policy rules: §6 becomes a `to` allowlist of the Uniswap router, §3 becomes an `amountIn` ceiling, §4 becomes a `tokenOut` allowlist — and those last two are `ethereum_calldata` conditions, meaning Privy decodes the swap arguments with the ABI and compares them inside the enclave. That's what makes "the agent physically cannot" true rather than aspirational. Four things cost us real time there, none of them documented where we looked: A catch-all `{method: "*", action: "DENY"}` rule silently disables the entire policy. We added it as a safety net and every transaction started failing, including compliant ones. Bisecting it: a minimal policy with one ALLOW rule passes; add the catch-all and the same transaction is refused. A matching DENY overrides every ALLOW regardless of order, and policies are already deny-by-default, so the "safety net" only removes function. The failure is silent — `policy_violation` looks identical whether your conditions are wrong or an extra rule is eating them. Constraining only `eth_sendTransaction` leaves a hole: the agent can call `eth_signTransaction` and broadcast the signed payload through any RPC it likes. We emit rules for both. Rule names must be under 50 characters. `chain_id` conditions take a string, not a number. Calldata fields are `"functionName.argumentName"` — `exactInputSingle.params.tokenOut`, not `params.tokenOut`. Each of those was a 400 with a different message. And `PrivyClientOptions` takes `appId` while the internal client type uses `appID`. Passing `appID` type-errors but *works at runtime*, because the SDK silently falls back to `process.env.PRIVY_APP_ID`. That sent us debugging a perfectly good app for a while. We ended up reading the SDK's `.d.ts` files directly rather than the docs, which is how we found the calldata condition shape too. **The hacky bit worth mentioning:** our signer asks Privy to *sign* and then broadcasts the raw transaction through our own RPC, rather than letting Privy send it. That was originally so we could execute against a local fork — Privy would have sent it to real Base. But it's better in production too: the policy is exercised at signature time, so it holds no matter which RPC is in play. The owner override uses the same trick in reverse. A valid EIP-712 override still isn't enough, because Privy will refuse anyway — so reckon compiles a widened policy containing one extra ALLOW rule for that specific token and amount, attaches it, executes, and restores the base policy in a `finally`. The widening lasts one transaction and the receipt cites the override hash. **The Graph** is two products, and they authenticate separately — a Graph Studio key returns 401 against the Token API with no `WWW-Authenticate` header to tell you why. The Token API also 500s on roughly one call in five (we measured: `500 200 200 200 200`), which matters because our risk gate fails closed. A flaky third-party turned into "the agent refuses to trade" three times in a row during a demo run. Retry with backoff, cache for 15 minutes, and prefer a stale answer over null. The subgraph gives token age and `poolDayDatas`. We take the *minimum* TVL across the window rather than an average — an average hides the day a pool got drained, which is the exact risk the clause exists to catch. Worth knowing: the community Base deployments report broken numbers, one WETH pool claims $34bn TVL, so we aggregate across pools and take minimums. **Uniswap** — the subgraph proposes a fee tier ranked by TVL, and we don't trust it with the price. Quoting all four tiers in parallel through QuoterV2 (which reverts internally, so it goes through `simulateContract`) is about 0.6% better on WETH/USDC, and the winner flips from 0.01% to 0.05% around $5,000. TVL ranks candidates; the quoter decides. Testing runs against a fork of Base mainnet: real pools, real prices, real signatures, fake money. We used a Tenderly Virtual TestNet with chain id 8453 deliberately, so the wallet policy is byte-identical to one guarding a real mainnet wallet, and burned through the free-tier cheat-code quota twice; anvil is the fallback and has no limits. The agent wallet has never been funded on real Base, which is what makes the replay warning moot. Receipts are one JSON object per line in an append-only file. No database. The file *is* the ledger, and keeping it as one plain readable file was deliberate.