Recipes
The shape
type Recipe = {
v: 1;
name: string; // up to 40 characters, also the contract's name
target: "hook" | "stack"; // own v4 hook, or stacked on v4 Marketplace
baseFee: number; // pips, 0 to 100,000: the floor of every swap's fee
modules: { id: ModuleId; p: Record<string, number | string> }[];
};
type ModuleId =
| "launchFee" | "volatilityFee" | "offHoursFee" | "cooldown" | "priceGuard"
| "launchAllowlist" | "holderRebate" | "agentRebate" | "swapTax" | "lpLock";Each module's parameters are listed with their units and bounds on the modules page, and served as JSON by GET /api/modules.
How a recipe is checked
Before a line is written, the recipe is normalized. Anything out of bounds is refused with the name of the module and the parameter:
vmust be 1. A target other thanstackis read ashook. An empty name becomes My Hook.- Each module at most once, at least one module, and every number a whole number inside its bounds.
- Addresses are checksummed and cannot be zero. An allowlist holds up to 50 wallets, duplicates removed. A token amount is a whole number of the token's own units.
- The volatility fee's cap must be at least its calm fee.
- A stacked recipe cannot take swap tax or liquidity lock: a stacked hook never moves funds.
- Modules are sorted into the builder's fixed order, so the same choice always gives the same source and the same address.
The string on chain
The factory records the normalized recipe as compact JSON, with g naming the generator that wrote the source, today v4builder/2. This is the string the Fair Launch example records:
{"v":1,"g":"v4builder/2","name":"Fair Launch","target":"hook","baseFee":3000,"modules":[{"id":"launchFee","p":{"startFee":250000,"duration":600}},{"id":"cooldown","p":{"seconds":30}},{"id":"lpLock","p":{"days":30}}]}Rebuilding a hook
Every hook built here can be checked from the chain alone. The recipe is in the factory's Built event; the generator turns it into the same Solidity; the compiler settings are fixed. The verify endpoint runs these steps and publishes the result on Sourcify.
// 1. Read the Built event for the hook from the factory.
event Built(address indexed hook, address indexed builder, uint8 indexed kind,
uint16 flags, bytes32 recipeHash, string recipe, uint256 paid);
// 2. keccak256(bytes(recipe)) == recipeHash, and the same hash is in built(hook).
// 3. JSON.parse(recipe), then build it with the generator named in "g".
// 4. Compile with solc 0.8.26, optimizer 200 runs, evmVersion cancun
// (the IR pipeline only if the legacy one runs out of stack).
// 5. Match the result against the hook's creation transaction, as Sourcify does.The three examples
The builder starts from any of these, or from a single module.
{
"v": 1,
"name": "Fair Launch",
"target": "hook",
"baseFee": 3000,
"modules": [
{
"id": "launchFee",
"p": {
"startFee": 250000,
"duration": 600
}
},
{
"id": "cooldown",
"p": {
"seconds": 30
}
},
{
"id": "lpLock",
"p": {
"days": 30
}
}
]
}{
"v": 1,
"name": "Stock Hours",
"target": "stack",
"baseFee": 500,
"modules": [
{
"id": "volatilityFee",
"p": {
"calmFee": 500,
"maxFee": 20000,
"sensitivity": 2000
}
},
{
"id": "offHoursFee",
"p": {
"closedFee": 10000
}
},
{
"id": "priceGuard",
"p": {
"maxMove": 500,
"window": 900
}
}
]
}{
"v": 1,
"name": "Agent Lane",
"target": "hook",
"baseFee": 3000,
"modules": [
{
"id": "agentRebate",
"p": {
"rebate": 50
}
},
{
"id": "swapTax",
"p": {
"bps": 20,
"to": "0x000000000000000000000000000000000000dEaD"
}
}
]
}