Hooks / Agent-native
AgentBudget
A spending limit the venue itself enforces: an autonomous agent may swap on this pool only within a budget its principal signed, and the pool refuses the swap that would exceed it.
AgentBudget implements 1 of the fourteen Uniswap v4 callbacks:
afterSwap.
How it works
Handing a key to an autonomous agent means choosing between two bad options. Approve a router for a small amount and the agent stalls the moment it needs more, at which point somebody has to be awake to raise it. Approve it for the usual unbounded amount and the only thing standing between a bug and the whole balance is the agent's own code, which is the thing you were trying not to trust.
Smart accounts answer this with a policy engine, which works and costs you a smart account: a migration, a new address, a new set of integrations, and a policy layer that every venue has to be taught about. This hook puts the limit somewhere neither of those touch, in the pool, where it applies to any wallet, any router and any account type, because it is enforced by the venue rather than by the spender. A principal signs one `Delegation`, off-chain and once: an agent address, a per-epoch cap in each of the pool's two currencies, an epoch length, an expiry.
The agent then signs each swap it makes under that delegation. On `afterSwap` the hook checks both signatures, measures what the swap actually spent from the balance delta, and reverts if that would take the agent past its cap for the current epoch. A revert in `afterSwap` unwinds the swap with it, so an over-budget trade cannot land.
Measuring in `afterSwap` rather than `beforeSwap` is deliberate and is what makes the cap honest. Before the swap the only figure available is `amountSpecified`, which on an exact-output swap says nothing about how much the swapper will actually pay; a budget checked against it would be trivially evaded by asking for an exact output and letting the input land wherever the curve puts it. After the swap the true spend is in the delta.
What this does not do, stated plainly, because the distinction matters: it never custodies funds, never moves a token, and grants no allowance. The agent still needs its own ERC-20 approval to trade at all. The hook only refuses to let this pool be the venue for a swap outside the budget.
An agent with an unbounded approval can still spend elsewhere, so this is a limit on a venue, not on a key, and it is worth exactly as much as the set of venues that enforce it. Both signatures are checked with ERC-1271 as well as ECDSA, so a principal or an agent may itself be a contract.
Prior art
Per-agent policy engines exist in smart-account land (session keys, ERC-7710 delegations, module-based spending limits), and hooks that gate swaps on an allowlist or a credential are common. Enforcing a signed, per-epoch, per-currency spending cap inside the AMM, on behalf of an EOA principal with no smart account anywhere in the path, is the contribution here.
Where it does not help
The cap binds this pool only. An agent holding an unbounded ERC-20 approval can spend the same funds on any venue that does not enforce the delegation, so this raises the cost of a compromised agent rather than bounding it absolutely. It also requires the caller to pass hookData, so an aggregator that strips it will simply be unable to trade the pool.
From TypeScript
The SDK ships the catalogue, the address book and the pool-key helpers, so a client never hardcodes an address or recomputes a pool id by hand.
npm i @hookforge/sdk
import {getHook, hookAddress, poolKeyFor, poolId} from "@hookforge/sdk";
const hook = getHook("agent-budget");
const address = hookAddress("agent-budget", 1);
const key = poolKeyFor({hook: address, currencyA: USDC, currencyB: WETH, tickSpacing: 60});
console.log(poolId(key));
What it reverts with
| Error | Meaning |
|---|---|
AuthorizationRequired() | The swap carried no `hookData`, so there was no delegation to check it against. |
BadAgentSignature() | The swap signature does not recover to the delegation's agent. |
BadPrincipalSignature() | The delegation signature does not recover to the named principal. |
BudgetExceeded(uint8,uint128,uint256) | The swap would take the agent past its budget for the epoch in progress. |
Expired() | The delegation has expired, or the swap authorisation has. |
InvalidEpochLength() | `epochLength` of zero would make every swap its own epoch, which is no budget at all. |
NonceAlreadyUsed(uint256) | This nonce has already been spent under this delegation. |
Revoked() | The principal has revoked this delegation. |
WrongPool() | The delegation is for a different pool than the one being swapped. |
Addresses
No addresses published yet. The deploy script mines a deterministic address per chain, so the address a hook will occupy is known before it is deployed; this hook has not had that step run.
Source and verification
The contract is contracts/src/hooks/AgentBudgetHook.sol,
and everything on this page is generated from it: the prose is its NatSpec, the parameters are its
configure ABI, the callbacks above are the flags it declares, and the tags are the strings its own
hookTags() returns. A hook cannot be documented here as something it is not.
Ask the deployed contract what it is and it answers directly, with no registry in the loop:
cast call $HOOK "hookName()(string)" # AgentBudget
cast call $HOOK "specURI()(string)" # https://hookforge.pages.dev/schema/hooks/agent-budget.json
cast call $HOOK "hookTags()(string[])" # agent, delegation, spending-limit, eip712, no-admin