Docs

The hook that only lets agents in. A Uniswap v4 hook on Robinhood Chain (chain id 4663) that rejects every swap without a short-lived agent pass.

It doesn't prove you're an AI. A human with a script counts as an agent here. That's the joke and the rule.

Quickstart

Try it in 10 seconds, no wallet. This asks the pass server to fetch a challenge, solve it, get a pass and simulate a 0.0001 ETH swap from the demo wallet. Nothing is sent on-chain.

curl -s -X POST https://hook404.xyz/api/simulate -H 'content-type: application/json' -d '{"mode":"agent"}'

The same call with {"mode":"human"} simulates a swap without a pass and returns 404: humans not found (HumansNotFound()). That count is labelled simulated everywhere.

For a real swap from your own wallet, download the agent and run it. Maximum 0.0002 ETH per swap.

curl -O https://hook404.xyz/agent.mjs
node agent.mjs --pk 0x<your-private-key> --eth 0.0001

The agent asks for a challenge, answers it, takes the pass, calls router.swap and prints the transaction with a Blockscout link. Use a throwaway wallet with a few cents of ETH. There is no Connect Wallet button. Agents don't click.

Challenge

A reverse CAPTCHA: easy for a program, impossible by hand.

GET /api/challenge?swapper=0x<your-address>
{
  "id": "0x<32 bytes>",
  "swapper": "0x<your-address>",
  "items": ["0x<32 bytes>", ... 64 items],
  "expiresInMs": 2000,
  "rule": "answer[i] = keccak256(keccak256(items[i]) ++ id)"
}
  • Answer rule: answer[i] = keccak256(keccak256(items[i]) ++ id). Hash each item as 32 raw bytes, append the 32-byte id, hash again. Keep the same order.
  • 64 items. Send all 64 answers to POST /api/pass within 2 seconds of the challenge being created (measured by the server). A program takes about 30 ms. A hand can't.
  • A challenge works once and is bound to the swapper address you asked for.
  • Failures: 400 with too_slow, wrong_answer, unknown_challenge, used or bad_swapper; 429 with quota and resetAt.
import { keccak256, concat } from "viem";
const c = await (await fetch(`https://hook404.xyz/api/challenge?swapper=${me}`)).json();
const answers = c.items.map((x) => keccak256(concat([keccak256(x), c.id])));
const body = JSON.stringify({ id: c.id, swapper: me, answers });
const pass = await (await fetch("https://hook404.xyz/api/pass", { method: "POST", headers: { "content-type": "application/json" }, body })).json();

me is your wallet address as a hex string.

Pass

A correct, in-time answer returns an agent pass: an EIP-712 signature from the pass server over (swapper, expiry, nonce).

Domain name"Hook404"
Version"1"
chainId4663
verifyingContractthe hook address (see Contracts)
TypehashPass(address swapper,uint256 expiry,bytes32 nonce)
Lifetime120 seconds. Single use: the hook marks the nonce used on-chain.
POST /api/pass  {"id", "swapper", "answers"}
{
  "swapper": "0x...", "expiry": 1761830000, "nonce": "0x...", "sig": "0x...",
  "hookData": "0x...",     // abi.encode(Pass{swapper, expiry, nonce, sig})
  "expiresAt": ...
}

hookData = abi.encode(Pass) where struct Pass { address swapper; uint256 expiry; bytes32 nonce; bytes sig; }. Pass the returned hookData as the last argument of router.swap. Ask for a new pass right before you send: a late transaction reverts with HumansNotFound().

Swap

The router is the only door to the pool. It takes ETH or USDG from the caller, swaps through the PoolManager, and pays the caller.

function swap(PoolKey calldata key, bool zeroForOne, uint256 amountIn, uint256 minOut, bytes calldata pass)
    external payable returns (uint256 amountOut);
currency00x0000000000000000000000000000000000000000 (native ETH)
currency10x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 (USDG, 6 decimals)
fee3000
tickSpacing60
hooksthe hook address (see Contracts)
  • zeroForOne = true: ETH to USDG. Send msg.value == amountIn. Nothing to approve.
  • zeroForOne = false: USDG to ETH. Approve the router for USDG first; msg.value must be 0.
  • amountIn is exact input. minOut is the least output you accept, else TooLittleOut and the pass nonce stays unused.
  • Maximum 0.0002 ETH per swap. The server and the agent refuse more. The pool is a demo.
  • Wrong msg.value reverts with WrongValue. Universal Router or any other router gets HumansNotFound(): the hook only accepts the Hook404 router.

Example with cast, using the hookData from the previous step (0.0001 ETH is 100000000000000 wei):

export RPC=https://robinhood-rpc.publicnode.com
cast send $ROUTER \
  "swap((address,address,uint24,int24,address),bool,uint256,uint256,bytes)" \
  "(0x0000000000000000000000000000000000000000,0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168,3000,60,$HOOK)" \
  true 100000000000000 0 $HOOKDATA \
  --value 100000000000000 --private-key $PK --rpc-url $RPC

minOut is 0 here for brevity; set a real floor when it matters. Quote first with cast call using the same arguments and your address as --from.

Design

  • A pass is not proof of AI. The challenge is built so any program passes and a human hand does not.
  • Replay. The nonce is single use on-chain. The pass is bound to the swapper and to a 120 second expiry. The EIP-712 domain binds chain id and hook address, so a pass from another chain or contract is worthless.
  • Bound to you. The router writes swapper = msg.sender into the pass before the hook sees it. A pass copied from another wallet fails.
  • Simulations do not burn nonces. eth_call changes nothing. The server marks a simulated pass as spent (sim) so it is not handed out again, but it would still be valid on-chain for the same swapper.
  • Signer key leaks. Humans could get in. No funds are at risk because the hook holds nothing; the plan would be a new hook and pool, written here as a plan, not a promise.
  • Server down. No pass server, no swaps. Liquidity is never locked.
  • Sequencer. Robinhood Chain has a single sequencer and no public mempool, so a pass cannot be lifted from the mempool within its 120 seconds.
  • One error. Every rejection is HumansNotFound(): no pass, stale pass, reused nonce, wrong signature, wrong swapper, wrong router. The detail lives in /api/simulate.

The rule

It doesn't prove you're an AI. A human with a script counts as an agent here. That's the joke and the rule.

The pass proves a program answered a challenge in time. That is all it claims.

Quota

Holding ≥ 1,000,000 raises your passes from 24 to 1,000 a day. Enforced by the pass server, not by the contract.

Limits are per swapper address per UTC day. Before launch everyone gets 24. Holders also get a 600 second pass instead of 120. Over the limit, /api/pass answers 429 with quota and a reset time. The contract knows nothing about the token.

Contracts

No owner, no pause, holds nothing. The hook has one job (beforeSwap). The router is a plain door; the hook is the bouncer. Addresses below are read live from the API and link to Blockscout; the source is verified on Sourcify (exact match).

Loading addresses...

Interfaces

contract Hook404 {
  struct Pass { address swapper; uint256 expiry; bytes32 nonce; bytes sig; }
  bytes32 public constant PASS_TYPEHASH = keccak256("Pass(address swapper,uint256 expiry,bytes32 nonce)");
  address public immutable router;  address public immutable signer;
  mapping(bytes32 => bool) public used;   // nonce, single use
  uint256 public admitted;                // swaps that got through
  event AgentAdmitted(address indexed swapper, bytes32 indexed nonce, uint256 expiry, bool zeroForOne, int256 amountSpecified);
  error HumansNotFound();
  function passDigest(address swapper, uint256 expiry, bytes32 nonce) public view returns (bytes32);
  function isValidPass(address swapper, uint256 expiry, bytes32 nonce, bytes calldata sig) external view returns (bool);
}
contract Hook404Router {
  event Swapped(address indexed swapper, bool zeroForOne, uint256 amountIn, uint256 amountOut);
  error WrongValue(); error TooLittleOut(uint256 got, uint256 min);
  function swap(PoolKey calldata key, bool zeroForOne, uint256 amountIn, uint256 minOut, bytes calldata pass) external payable returns (uint256 amountOut);
}

The hook address carries the beforeSwap flag in its low bits. The signer, router and pool manager are immutable: nobody can change them after deployment. Pool manager: 0x8366a39CC670B4001A1121B8F6A443A643e40951. Live proof of every deployment step is on /proof.

Liquidity

Anyone can add liquidity. Our hook only guards swaps. The team seeded a small full-range position, held in a wallet, with no withdraw function in our contracts. Liquidity in the pool is tiny; see the live figure on /proof.

Risks

  • Tiny liquidity: prices can move a lot, even on 0.0002 ETH.
  • Small team, small server. If the pass server stops, the pool goes quiet.
  • USDG is issued by a third party that can pause or freeze transfers; a swap then reverts as a whole.
  • If the signer key leaks, humans can get in until a new hook and pool exist.
  • The router has a swap limit of 0.0002 ETH for the demo. The hook itself has no amount cap.
  • The contracts are source-verified on Sourcify. They have not been reviewed by an outside firm.

FAQ

Can a human trade?

Yes, by writing a script. That's the point.

Does it prove I'm an AI?

No.

What if your server is down?

No passes, no swaps; LPs can always exit.

Is there a Connect Wallet button?

No. Agents don't click. Use the curl line or agent.mjs.

Why did my swap revert with HumansNotFound()?

No pass, a pass older than 120 seconds, a reused nonce, a pass for another wallet, or a swap through another router. Get a fresh pass right before you send.

Where do I see what happened?

/feed lists every admitted swap. /proof links the contracts, pool, liquidity and a real reverted human transaction.