Swap API
POST /v1/swap/build turns a pool, an input token, and an amount into the exact
transactions a wallet needs to swap on Uniswap v4: approvals when needed, then
a single UniversalRouter call. HookKit never signs, never holds funds, and
never submits transactions. The output goes to sender.
POST /v1/swap/buildRequest
Section titled “Request”| Field | Required | Notes |
|---|---|---|
poolId |
one of | PoolId of an indexed pool |
currency0, currency1, fee, tickSpacing, hooks |
one of | The pool key, for any pool |
tokenIn |
no | Input token address (or use zeroForOne, default true) |
amount |
yes | Exact input amount, decimal string in token units |
sender |
yes | The wallet that signs, pays, and receives the output |
slippageBps |
no | 1 to 5000. Default 50 (0.5%) |
hookData |
no | Bytes passed to the hook. Default 0x |
Swaps are exact-input. HookKit quotes the swap with the V4Quoter (running the
hook), then sets minAmountOut = expectedAmountOut * (1 - slippageBps / 10000).
Example: ERC-20 in
Section titled “Example: ERC-20 in”Swapping 25 USDG for ETH from a wallet that has never used Permit2:
curl -X POST https://api.hookkit.dev/v1/swap/build \ -H "content-type: application/json" \ -d '{ "poolId": "0x387bf619da4d3fb62bb276482693dba1b9b3520f573cabdfe033384a24125982", "tokenIn": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", "amount": "25", "sender": "0x1111111111111111111111111111111111111111", "slippageBps": 50 }'{ "chainId": 4663, "poolId": "0x387bf619da4d3fb62bb276482693dba1b9b3520f573cabdfe033384a24125982", "sender": "0x1111111111111111111111111111111111111111", "tokenIn": { "address": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", "symbol": "USDG", "name": "Global Dollar", "decimals": 6 }, "tokenOut": { "address": "0x0000000000000000000000000000000000000000", "symbol": "ETH", "name": "Ether", "decimals": 18 }, "amountIn": "25", "expectedAmountOut": "0.010760328858221029", "minAmountOut": "0.010706527213929923", "slippageBps": 50, "deadline": 1793273323, "transactions": [ { "to": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", "data": "0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3ffff...ffff", "value": "0", "description": "Approve Permit2 to spend the input token (one-time)" }, { "to": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "data": "0x87517c450000000000000000000000005fc5360d0400a0fd4f2af552add042d716f1d168...", "value": "0", "description": "Allow the Universal Router to pull the input token via Permit2 (30 days)" }, { "to": "0x204FAca1764B154221e35c0d20aBb3c525710498", "data": "0x3593564c...", "value": "0", "description": "Swap through the Uniswap v4 Universal Router" } ], "note": "Unsigned. Send the transactions in order from `sender`; HookKit never signs or holds funds."}What the transactions do
Section titled “What the transactions do”- ERC-20
approve(Permit2, max): only when the token’s allowance to Permit2 is too low. One-time per token. Permit2.approve(token, UniversalRouter, amount, expiration): only when the router’s Permit2 allowance is too low or expired. Valid for 30 days.UniversalRouter.executewith oneV4_SWAPcommand runningSWAP_EXACT_IN_SINGLE,SETTLE_ALL, andTAKE_ALL. The output is sent tosender, and the swap reverts if it would receive less thanminAmountOutor ifdeadline(20 minutes from the build) has passed.
HookKit reads the sender’s current allowances when building, so steps 1 and 2
are left out when they are already in place. When tokenIn is native ETH there
is nothing to approve: you get only the swap, with value set to the input
amount.
{ "amountIn": "0.01", "expectedAmountOut": "23.204353", "minAmountOut": "23.088331", "transactions": [ { "to": "0x204FAca1764B154221e35c0d20aBb3c525710498", "data": "0x3593564c...", "value": "10000000000000000", "description": "Swap through the Uniswap v4 Universal Router" } ]}Send them with viem
Section titled “Send them with viem”Send the transactions in order, waiting for each to be mined, from the same
account you passed as sender.
import { createWalletClient, createPublicClient, defineChain, http } from "viem"import { privateKeyToAccount } from "viem/accounts"import { HookKit } from "@hookkit/sdk"
const robinhoodChain = defineChain({ id: 4663, name: "Robinhood Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },})
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)const wallet = createWalletClient({ account, chain: robinhoodChain, transport: http() })const client = createPublicClient({ chain: robinhoodChain, transport: http() })
const hookkit = new HookKit()
const swap = await hookkit.swap.build({ poolId: "0x387bf619da4d3fb62bb276482693dba1b9b3520f573cabdfe033384a24125982", tokenIn: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", // USDG amount: "25", sender: account.address, slippageBps: 50,})
console.log(`Swapping ${swap.amountIn} ${swap.tokenIn.symbol}, min out ${swap.minAmountOut} ${swap.tokenOut.symbol}`)
for (const tx of swap.transactions) { const hash = await wallet.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value), }) const receipt = await client.waitForTransactionReceipt({ hash }) if (receipt.status !== "success") throw new Error(`${tx.description} failed: ${hash}`) console.log(tx.description, hash)}In a browser, use your wallet connector’s WalletClient (wagmi, RainbowKit,
Privy, and so on) instead of a private key; the loop is the same.
Before you sign
Section titled “Before you sign”A hook runs inside your swap. Check the pool’s hook first:
GET /v1/pools/:poolIdreturnshook.capabilitiesfor the pool’s hook.GET /v1/hooks/:addressadds bytecode and proxy checks.
See Reading a hook’s risk profile. The
minAmountOut bound protects you against a hook that returns less than quoted,
but not against approvals you grant to other contracts.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 400 | bad_request |
Missing sender, bad amount, tokenIn not in the pool |
| 404 | not_found |
poolId not indexed (pass the key) or pool not initialized |
| 422 | no_route |
The quote reverted, so no swap can be built |
