Using it in a bot or an agent
Call the scan before routing through a v4 pool, turn it into a yes or no in code, and give an AI agent the evidence rather than the decision.
A trading bot, an aggregator or an AI agent that routes through v4 pools on Robinhood Chain runs into hooks it has never seen. The scan API lets it look before it trades. This page shows one way to wire it in.
Where the hook address comes from
Every v4 pool key carries its hook: key.hooks. If you have the pool key (from a quote, a router path or your own index), you have the hook. If you only have a pool id, the PoolManager's Initialize event for that id lists the key, hook included. A hook address of 0x0000000000000000000000000000000000000000 means the pool has no hook, and there is nothing to scan.
A policy in code
Keep the decision in code you control. The scan provides the facts; the thresholds are yours.
type Status = "pass" | "caution" | "fail" | "n/a";
type Swap =
| { zeroForOne: boolean; ok: true; feePpm: number; takePpm: number }
| { zeroForOne: boolean; ok: false; why: "blocked" | "thin"; reason: string };
type Scan = {
hook: string;
block: number;
owner: string | null;
score: number;
verdict: "safe" | "caution" | "danger";
checks: { id: string; title: string; status: Status; detail: string }[];
pools: { id: string; key: { fee: number }; swaps: Swap[]; roundTrip: { ok: boolean } | null }[];
};
// The origin of the censoredHooks site.
const BASE = process.env.CENSOREDHOOKS_URL!;
export async function scan(hook: string): Promise<Scan> {
const res = await fetch(`${BASE}/api/scan?hook=${hook}`, { signal: AbortSignal.timeout(60_000) });
if (!res.ok) throw new Error(`scan ${hook}: HTTP ${res.status}`);
return (await res.json()) as Scan;
}
export function decide(s: Scan, poolId: string, maxCostPpm = 20_000): { route: boolean; why: string } {
if (s.verdict === "danger") {
const failed = s.checks.filter((c) => c.status === "fail").map((c) => `${c.id}: ${c.detail}`);
return { route: false, why: failed.join(" ") || "no contract at this address" };
}
const swaps = s.pools.flatMap((p) => p.swaps);
if (swaps.some((w) => !w.ok && w.why === "blocked")) return { route: false, why: "a probe swap was refused" };
const measured = swaps.filter((w): w is Extract<Swap, { ok: true }> => w.ok);
if (measured.length === 0) return { route: false, why: "nothing could be measured" };
// Pool fee plus the hook's take: what a swap costs before price impact (a close upper bound).
const worst = Math.max(...measured.map((w) => w.feePpm + w.takePpm));
if (worst > maxCostPpm) return { route: false, why: `a swap costs ${worst / 10_000}% in pool fee and hook take` };
const own = s.pools.some((p) => p.id.toLowerCase() === poolId.toLowerCase());
return { route: true, why: `${s.verdict}, worst cost ${worst / 10_000}%${own ? "" : " (measured on a sibling pool)"}` };
}
What this policy does, and why:
- Danger stops the route. The failing checks' sentences become the reason, which is what you want in a log.
- A refused probe swap stops the route, even if the verdict were not danger. A refusal is the one thing a bot cannot recover from after the fact. Only a revert counts: a
thindirection (for instance "no liquidity below the price" on a launch pool nobody has traded yet) is not a refusal. - No measurement stops the route. "Could not measure" is unknown, and unknown is not a pass.
- The cost is capped by you. Pool fee plus hook take, 2% here. The scan judges the two separately (the fee level in 04, the take in 02), but a swap pays both. Compare the total with your expected edge rather than with a fixed number if you can.
- It says whether your pool was measured. A scan measures up to two recent pools of the hook. If yours is not among them, the reading comes from a sibling pool.
Caching and rescanning
A scan is not free for the server and not instant for you.
- Cache per hook, keyed by the hook address. Many pools can share a hook. What the scan reads from the address and the code applies to all of them; the measured swaps come from up to two of them.
- Expire sooner when the hook can change. If
owneris set, or a pool key's fee has the0x800000bit (dynamic fee), keep the result for minutes, not hours, and rescan before large trades. - Use
block, not your own clock, to know how old a result is. - Treat errors as unknown. A 502 or a timeout is not a pass. Skip the route or fall back to pools with no hook.
Giving it to an AI agent
If an agent chooses routes, give it the scan as a tool and keep the hard rules in code.
- Run
decidebefore the model sees the route. A route that fails it never reaches the model. - For routes that pass, give the model the verdict, the score and each check's
detailsentence. They are written to be read: "It takes 3% of each swap on top of the pool fee." "Owned by 0x…. Owner-style functions: setFee, withdraw." - Do not let the model override a refusal. A prompt can be argued with; a threshold in code cannot.
- Log the scan's
blockwith every trade, so a later review can tell what the agent knew when it traded.
What it will not catch
The probe trades a small amount from a fresh address with empty hookData. A hook that taxes your bot's address, your size or your router differently will not show that in the scan. Read Limits once, and keep your own post-trade check: compare what you received with the quote, and stop routing through a hook the first time they disagree.