Hooks / Agent-native
X402Gate
Makes the pool an x402 resource server: a swapper that presents a signed x402 payment gets a cheaper fee on that swap, and the payment settles atomically with the trade.
X402Gate implements 2 of the fourteen Uniswap v4 callbacks:
afterInitialize, beforeSwap.
How it works
x402 is how autonomous agents already pay for things. An agent asks for a resource, gets back HTTP 402 with a machine-readable price, signs an EIP-3009 authorization for that amount, retries with the signature in a header, and the resource server settles it. Thousands of endpoints speak it and every agent framework has a client for it.
What nothing speaks it for is liquidity, which is odd, because a fee tier is exactly the kind of thing an agent would want to buy: it is metered, it is worth different amounts to different callers, and the caller knows its own value better than the venue does. The usual way to give one class of swapper a better price is to gate on what they *are*: hold this NFT, be on this allowlist, pass this KYC check. Every one of those is a proxy for willingness to pay, maintained by hand, and wrong at the edges.
This hook gates on the thing itself. Pay the pool's posted price and the swap is cheaper. Do not, and it is not.
Nobody curates anything. Three properties fall out of doing this in the hook rather than over HTTP: The 402 challenge is on-chain. {quote} returns the same fields an x402 client reads out of a 402 response body: scheme, network, amount, asset, recipient, resource, timeout.
An agent discovers the price with one `eth_call` against the pool it was already going to trade, with no endpoint to find, no server to be up, and no TLS. Payment and delivery are atomic, which over HTTP they are not. An x402 client that pays for an API call and then receives a 500 has paid for nothing and must argue about a refund.
Here the payment settles inside `beforeSwap`, so if the swap reverts for any reason afterwards, slippage, liquidity, another hook, the payment reverts with it. The failure mode that makes x402 awkward to build on does not exist in this direction. There is no facilitator.
The x402 deployment model puts a trusted service between payer and resource server to verify and broadcast the authorization. The pool can do both itself, because it is already a contract and the payment is already an on-chain object, so the trusted third party is simply absent rather than decentralized. How the discount is applied matters.
The fee this hook returns is an *LP* fee, so the discount is paid for by liquidity providers taking less on that swap, and the payment is what compensates them. Providers are therefore selling cheap execution for a fixed fee up front instead of a variable one on the back end, which is a trade they can price: if the posted price is set below the fee revenue given up, the pool leaks, and setting it is the one judgement the pool creator has to get right. Payments are pulled with `receiveWithAuthorization` rather than `transferWithAuthorization`.
Both are EIP-3009 and x402's own reference facilitator uses the latter, but the latter can be broadcast by anyone: a bystander could submit the payer's authorization on its own, the payment would land, the nonce would burn, and the swap that the payment was for would then revert with the payer out of pocket. sender == to` removes that. Unrecognised `hookData` is ignored rather than rejected.
The payload is prefixed with {X402_PAYMENT_MAGIC}, and anything that does not start with it is treated as "no payment offered" and charged the base fee. A pool that reverted on hookData it did not understand would be untradeable through any router that puts its own data there, which is most of them.
Prior art
Fee discounts gated on NFT or token ownership, allowlists and KYC attestations are among the most common hook patterns, and x402 itself is a widely deployed HTTP payment protocol with an EIP-3009 settlement scheme. Neither has met the other: making an AMM pool an x402 resource server, so that the 402 challenge is an `eth_call` and settlement is atomic with the swap it paid for, is the contribution here.
Where it does not help
A payment costs a full EIP-3009 transfer, so the discount only repays its own gas above a swap size that depends on the chain and the spread between the two fee tiers. On a mainnet-priced chain that floor is high enough that this is a hook for agents moving real size, not for retail swaps. It also inherits the hookData problem: an aggregator that does not forward hookData cannot present a payment, and its users silently pay the base fee rather than getting an error telling them why.
Configuring a pool
Uniswap v4 removed hookData from initialize, so a hook that needs per-pool parameters has to
receive them out of band. X402Gate takes them through configure, which anyone may call for a pool
key whose pool does not exist yet, and which nobody may call afterwards. The parameters are part of what the pool
is, so they are fixed for its lifetime.
// 1. Fix the terms, before the pool exists.
hook.configure(
key,
X402GateHook.Config({
asset: /* address */ 0,
payTo: /* address */ 0,
price: /* uint256 */ 0,
baseFee: /* uint24 */ 0,
discountedFee: /* uint24 */ 0
})
);
// 2. Initialize the pool. The hook rejects a pool it was never configured for.
poolManager.initialize(key, startingSqrtPriceX96);
| Parameter | Type | Units |
|---|---|---|
asset | address | |
payTo | address | |
price | uint256 | |
baseFee | uint24 | hundredths of a bip (3000 = 0.30%) |
discountedFee | uint24 | hundredths of a bip (3000 = 0.30%) |
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("x402-gate");
const address = hookAddress("x402-gate", 1);
const key = poolKeyFor({hook: address, currencyA: USDC, currencyB: WETH, tickSpacing: 60, dynamicFee: true});
console.log(poolId(key));
What it reverts with
| Error | Meaning |
|---|---|
DiscountNotADiscount(uint24,uint24) | A pool posted a discounted fee that is not below its base fee, so paying would buy nothing. |
FeeTooLarge(uint24) | A fee was configured above the protocol maximum of 100%. |
InvalidTerms() | Terms named the zero address as the payment asset or the payee. |
NotDynamicFee() | The hook was attempted to be initialized with a non-dynamic fee. |
NothingCollected() | There is nothing accrued for this pool to withdraw. |
PaymentBelowPrice(uint256,uint256) | The payment offered is smaller than the pool's posted price. |
PoolAlreadyInitialized() | The pool already exists, so its configuration is final. |
PoolNotConfigured() | The pool was initialized without a configuration for this hook. |
PriceRequired() | Terms posted a price of zero, which would make the discount free and the gate pointless. |
SafeERC20FailedOperation(address) | An operation with an ERC-20 token failed. |
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/X402GateHook.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)" # X402Gate
cast call $HOOK "specURI()(string)" # https://hookforge.pages.dev/schema/hooks/x402-gate.json
cast call $HOOK "hookTags()(string[])" # agent-native, x402, dynamic-fee, metering