For builders
Build on Montra
Montra pools are standard Uniswap v4 pools with the Montra hook attached: they trade through Uniswap's Universal Router or any other v4 router, a quote matches its trade exactly, and Montra's lens and its own simple router make reading, quoting and trading a pool one call each.
On this page
- Registry
- get(name) · hooks() · isKnownHook(hook)
- Names every other address and lists every hook.
- Lens
- tokenView(token)
- A pool's key, terms, price, fees and progress in one read.
- Router
- quoteExactIn · swapExactIn
- LaunchRouter, the site's simple single-pool router: a quote and a trade, one code path.
- Any v4 router
- Universal Router · V4Quoter
- Uniswap's own, multi-hop included, with the key the hook gives.
- Hook
- poolKey(poolId) · poolConfig(poolId)
- A pool's key and its terms.
No bonding curve, no migration
A Montra token has no bonding curve, and nothing migrates. There is no second pool, no liquidity to move and no new address: the Uniswap v4 pool a token launches in is its market for life, and its poolId and PoolKey never change. A graduated token trades as it did before.
Graduation is still there, as a line in the pool: 16.8× the opening price on the menu's rows, about $58.7k of market cap from a $3.5k opening. Passing it moves nothing and changes no fee. It is there so a terminal's usual columns work on Montra as they do anywhere else.
Column
- New
- Which poolsLess than half way to the line
- API
stagenew - On chain
graduationProgressunder 5,000,PoolConfig.stage0 - About to graduate
- Which poolsHalf way or more, not yet graduated
- API
stagegraduating - On chain
graduationProgressof 5,000 or more,PoolConfig.stage0 or 1 - Graduated
- Which poolsPast the line and confirmed, for good
- API
stagegraduated - On chain
PoolConfig.stage2, 3 or 4
| Column | Which pools | API stage | On chain |
|---|---|---|---|
| New | Less than half way to the line | new | graduationProgress under 5,000, PoolConfig.stage 0 |
| About to graduate | Half way or more, not yet graduated | graduating | graduationProgress of 5,000 or more, PoolConfig.stage 0 or 1 |
| Graduated | Past the line and confirmed, for good | graduated | PoolConfig.stage 2, 3 or 4 |
Half way is the cut Montra's own board uses. progressBps runs from 0 to 10,000, so a terminal can draw the column wherever it likes.
- A column.
GET /api/tokens?stage=graduating&sort=progresslists the pools closest to the line first.stagetakesnew,graduatingorgraduated, andsort=graduatedputs the latest graduates first. - A progress bar.
graduation.progressBpson a card, orLaunchLens.graduationProgress(address hook, bytes32 poolId) view returns (uint256)on chain: the quote the seed has collected over the quote it holds at the line, the figure a bonding curve's bar shows. - The moment it graduates. The hook's
Graduatedevent, orgraduation.graduatedAtin the API. There is no migration transaction to watch for: the first swap of a later transaction confirms the line, and nobody has to call anything. - Past the line, not yet confirmed.
graduationProgressreads 10,000 at stageCrossed(1). The next transaction's swap graduates the pool, whichever way it trades, so a pool atCrossedis almost always about to move: keep it at the top of About to graduate untilGraduated. A sale can graduate a pool and leave the price back under the line. Only a sale in the transaction that crossed the line steps the pool back, before any read can see it. - A pool with no line.
graduationTicksof zero inPoolRegisteredmeans graduated from birth, with noGraduatedevent. The API already counts it asgraduated.
Find and trust a pool
Find tokens from the factory's TokenLaunched events or from the public API. Accept a pool only if its hook is one the factory announced (ModuleSetPublished, emitted once the factory has checked the hook), which the registry mirrors (hooks(), isKnownHook(hook)): filter by it rather than taking the first pool you find for a token. An indexer pins the factory's proxy, whose address never moves; an app pins the registry and reads every other address from it by name. The addresses themselves are under Contracts.
Trust a token by its code, never by its name: a launch is an EIP-1167 clone of a token master the factory published, with a supply. Anyone can clone a master, but only the factory it names can initialize one, so a clone with a supply is a launch.
MONTRA, the launchpad's own token, is the token the registry names under montra (get("montra"), the name's ASCII right-padded to 32 bytes), and nothing else is, whatever its name or symbol. Until the registry names one, there is no MONTRA.
An indexer starts at the factory proxy's deployment block: no launch comes before it, and the creator's first buy is logged inside the launch's own transaction, before its TokenLaunched.
PoolKey({
address currency0: the lower address (native HYPE = 0x0000…0000),
address currency1: the higher address,
uint24 fee: 0x800000, // dynamic: the LP fee lives in the pool's slot0
int24 tickSpacing: 200, // the row's; every row of the menu's
address hooks: <launch hook> // one the factory announced; this one's low 14 bits are 0x38EC
})The registry's keys
get(bytes32 key) returns (address) takes the key's ASCII right-padded to 32 bytes (viem's stringToHex(key, { size: 32 }), cast's --format-bytes32-string), and answers zero for a key not set. Every change is announced by AddressSet(key, previous, current); hooks are not keys but a list (hooks(), isKnownHook(hook), HookAdded), from which none is ever removed.
Key
factory- What it namesThe factory's proxy: launches, rows and module sets. Once deployed, its address never moves.
- SetAt deployment
router- What it namesLaunchRouter: a quote and an exact-input swap on one pool.
- SetAt deployment
lens- What it namesLaunchLens: stateless reads, replaceable, so read it here.
- SetAt deployment
quoteRegistry- What it namesThe quote assets a launch can pair with.
- SetAt deployment
protocolVault- What it namesProtocolFeeVault: the protocol's share and the launch fees.
- SetAt deployment
feeMarket- What it namesFeeMarket: the sales of the protocol's fees.
- SetAt deployment
routeFiller- What it namesRouteFiller: fills a sale through pools.
- SetAt deployment
buybackFiller- What it namesBuybackFiller: the buyback's only taker.
- SetAt deployment
poolManager- What it namesUniswap v4's PoolManager, where every pool lives.
- SetAt deployment
custodian- What it namesWhere the venue keeps every pool's balances, which holder rewards leave out: on v4 the PoolManager itself.
- SetAt deployment
universalRouter- What it namesUniswap's Universal Router, for exact-output and multi-hop trades through a launch pool. The registry's own address here, or under
permit2orquoter, means withdrawn: none. - SetBy the governor; zero while unset, which is not an error
permit2- What it namesCanonical Permit2, which that router pulls every ERC-20 input with: a sale's token, and the quote of a buy in an ERC-20. Each token needs two approvals: the token's to Permit2, then Permit2's to the router, with an expiry.
- SetWith
universalRouter; zero while unset quoter- What it namesUniswap's V4Quoter, for quoting a swap by either side:
quoteExactInquotes exact input alone. - SetBy the governor; zero while unset, which is not an error
gatewayRouter- What it namesThe chain's token bridge router, which Montra's API asks whether a listed quote is the bridge's canonical token. Nothing on chain calls it.
- SetBy the governor; zero while unset
| Key | What it names | Set |
|---|---|---|
factory | The factory's proxy: launches, rows and module sets. Once deployed, its address never moves. | At deployment |
router | LaunchRouter: a quote and an exact-input swap on one pool. | At deployment |
lens | LaunchLens: stateless reads, replaceable, so read it here. | At deployment |
quoteRegistry | The quote assets a launch can pair with. | At deployment |
protocolVault | ProtocolFeeVault: the protocol's share and the launch fees. | At deployment |
feeMarket | FeeMarket: the sales of the protocol's fees. | At deployment |
routeFiller | RouteFiller: fills a sale through pools. | At deployment |
buybackFiller | BuybackFiller: the buyback's only taker. | At deployment |
poolManager | Uniswap v4's PoolManager, where every pool lives. | At deployment |
custodian | Where the venue keeps every pool's balances, which holder rewards leave out: on v4 the PoolManager itself. | At deployment |
universalRouter | Uniswap's Universal Router, for exact-output and multi-hop trades through a launch pool. The registry's own address here, or under permit2 or quoter, means withdrawn: none. | By the governor; zero while unset, which is not an error |
permit2 | Canonical Permit2, which that router pulls every ERC-20 input with: a sale's token, and the quote of a buy in an ERC-20. Each token needs two approvals: the token's to Permit2, then Permit2's to the router, with an expiry. | With universalRouter; zero while unset |
quoter | Uniswap's V4Quoter, for quoting a swap by either side: quoteExactIn quotes exact input alone. | By the governor; zero while unset, which is not an error |
gatewayRouter | The chain's token bridge router, which Montra's API asks whether a listed quote is the bridge's canonical token. Nothing on chain calls it. | By the governor; zero while unset |
The launchpad's own token's key is set by the governor's last call in the batch that launches it, and reads zero until then.
The ABIs
Every Montra contract's ABI, and Uniswap's PoolManager's, is published from the contracts' build: /docs/abis.json, an object of from, the build they came from, and contracts, each contract's ABI by its name; /docs/abis/<Contract>.json, one contract's as a plain array, ready for viem or ethers; and /docs/abis/events.json, every event once, to decode any log an indexer reads. Each contract change refreshes them (npm run pull:abis in the site's repo), and every function, event, error, argument and field these pages name is checked against them.
| File | What it is |
|---|---|
LaunchpadRegistry.json | LaunchpadRegistry: the address book |
LaunchFactory.json | LaunchFactory: launches and module sets (the proxy's ABI) |
LaunchToken.json | LaunchToken: every launch token |
LaunchHook.json | LaunchHook: the pools' hook |
LaunchLens.json | LaunchLens: the read views |
LaunchRouter.json | LaunchRouter: quotes and exact-input swaps |
QuoteRegistry.json | QuoteRegistry: the quote assets |
FeeSplitter.json | FeeSplitter: a split's clone |
ProtocolFeeVault.json | ProtocolFeeVault: the protocol's share |
FeeMarket.json | FeeMarket: the fee market's sales |
RouteFiller.json | RouteFiller: filling a sale through pools |
BuybackFiller.json | BuybackFiller: the buyback's filler |
TokenBurner.json | TokenBurner: the buyback's burner |
PoolManager.json | PoolManager: Uniswap v4's, for Swap, Initialize and ModifyLiquidity, and the errors a refusal arrives in, the V4Quoter's wrapper among them |
The Universal Router, the V4Quoter and Permit2 are Uniswap's, and their ABIs are not published here. Take each from Uniswap's own repository (Uniswap/universal-router, Uniswap/v4-periphery, Uniswap/permit2) at the version the address the registry names is running, or from the chain's explorer where that address is verified. The calls these pages use are written out under Trade through the Universal Router.
A script or an AI tool reads the docs' machine files as plain files: these ABIs, /llms.txt, /llms-full.txt and each page's Markdown at /docs/<page>.md.
curl -s https://montramarkets.xyz/docs/abis/LaunchRouter.jsonRead, quote and trade
- An app hard-codes the registry and reads the rest from it. Names are
bytes32strings. In these samplesclientis a viem public client,walleta wallet client foraccount, andregistryAbi,lensAbiandrouterAbithe registry's, the lens's and the router's ABIs.TypeScript// get(bytes32 key) view returns (address) import { stringToHex, zeroAddress } from "viem"; const registry = "0xF1E2FA75422F889e565F3142bdbf60B28dce2aa7"; const name = (s: string) => stringToHex(s, { size: 32 }); const lens = await client.readContract({ address: registry, abi: registryAbi, functionName: "get", args: [name("lens")], }); const router = await client.readContract({ address: registry, abi: registryAbi, functionName: "get", args: [name("router")], }); - One call returns the pool key, its terms (
config), the price, what the next trade pays and the pool's progress: aPoolViewof 50 words, field by field under Contract reference. Check the hook against the registry. An address that is not a launch token makes it revert with no data, so there is no error to decode.TypeScript// tokenView(address token) view returns (PoolView) const view = await client.readContract({ address: lens, abi: lensAbi, functionName: "tokenView", args: [token], }); view.key; // the pool key to trade with view.config.quoteIsCurrency0; // which side the quote asset is view.sqrtPriceX96; // the price now view.effectiveTotalFee; // what the next sale pays all in, in pips view.buyEffectiveTotalFee; // what a buy pays all in now: more inside an anti-snipe window await client.readContract({ address: registry, abi: registryAbi, functionName: "isKnownHook", args: [view.key.hooks], }); // true - Buying the token spends the quote asset, so a buy swaps from the quote's side.
quoteExactInreturns the output after every fee, as any call returns: it runs the swap inside the PoolManager and undoes it, so it changes nothing, but it is not marked view, so make it aneth_call(viem'ssimulateContract). On a Montra pool its quote is exactly what the trade then pays out. A swap the pool or the hook refuses makes the quote revertUnexpectedRevert, carrying the refusal. The amounts areuint128.TypeScript// quoteExactIn(PoolKey key, bool zeroForOne, uint128 amountIn) returns (uint256) const zeroForOne = view.config.quoteIsCurrency0; // a buy; a sell is the opposite const { result: amountOut } = await client.simulateContract({ address: router, abi: routerAbi, functionName: "quoteExactIn", args: [view.key, zeroForOne, amountIn], }); - How a refusal arrives depends on the call, so take each layer only if it is there.
quoteExactInrevertsUnexpectedRevert(bytes reason), selector0xc47bc67c, with the refusal as itsreason. The V4Quoter does the same inUnexpectedRevertBytes(bytes revertData), selector0x6190b2b0.swapExactInadds no layer: the refusal arrives as it is, as it does through the deployed Universal Router, and another router may wrap it in an error of its own. Under that, a pool manager's own refusal is the error itself. A hook's arrives wrapped, as ERC-7751 has it, inWrappedError(address target, bytes4 selector, bytes reason, bytes details), selector0x90bfb865:targetis the hook,selectorthe callback that refused,reasonthe hook's own error anddetailsthe selector ofHookCallFailed(),0xa9e35b2f. An exact-output buy for more than the pool can deliver revertsSafeCastOverflow(), selector0x93dafdf1.UnexpectedRevertis inLaunchRouter.json, the hook's errors inLaunchHook.json, and the rest inPoolManager.json.TypeScript// quoteExactIn: UnexpectedRevert(bytes reason) // V4Quoter: UnexpectedRevertBytes(bytes revertData) // swapExactIn: no layer // WrappedError(address target, bytes4 selector, bytes reason, bytes details) // PartialFillRejected() import { decodeErrorResult } from "viem"; // revertData: the bytes the call reverted with const abi = [...routerAbi, ...poolManagerAbi, ...hookAbi]; let refusal = decodeErrorResult({ abi, data: revertData }); if (refusal.errorName === "UnexpectedRevert" || refusal.errorName === "UnexpectedRevertBytes") { refusal = decodeErrorResult({ abi, data: refusal.args[0] }); } if (refusal.errorName === "WrappedError") { refusal = decodeErrorResult({ abi, data: refusal.args[2] }); } refusal.errorName; // "PartialFillRejected" - One exact-input swap with a minimum and a deadline. A native input is sent as value; an ERC-20 input needs an approval for the router first. Reverts
Expired,ZeroAddress,InvalidRecipient(the router itself asrecipient),IncorrectValueorTooLittleReceived, or with the pool's or the hook's refusal, read as above; emitsSwapped.TypeScript// swapExactIn((PoolKey key, bool zeroForOne, uint128 amountIn, uint128 minAmountOut, address recipient, uint256 deadline) s) // payable returns (uint256 amountOut) const input = zeroForOne ? view.key.currency0 : view.key.currency1; await wallet.writeContract({ address: router, abi: routerAbi, functionName: "swapExactIn", args: [{ key: view.key, zeroForOne, amountIn, minAmountOut: amountOut * 99n / 100n, // 1% slippage recipient: account.address, deadline: BigInt(Math.floor(Date.now() / 1000) + 300), }], value: input === zeroAddress ? amountIn : 0n, });
Trade through the Universal Router
LaunchRouter makes one exact-input swap on one pool. For exact output, or a route through several pools, use the Universal Router the registry names under universalRouter, and the V4Quoter under quoter to quote it. A trade is one execute call: commands has a byte per command and inputs an entry per command. A V4_SWAP command's input is abi.encode(bytes actions, bytes[] params), with a byte per action in actions and an entry per action in params.
Kind
- Command
- Name
V4_SWAP - Byte
0x10 - InputIts actions and their params.
- Command
- Name
SWEEP - Byte
0x04 - Input
(address token, address recipient, uint256 amountMinimum). Pays out all the router holds oftoken, the zero address being the native currency, torecipient, address 1 being the caller. - Action
- Name
SWAP_EXACT_IN_SINGLE - Byte
0x06 - Input
ExactInputSingleParams: one pool, exact input. - Action
- Name
SWAP_EXACT_IN - Byte
0x07 - Input
ExactInputParams: a route, exact input. - Action
- Name
SWAP_EXACT_OUT_SINGLE - Byte
0x08 - Input
ExactOutputSingleParams: one pool, exact output. - Action
- Name
SWAP_EXACT_OUT - Byte
0x09 - Input
ExactOutputParams: a route, exact output. - Action
- Name
SETTLE_ALL - Byte
0x0c - Input
(address currency, uint256 maxAmount). Pays what the swap owes, from the caller: through Permit2, or out of the call's value. - Action
- Name
TAKE_ALL - Byte
0x0f - Input
(address currency, uint256 minAmount). Delivers what the swap made, to the caller.
| Kind | Name | Byte | Input |
|---|---|---|---|
| Command | V4_SWAP | 0x10 | Its actions and their params. |
| Command | SWEEP | 0x04 | (address token, address recipient, uint256 amountMinimum). Pays out all the router holds of token, the zero address being the native currency, to recipient, address 1 being the caller. |
| Action | SWAP_EXACT_IN_SINGLE | 0x06 | ExactInputSingleParams: one pool, exact input. |
| Action | SWAP_EXACT_IN | 0x07 | ExactInputParams: a route, exact input. |
| Action | SWAP_EXACT_OUT_SINGLE | 0x08 | ExactOutputSingleParams: one pool, exact output. |
| Action | SWAP_EXACT_OUT | 0x09 | ExactOutputParams: a route, exact output. |
| Action | SETTLE_ALL | 0x0c | (address currency, uint256 maxAmount). Pays what the swap owes, from the caller: through Permit2, or out of the call's value. |
| Action | TAKE_ALL | 0x0f | (address currency, uint256 minAmount). Delivers what the swap made, to the caller. |
- The structs depend on the router's build. The samples below are what the deployed router requires: a
uint256 minHopPriceX36beforehookDatain a one-pool swap, and auint256[] minHopPriceX36afterpathin a route. The structs in many older samples have neither, and builds of the Universal Router differ, so check the router the registry names before you encode. - How to check.
executetakes bytes, so no ABI shows these structs, but a build's ABI lists its errors:InvalidHopPriceLengthis there on a build that takes the field. Confirm it in the router's source. - The wrong shape does not fail cleanly. Sent to the deployed router, the older struct reverts with no data where
currency0is an ERC-20. Wherecurrency0is native it goes through, so a test on a native-quoted pool proves nothing. - What the field does. It is the least a hop may pay, as
amountOut × 1e36 / amountIn, and a hop under it revertsV4TooLittleReceivedPerHopSingleor, in a route,V4TooLittleReceivedPerHop; on exact output,V4TooMuchRequestedPerHopSingleorV4TooMuchRequestedPerHop. Zero, or an empty list, turns it off; a list has one entry per pool, or the swap revertsInvalidHopPriceLength. The samples turn it off:amountOutMinimumalready holds the whole trade.
- In these samples
universalRouter,permit2andquoterare the registry's addresses, read as under Read, quote and trade, andview,zeroForOneandamountInare as they are there.tradesends oneV4_SWAP. With a native input it adds aSWEEP, so nothing the swap did not spend stays behind.TypeScript// execute(bytes commands, bytes[] inputs, uint256 deadline) payable import { encodeAbiParameters, pad, parseAbi, parseAbiParameters, zeroAddress } from "viem"; const poolKey = "(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey"; const pathKeys = "(address intermediateCurrency, uint24 fee, int24 tickSpacing, address hooks, bytes hookData)[] path"; // the swap actions' params, as the deployed router takes them const exactInSingle = parseAbiParameters(`(${poolKey}, bool zeroForOne, uint128 amountIn, uint128 amountOutMinimum, uint256 minHopPriceX36, bytes hookData)`); const exactIn = parseAbiParameters(`(address currencyIn, ${pathKeys}, uint256[] minHopPriceX36, uint128 amountIn, uint128 amountOutMinimum)`); const exactOutSingle = parseAbiParameters(`(${poolKey}, bool zeroForOne, uint128 amountOut, uint128 amountInMaximum, uint256 minHopPriceX36, bytes hookData)`); const exactOut = parseAbiParameters(`(address currencyOut, ${pathKeys}, uint256[] minHopPriceX36, uint128 amountOut, uint128 amountInMaximum)`); // SETTLE_ALL's and TAKE_ALL's params const currencyAmount = parseAbiParameters("address, uint256"); // one V4_SWAP command's input: its actions, a byte each, and their params const v4Swap = (actions, params) => encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [actions, params]); // a SWEEP of the native currency to the caller, which the router reads from address 1 const sweep = encodeAbiParameters(parseAbiParameters("address, address, uint256"), [zeroAddress, pad("0x01", { size: 20 }), 0n]); const universalRouterAbi = parseAbi(["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"]); const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); const trade = (swap, input, value) => wallet.writeContract({ address: universalRouter, abi: universalRouterAbi, functionName: "execute", args: input === zeroAddress ? ["0x1004", [swap, sweep], deadline] : ["0x10", [swap], deadline], value: input === zeroAddress ? value : 0n, }); - Every ERC-20 the router spends goes through Permit2: the launch token on a sale, and the quote on a buy in an ERC-20. Each token needs two approvals: the token's own
approveto Permit2, then Permit2'sapproveto the router, with an amount and an expiry. Read the second withallowanceand renew it in time: an expired one revertsAllowanceExpired, and one too smallInsufficientAllowance. An expiry of zero lasts only the block. A native input needs neither.TypeScript// approve(address token, address spender, uint160 amount, uint48 expiration) import { erc20Abi, maxUint256 } from "viem"; const permit2Abi = parseAbi([ "function approve(address token, address spender, uint160 amount, uint48 expiration)", "function allowance(address user, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)", ]); const input = zeroForOne ? view.key.currency0 : view.key.currency1; // once a token await wallet.writeContract({ address: input, abi: erc20Abi, functionName: "approve", args: [permit2, maxUint256], }); // again when it runs out or expires: an hour here await wallet.writeContract({ address: permit2, abi: permit2Abi, functionName: "approve", args: [input, universalRouter, amountIn, Math.floor(Date.now() / 1000) + 3600], }); - The V4Quoter's structs have no
minHopPriceX36. It quotes either side and returns the amount and a gas estimate. It is not marked view, so make it aneth_call. What it answers is what the swap then does, and it reverts as under Read a refusal.TypeScript// quoteExactInputSingle(params) returns (uint256 amountOut, uint256 gasEstimate) const quoterAbi = parseAbi([ `function quoteExactInputSingle((${poolKey}, bool zeroForOne, uint128 exactAmount, bytes hookData) params) returns (uint256 amountOut, uint256 gasEstimate)`, `function quoteExactOutputSingle((${poolKey}, bool zeroForOne, uint128 exactAmount, bytes hookData) params) returns (uint256 amountIn, uint256 gasEstimate)`, `function quoteExactInput((address exactCurrency, ${pathKeys}, uint128 exactAmount) params) returns (uint256 amountOut, uint256 gasEstimate)`, `function quoteExactOutput((address exactCurrency, ${pathKeys}, uint128 exactAmount) params) returns (uint256 amountIn, uint256 gasEstimate)`, ]); const { result: [quoted] } = await client.simulateContract({ address: quoter, abi: quoterAbi, functionName: "quoteExactInputSingle", args: [{ poolKey: view.key, zeroForOne, exactAmount: amountIn, hookData: "0x" }], }); - A buy or a sale, as
zeroForOnesays.SETTLE_ALLpays the input andTAKE_ALLdelivers the output, each with its limit. The caller is both the payer and who receives.TypeScriptconst input = zeroForOne ? view.key.currency0 : view.key.currency1; const output = zeroForOne ? view.key.currency1 : view.key.currency0; const minimum = quoted * 99n / 100n; // 1% slippage const swap = v4Swap("0x060c0f", [ // SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL encodeAbiParameters(exactInSingle, [{ poolKey: view.key, zeroForOne, amountIn, amountOutMinimum: minimum, minHopPriceX36: 0n, hookData: "0x", }]), encodeAbiParameters(currencyAmount, [input, amountIn]), encodeAbiParameters(currencyAmount, [output, minimum]), ]); await trade(swap, input, amountIn); pathlists the pools in order, each entry naming the currency its hop ends in. This one buys a token quoted in an ERC-20 with native HYPE: through a plain pool intoquote, then through the launch pool intotoken. A sale runs the same pools the other way: the launch pool's entry first, ending inquote, then the plain pool's, ending in the zero address. The hook's fee is charged once, on the launch pool's hop.TypeScriptconst path = [ { intermediateCurrency: quote, fee: plain.fee, tickSpacing: plain.tickSpacing, hooks: plain.hooks, hookData: "0x" }, { intermediateCurrency: token, fee: view.key.fee, tickSpacing: view.key.tickSpacing, hooks: view.key.hooks, hookData: "0x" }, ]; const { result: [routeQuote] } = await client.simulateContract({ address: quoter, abi: quoterAbi, functionName: "quoteExactInput", args: [{ exactCurrency: zeroAddress, path, exactAmount: amountIn }], }); const minimum = routeQuote * 99n / 100n; // 1% slippage const swap = v4Swap("0x070c0f", [ // SWAP_EXACT_IN, SETTLE_ALL, TAKE_ALL encodeAbiParameters(exactIn, [{ currencyIn: zeroAddress, path, minHopPriceX36: [], amountIn, amountOutMinimum: minimum }]), encodeAbiParameters(currencyAmount, [zeroAddress, amountIn]), encodeAbiParameters(currencyAmount, [token, minimum]), ]); await trade(swap, zeroAddress, amountIn);- You name what you receive and cap what you pay. A native input sends the whole cap as the call's value and pays only what the swap costs. Without a
SWEEP, the surplus stays in the router, and any caller'sSWEEPtakes it. An ERC-20 input is pulled for exactly what the swap costs, so it leaves nothing. A route usesSWAP_EXACT_OUT(0x09) andExactOutputParams, whosepathnames each hop by the currency it starts from.TypeScriptconst input = zeroForOne ? view.key.currency0 : view.key.currency1; const output = zeroForOne ? view.key.currency1 : view.key.currency0; const { result: [cost] } = await client.simulateContract({ address: quoter, abi: quoterAbi, functionName: "quoteExactOutputSingle", args: [{ poolKey: view.key, zeroForOne, exactAmount: wanted, hookData: "0x" }], }); const maximum = cost * 102n / 100n; // 2% slippage const swap = v4Swap("0x080c0f", [ // SWAP_EXACT_OUT_SINGLE, SETTLE_ALL, TAKE_ALL encodeAbiParameters(exactOutSingle, [{ poolKey: view.key, zeroForOne, amountOut: wanted, amountInMaximum: maximum, minHopPriceX36: 0n, hookData: "0x", }]), encodeAbiParameters(currencyAmount, [input, maximum]), encodeAbiParameters(currencyAmount, [output, wanted]), ]); await trade(swap, input, maximum); // a native input sends the cap, and the SWEEP returns the rest
A trade through this router can also revert V4TooLittleReceived, under its minimum; V4TooMuchRequested, over its cap; or TransactionDeadlinePassed. A native input whose value does not cover the swap reverts with no data.
Launch a token
One call to the factory makes the token, its pool, its locked liquidity and the first buy. Whoever calls launch is the creator. A product that sends launches can earn a share of the fees they go on to pay: Builder programme. In these samples factory is the registry's factory, read as above, factoryAbi is LaunchFactory.json, and zeroAddress, zeroHash and parseEventLogs are viem's.
- A token carries its image in two places, and both are fixed at launch, so set both.
metadataURInames a document, the usual place wallets and explorers look;profile.logokeeps the image on the token itself, in the form terminals read throughgetTokenInfo(). The site shows the document's image, and the token'slogowhere there is no document or it names none. These two routes are what the site itself uses, and a script calls them the same way, with no key: the image first, as its raw bytes (PNG, JPEG, WebP or GIF, 2 MB at most, 20 an hour per connection), then a document naming it (name,symbol,description,image, and any ofwebsite,twitter,telegram,discord; 30 an hour). Upload shortly before you launch: anything no launch has named after about an hour may be cleared, and uploading the same file again gives the same URI, so it is safe to repeat. Once the launch is seen, both are pinned to IPFS. Or host your own: anyipfs://orhttps://URI to a JSON document with those fields works, naming anipfs://orhttps://image.TypeScriptimport { readFileSync } from "node:fs"; const api = "https://api.montramarkets.xyz"; const posted = async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body.error ?? r.statusText); return body; }; const image = await fetch(`${api}/api/metadata/image`, { method: "POST", headers: { "content-type": "image/png" }, body: readFileSync("logo.png"), }).then(posted); const metadata = await fetch(`${api}/api/metadata`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: "Example", symbol: "EXMPL", description: "What it is.", image: image.uri, website: "https://example.com", }), }).then(posted); - A launch names a row of the factory's menu by
configId. Ids count from 0,configCount()says how many there are andgetConfig(id)returns a row: one tier for one quote asset, launchable whileenabled. Rows are added and switched off, so read them rather than hard-coding an id. The row picks the hook:getModuleSet(row.moduleSetId).hook, not the newest of the registry'shooks(). That list holds every hook ever published and never loses one, so a pool's hook is checked against it, and a launch's is chosen by its row.TypeScript// ids run from 0 to count - 1: read each, and offer those with row.enabled const count = await client.readContract({ address: factory, abi: factoryAbi, functionName: "configCount" }); const row = await client.readContract({ address: factory, abi: factoryAbi, functionName: "getConfig", args: [configId] }); const { hook } = await client.readContract({ address: factory, abi: factoryAbi, functionName: "getModuleSet", args: [row.moduleSetId], }); LaunchParamshas twelve fields, in the order below; each is in the structs. The launch fee is native and separate: on a native-quoted rowvalueis the fee plus the first buy, and on an ERC-20 row it is the fee alone, with the first buy pulled from the sender, who approves the factory first or passes apermit.firstBuyAmountis the whole first buy, its fee included, andfirstBuyMinOutthe fewest tokens you accept for it, 0 for any: it lands in the launch's own transaction, so nothing can trade ahead of it. There is no pool to quote yet: to show what it will buy,LaunchLens.seedPreview(factory, configId, supply)gives the pool it opens on.TypeScriptconst params = { name: "Example", symbol: "EXMPL", metadataURI: metadata.uri, // the document, with the image in it profile: { logo: image.uri, // the same image, kept on the token description: "What it is.", socials: { telegram: "", twitter: "", discord: "", website: "", extra: "" }, }, configId, supply: 0n, // the row's own salt: "0x…", // any 32 bytes of your own feeRecipients: [], // the creator's own wallet feeShares: [], firstBuyAmount, firstBuyMinOut, permit: { deadline: 0n, v: 0, r: zeroHash, s: zeroHash }, // none }; const launchFee = await client.readContract({ address: factory, abi: factoryAbi, functionName: "launchFee" }); const value = row.quote === zeroAddress ? launchFee + firstBuyAmount : launchFee;TokenLauncheddescribes the launch in full. ItsfirstBuyInisfirstBuyAmount, the swap's fee included, itsfirstBuyOutthe tokens the creator received after that fee, and the launch fee is in neither. A product's builder code goes on asdataSuffix: Attributing a launch.TypeScriptawait client.simulateContract({ address: factory, abi: factoryAbi, functionName: "launch", args: [params], account, value }); const hash = await wallet.writeContract({ address: factory, abi: factoryAbi, functionName: "launch", args: [params], value }); const receipt = await client.waitForTransactionReceipt({ hash }); const [launched] = parseEventLogs({ abi: factoryAbi, eventName: "TokenLaunched", logs: receipt.logs }); const { token, poolId, firstBuyIn, firstBuyOut } = launched.args;
Link to a token
A token's page is https://montramarkets.xyz/token/<address>, the address in lowercase; any other spelling redirects there. A wallet's is /wallet/<address>, and an approved builder's /builders/<code>.
Which fee to show
LaunchLens.feeBreakdown(hook, poolId).total- Show this as the pool's fee. In pips (1,000,000 is 100%), the whole fee a sale pays, and a buy outside an anti-snipe window, the LP fee included once it is on; the same read gives its split.
LaunchLens.poolView(hook, poolId).buyFee- What a buy pays now, the hook's own
currentBuyFeeRate(poolId): the total, or inside the pool's anti-snipe window the higher rate falling to it. Show a buyer its price: a buy pays1 / (1 − buyEffectiveTotalFee / 1e6)times the pool's price, before its move. Montra's site asks before a buy above 10%, which only a window can charge. Showing a window: The anti-snipe fee. LaunchToken.taxBps()- The total in basis points, rounded up, from the token alone, for a scanner that reads only tokens (
sellTaxRate()says the same;buyTaxRate()says what a buy pays now, more than the total inside an anti-snipe window;taxRatePips()gives the total in exact pips). lp,protocol,creator- Where the total goes: the pool's liquidity providers, Montra, and the creator side.
effectiveTotal- The hook's fee and the LP fee compounded, a hair under
totalonce the LP fee is on: what a trade's amounts work out to, for exact arithmetic. LaunchRouter.quoteExactIn- What one trade of a given size delivers after every fee and the price's move: the trade's own figure.
coreProtocolFeeZeroForOne,coreProtocolFeeOneForZero(poolView)- Uniswap's own fee, on top and zero unless Uniswap switches it on: show it beside the total when it is not zero.
Never show Swap.fee or slot0's LP fee, which read zero until the LP line while every trade pays the total, nor the key's 0x800000, which is only the dynamic-fee flag. There is no transfer tax.
The rules a pool keeps
No tier of the menu has a whitelist, per-wallet logic or a trading window: a pool trades from its first block, and the hook sees the router as the sender, never the trader. The fee is netted into the swap's deltas, so standard routers and quoters handle it, and quote exactly what the trade pays.
- Partial fills
- Refused: a swap must fill the whole amount it names, whichever currency it names. One stopped part-way by its price limit, or run out of liquidity, reverts
PartialFillRejected, so no swap can set a price without trading. Set no price limit (the extremes, as V4Router does) and express slippage as a minimum output; a price limit is fill-or-revert, never fill-up-to. - The opening
- A sale may take the price back to the pool's opening and no further: one that would leave it past the opening reverts
PastOpening. No ordinary sale meets it: every swap rounds in the pool's favour, so the tokens outstanding never carry the price back past the opening. - Donations
- Refused (
DonationsLocked), so nothing pays a pool's liquidity outside a trade. A position that others' trades fill pays no hook fee on what it sells or buys that way; the trades that fill it pay theirs. - Liquidity
- Anyone may add; removals have no hook callback and always work. Positions earn the LP fee (Experimental) from the LP line on.
- The fee now
poolConfig(poolId).totalFeePipsis the advertised total, which only ever falls; the hook takes it less the LP fee in force, and the pool charges that LP fee from slot0. Model the next trade withLaunchLens.feeBreakdown(hook, poolId), not slot0 alone: the LP line switches on inside thebeforeSwapof the first trade after the hold, so that trade already pays the LP fee while slot0 still reads zero.- Terms per pool
- Read every pool's terms from the chain (
poolConfig(poolId),feeBreakdown(hook, poolId)) and never hard-code the tiers: a pool keeps the row it launched with, a total its recipient lowered and a protocol share the owner lowered, and a row off the menu is the governor's to publish. The menu is what launches on mainnet are offered. - hookData
- No tier of the menu reads it. Send empty bytes.
- Anti-snipe fee
- A pool's terms may carry one (
poolConfig(poolId).earlyBuyFeeUntilnot zero). From the launch second a buy paysearlyBuyFeeStartPips(at most 99%), falling in a straight line to the total atearlyBuyFeeUntil(at most 30 minutes after launch), the drop rounded down; a sale never pays it, and neither does the factory's own first buy in the launch transaction. Read it ascurrentBuyFeeRate(poolId)at the latest block's time, never the wall clock's: it only falls, so a figure read earlier errs high. The hook takes it in the swap's deltas like the rest of its fee, so v4'sSwapnever shows it: what a buyer paid is theSwap's quote amount plusFeeAccrued'sfee. The formula is under Contract reference; how to read, track and show it, under The anti-snipe fee. - Other routers
- Uniswap's Universal Router (
V4_SWAP), V4Router and V4Quoter trade and quote a Montra pool as any v4 pool, multi-hop included, the hook's fee charged once where the route crosses its pool. The quoter's answer is exactly what the router then trades (the contracts' parity suite holds the two together), as isLaunchRouter.quoteExactIn's on a Montra pool. The registry names their addresses underuniversalRouter,permit2andquoter, andcontractsin/api/configrepeats them, each null until it is set. An exact-output quote needs the V4Quoter:quoteExactInquotes exact input alone. How to encode a trade is under Trade through the Universal Router. - Gas
- Through LaunchRouter about 140-250k for a buy and 140-210k for a sell, by what the trade does: crossing or confirming a line, settling holder rewards mid-stream, moving an ERC-20 quote (measured over 60 trades); a buy inside an anti-snipe window about 3k more. A plain test router on a local chain takes 164k and 173k (the contracts' gas snapshot). Estimate each trade and add a margin.
The quote is the amount you name. The hook takes its fee off it first; the pool swaps the rest and charges its LP fee on that.
fee = amountIn × h / 1e6You name the tokens. The quote is worked out by the swap; the hook adds its fee to what you pay.
fee = quoteIn × h / (1e6 − h)You name the tokens. The quote is what the pool pays out; the hook's fee comes off it.
fee = quoteOut × h / 1e6You name the quote you want. The pool pays out that plus the hook's fee, which the hook keeps.
fee = amountOut × h / (1e6 − h)The fee is always in the quote asset and returned as a hook delta (beforeSwapReturnDelta when the quote is the specified currency, afterSwapReturnDelta when it is not), so the caller's delta already includes it. Exact-output swaps gross the fee up, so the rate is the same in both directions. Every shape fills the whole amount it names or reverts (PartialFillRejected).
| Hook callback | Bit |
|---|---|
beforeInitialize | 1<<13 |
afterInitialize | 1<<12 |
beforeAddLiquidity | 1<<11 |
beforeSwap | 1<<7 |
afterSwap | 1<<6 |
beforeDonate | 1<<5 |
beforeSwapReturnDelta | 1<<3 |
afterSwapReturnDelta | 1<<2 |
v4 reads a hook's permissions from the low 14 bits of its address; this hook's are 0x38EC: nothing on removing liquidity and nothing after a donation. A later hook may carry more, so trust a hook because the factory announced it, not by its bits.
