# Clank Developer > Integrate the Clank protocol: launch tokens, trade on the bonding curve, and trade after graduation. Index: https://developer.clank.trade/llms.txt # Introduction > What the Clank protocol is and what you can build on it. Source: https://developer.clank.trade/ · Markdown: https://developer.clank.trade/index.md

Clank is an AI-native token launch protocol on Robinhood Chain. Every token starts trading on a bonding curve, then graduates to a{" "} locked Uniswap V4 pool once its sale allocation sells out.

These docs are for developers who want to integrate the protocol directly: launch tokens, trade on the bonding curve, and trade in the pool after graduation.

- **Pons V2 compatible** — Existing Pons V2 integrations can reuse their core trading and migration flow with Clank V2.

Retained interfaces

Canonical bonding-curve trades, graduation calls, and core events.

Improved curve math

Maintains the 1 billion token supply through migration and a consistent market cap across graduation.

## What you can build - **Launch tokens** — Launch from your own app or bot. - **Trade the curve** — Buy and sell while a token is in its launch phase. - **Trade after graduation** — Route trades through the token's locked Uniswap V4 pool. - **Index the protocol** — Build launch and trade feeds from protocol events. ## Where to start - New to Clank? Read the token lifecycle. - Ready to write code? Open the quickstart. - Building with a coding model? Give it the Clank docs. - Need an address or ABI? Go to the contract reference. # Brandkit > Clank logos, colors, typography, and usage guidance. Source: https://developer.clank.trade/brandkit/ · Markdown: https://developer.clank.trade/brandkit/index.md Use these assets and tokens when presenting Clank in an integration, article, event, or partner interface. ## Mark
Clank mark
Open SVG Open PNG
Keep clear space around the mark equal to at least one quarter of its diameter. Do not stretch, rotate, redraw, or place it on a background that obscures its shape. ## Lockups
Clank lockup for dark backgrounds
Clank lockup for light backgrounds
Dark-background SVG PNG
Light-background SVG PNG
## Colors - **Clank purple** — Primary accent
`#C4B5FD` - **Canvas** — Primary background
`#131313` - **Foreground** — Primary text
`#EDEDED` - **Surface** — Cards and raised areas
`#181818` Use Clank purple for emphasis, active states, and primary actions. Keep body copy neutral so the accent remains distinctive and readable. ## Typography Clank uses **SuisseIntl** for product UI and brand-facing headings. Use the Book cut for body copy, Medium or Semibold for controls, and Bold sparingly for high-emphasis labels. Use a dedicated monospace face for code and addresses. Recommended fallback: ```css font-family: "SuisseIntl", system-ui, -apple-system, "Segoe UI", sans-serif; ``` ## Naming Write the product name as **Clank** or **clank.trade**. Use **Clank protocol** when referring to the contracts and launch system rather than the website. # Build with AI > Give any AI coding model the Clank documentation and a clear integration brief. Source: https://developer.clank.trade/build-with-ai/ · Markdown: https://developer.clank.trade/build-with-ai/index.md Clank's documentation is available as model-friendly Markdown. Describe what you want to build, copy the prompt below into your preferred coding model, and let it read the current docs before it writes code. ## Give your model this prompt Replace the first placeholder with your app, bot, or integration idea, then copy the whole prompt. > **Works with any model that can read a URL** > > If your model cannot browse the web, download or copy > [`llms-full.txt`](https://developer.clank.trade/llms-full.txt) and attach it > to the conversation with the prompt. ## Choose the right docs feed - **Complete context** — Use [`llms-full.txt`](https://developer.clank.trade/llms-full.txt) when the model should build an entire integration from one document. - **Documentation index** — Use [`llms.txt`](https://developer.clank.trade/llms.txt) when the model can browse links and should load only the pages relevant to the task. Every documentation page also has a Markdown version. Add `/index.md` to a page path—for example, [`/quickstart/index.md`](https://developer.clank.trade/quickstart/index.md)—when the model only needs one focused guide. ## Review before shipping AI-generated transaction code still needs human review. Confirm that it uses the documented chain and current contracts, checks the curve state, applies slippage protection, simulates writes, and handles wallet rejection and reverted transactions. # Fees > Launch fees, trading fees, and how fees are routed. Source: https://developer.clank.trade/concepts/fees/ · Markdown: https://developer.clank.trade/concepts/fees/index.md ## Current Robinhood configuration | Setting | Current value | | --------------------- | -------------------------------------------- | | Factory | `0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2` | | Chain ID | `4663` | | Current Config ID | `0` | | Launch fee | Free | | Bonding-curve fee | **1%** (`100` basis points) | | Uniswap V4 fee | **0.3%** (`3_000` millionths) | | Anti-snipe tax | Disabled | > **Fees are snapshotted** > > If the config is updated, previous curves will have the same config. > Only new curves are affected. ## Bonding curve The curve charges **1%** on buys and sells. Buy fees come out of the pair-token input; sell fees come out of the gross pair-token output. The quote functions already return fee-adjusted amounts: - `quoteBuyFor` returns the launch-token output, fee, and any terminal refund. - `quoteSell` returns the gross pair-token output, net output, and fee. Fees are rounded up to the pair token's smallest unit. See [Trade on the bonding curve](/guides/trade-on-the-bonding-curve) for executable examples. ## Uniswap V4 After graduation, the pool charges **0.3%** per swap. Fees earned by Clank's full-range liquidity position remain attached to its permanently locked position until collected. ## Fee collection Bonding-curve fees accrue in a shared escrow. V4 fees accrue to the locked position. Anyone may trigger collection, but all funds go to the factory's current `feeDestination`; callers cannot redirect them. The factory also requires a zero launch fee, zero creator tax, and no buyback. # Token lifecycle > How a Clank token moves from launch to bonding curve to Uniswap V4 pool. Source: https://developer.clank.trade/concepts/token-lifecycle/ · Markdown: https://developer.clank.trade/concepts/token-lifecycle/index.md Every Clank launch token starts with its full supply in a dedicated bonding curve. Trading moves through a fixed sequence of states before liquidity is created and permanently locked in Uniswap V4. - **Bonding curve** — The curve sells the launch allocation using virtual-reserve XYK pricing and accepts either native ETH or an approved ERC-20 pair token. - **Locked V4 liquidity** — Graduation seeds a full-range Uniswap V4 position whose NFT remains in the protocol locker permanently. ## Launch One factory transaction creates two contracts with deterministic addresses: - the ERC-20 `launchToken`, with its metadata and fixed supply - a dedicated bonding curve that initially holds the complete launch-token supply The creator selects a factory launch configuration and a `pairToken`. The zero address represents native ETH; an ERC-20 must be approved by the factory before it can be used as a pair token. A launch configuration defines the supply, bonding-curve fee, virtual pair-token reserve, graduation threshold, Uniswap V4 fee, and tick spacing. The factory also resolves pair-token-specific economics for approved ERC-20s. These values are hashed into an economics commitment and snapshotted by the new curve, so a later factory configuration change does not alter an existing launch. ## Bonding curve The curve begins in `Trading`. It uses constant-product pricing over effective reserves: tracked real reserves plus virtual reserves that shape the initial price. Virtual reserves affect price but are not assets that can be withdrawn. Buyers provide the pair token and receive launch tokens. Sellers return launch tokens and receive the pair token. Direct donations do not change the tracked reserves used for pricing, and fees are kept outside those pricing reserves. The last buy is allowed to fill only the remaining launch allocation. If the buyer offers more pair tokens than the curve needs, it consumes the required amount and immediately refunds the remainder. See [Trade on the bonding curve](/guides/trade-on-the-bonding-curve) for quote, approval, slippage, buy, and sell examples. ## Graduation Graduation is split into retryable steps. The normal path is permissionless; the caller does not need to be the launch creator or protocol owner. 1. **Trading** Buys and sells are enabled while launch tokens remain in the sale allocation. 2. **Ready** The terminal buy sells the last available launch tokens and permanently closes curve trading. It also attempts the first graduation step, but a failed attempt does not revert the completed buy. 3. **Swept** Anyone can call `graduate(launchToken)` on the factory. After validating that the reserves can seed V4, the factory collects the curve's tracked pair tokens and reserved launch tokens. 4. **PoolCreated** Anyone can call `createGraduatedPool(launchToken)`. The factory initializes the V4 pool, supplies full-range liquidity, locks the position, and records the pool result. | Curve state | Value | Can trade? | Meaning | | ------------- | ----: | :--------: | ------------------------------------------------------------ | | `Trading` | 0 | Yes | Bonding-curve buys and sells are enabled. | | `Ready` | 1 | No | Sale allocation is exhausted and awaits reserve sweeping. | | `Swept` | 2 | No | Reserves are held by the factory for V4 pool creation. | | `PoolCreated` | 3 | In V4 | The graduated pool and locked position exist. | | `Rescued` | 4 | No | Delayed recovery completed after migration could not finish. | > **Use the curve state for routing** > > The factory's compatibility `phase` field does not distinguish `Trading` from > `Ready`. Read `state()` from the curve when deciding whether to quote a curve > trade or wait for graduation. If the ordinary path remains blocked, delayed owner-only recovery becomes available after seven days. A curve stuck in `Ready` can be force-swept after a bounded retry. A launch stuck in `Swept` gets another bounded pool-creation attempt before reserves can move to the recovery owner and the curve enters `Rescued`. These are exceptional recovery paths, not normal routing targets. ## Uniswap V4 pool Pool creation uses the pair token, launch token, static fee, and tick spacing snapshotted at launch. The migration executor initializes the pool at the curve's terminal price and mints a full-range liquidity position directly to the protocol locker. The locker exposes no withdrawal, approval, transfer, or arbitrary-call path for the position NFT. It can collect earned LP fees without removing principal. Any launch-token rounding dust is also transferred to the locker; pair-token rounding dust is routed through the protocol fee escrow. Once the curve reaches `PoolCreated`, integrations should stop calling the bonding curve and route swaps through the graduated V4 pool. See [Trade after graduation](/guides/trade-after-graduation). # Index the protocol > Build a reorg-safe index of Clank launches, trades, and graduations. Source: https://developer.clank.trade/guides/index-the-protocol/ · Markdown: https://developer.clank.trade/guides/index-the-protocol/index.md Start from the canonical Clank factory, then discover each token and bonding curve from its launch event. This keeps the index scoped to contracts created by Clank rather than accepting arbitrary token addresses. ## Discover launches Watch `TokenLaunched` on the factory. Store the indexed `token`, `curve`, and `deployer` together with the quote asset, launch configuration, and graduation threshold carried by the event. The factory on Robinhood Chain (`4663`) is `0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2`. It was deployed at block `69067760`, so backfill from there. | Contract | Event | Use | | --- | --- | --- | | Factory | `TokenLaunched` | Register a token and its curve. | | Curve | `CurveBuy` | Record a bonding-curve buy, fees, and launch tax. | | Curve | `CurveSell` | Record a bonding-curve sell, fees, and launch tax. | | Curve | `StateChanged` | Track the curve state. `Ready` means the sale allocation sold out. | | Curve | `CurveCompleted` | Record reserves moved out of the curve. | | Factory | `LaunchSwept` | Record reserves moved out of the curve. | | Factory | `PoolGraduated` | Mark the Uniswap V4 pool as available. | | PoolManager | `Initialize` | Confirm the graduated pool's ID and opening price. | | PoolManager | `Swap` | Record a trade in the graduated pool. | ## Index trades After discovering a curve, scan it from the `TokenLaunched` block and subscribe to `CurveBuy` and `CurveSell`. Use `(chainId, transactionHash, logIndex)` as the event identity so retries do not create duplicate trades. Keep the buyer or seller separate from the recipient. Smart accounts, routers, and delegated execution can make those addresses different. For a launch made with `launchAndBuy`, `buyer` is the LaunchAndBuy forwarder. Credit that first buy to `recipient` or the transaction sender. `CurveBuy.quoteIn` is what the buyer paid, including `fee` and `tax`. On the final buy it already excludes any `CurveBuyRefunded` refund, so do not subtract the refund again. `CurveSell.quoteOut` is what the seller received after `fee`. The amount that moved the curve's reserves is `quoteIn - fee - tax` for a buy and `quoteOut + fee + tax` for a sell. Quote amounts are in the launch's pair token, so use its `decimals()` rather than assuming 18. ## Track graduation Treat `PoolGraduated` as the authoritative transition to pool trading. `StateChanged` to `Ready` means the bonding-curve allocation sold out. `CurveCompleted` and `LaunchSwept` are emitted together when the reserves leave the curve. From `Ready` until `PoolGraduated`, neither the curve nor the pool accepts trades. Show the token as graduating. The final buy normally sweeps the curve in the same transaction, but pool creation is always a separate transaction: anyone can call `createGraduatedPool(token)` on the factory once the curve is `Swept`. If the curve emits `AutoGraduationFailed`, the sweep did not happen, so call `graduate(token)` first. If the factory emits `LaunchGraduationRescued`, the owner recovered the reserves of a launch that could not graduate. No pool will be created for that token. ## Index pool trades `PoolGraduated` does not include the pool ID. Rebuild the pool key from the launch and hash it. `poolFee`, `tickSpacing`, and `pairToken` come from `getLaunchedToken(launchToken)` on the factory. Read the hook once from `initializationHook()` on the factory. ```ts title="pool-id.ts" import { encodeAbiParameters, keccak256, parseAbiParameters, type Address, } from "viem"; export function poolIdFor( launch: { launchToken: Address; pairToken: Address; poolFee: number; tickSpacing: number; }, hook: Address, ) { // Currencies are sorted by address, so native ETH is always currency0. const [currency0, currency1] = BigInt(launch.pairToken) < BigInt(launch.launchToken) ? [launch.pairToken, launch.launchToken] : [launch.launchToken, launch.pairToken]; return keccak256( encodeAbiParameters( parseAbiParameters( "address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks", ), [currency0, currency1, launch.poolFee, launch.tickSpacing, hook], ), ); } ``` Confirm the ID against the PoolManager `Initialize` event in the `PoolGraduated` transaction. Its `sqrtPriceX96` is the pool's opening price. Then follow `Swap` on the PoolManager, `0x8366a39CC670B4001A1121B8F6A443A643e40951`, filtered by pool ID. `id` is indexed, so one `eth_getLogs` request can cover many pools. Split large ID lists into groups, for example 100 IDs per request. ```ts title="pool-swaps.ts" import { parseAbiItem, type Hex } from "viem"; const swaps = await publicClient.getLogs({ address: "0x8366a39CC670B4001A1121B8F6A443A643e40951", event: parseAbiItem( "event Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee)", ), args: { id: poolIds as Hex[] }, fromBlock, toBlock, }); ``` Read each swap like this: - `amount0` and `amount1` are the swapper's balance changes. Negative was paid in and positive was paid out. A negative launch-token amount is a sell, and a positive one is a buy. - `sender` is the router or contract that called the PoolManager, not the user. Credit the trade to the transaction sender. - `sqrtPriceX96` is the pool price after this swap. See below for how to turn it into a token price. - `fee` is the swap fee in hundredths of a basis point, so `3000` is 0.3%. The Clank hook takes no extra fee, so the amounts are the whole trade. ### Pool price `sqrtPriceX96` encodes how much `currency1` one unit of `currency0` is worth. Two details matter when you convert it into a price for the launch token: - **Order.** Currencies are sorted by address. ETH is always `currency0`, but an ERC-20 pair token can be either one, so check which side the launch token is on. - **Decimals.** Launch tokens always have 18 decimals. Pair tokens may not, so read `decimals()` on the pair token. ETH has 18. ```ts title="pool-price.ts" // Returns the price of one launch token, in pair tokens. export function poolPrice( sqrtPriceX96: bigint, launchTokenIsCurrency0: boolean, pairDecimals: number, ) { // currency1 per currency0, in each token's smallest units const raw = Number(sqrtPriceX96) ** 2 / 2 ** 192; const pairPerLaunch = launchTokenIsCurrency0 ? raw : 1 / raw; return pairPerLaunch * 10 ** (18 - pairDecimals); } ``` For example, for an ETH pair, `poolPrice(sqrtPriceX96, false, 18)` returns the price in ETH. The result is a floating-point number, which is fine for display. Use bigint math if you need exact values. ## Track holders Launch tokens are standard ERC-20s. Build balances from `Transfer` events on each token. Leave protocol addresses out of holder counts and top-holder lists: - the token's curve, which holds the unsold tokens and the pool allocation until the sweep - the factory, which holds reserves between the sweep and pool creation - the PoolManager, which holds the graduated pool's liquidity - the protocol locker, which holds the locked position and leftover token dust. Read `locker()` on the contract returned by the factory's `graduationExecutor()`. ## Handle reorgs Persist the block number and block hash with every checkpoint. Before resuming, verify that the saved block hash is still canonical. On a mismatch, rewind to a previous confirmed checkpoint and replay logs in ascending block and log order. Do not use polling searches as the source of truth. Backfill bounded block ranges, follow new logs, and reconcile both paths through the same idempotent event reducer. # Launch a token > Deploy a launch token and its bonding curve through the Clank factory. Source: https://developer.clank.trade/guides/launch-a-token/ · Markdown: https://developer.clank.trade/guides/launch-a-token/index.md Launch through the canonical factory to create the ERC-20 `launchToken` and its bonding curve in one transaction. You can launch without buying, or use the factory's current forwarder to launch and make an opening buy atomically. - **Launch only** — Call `launchToken` on the factory. No pair-token approval is needed because the launch does not spend the pair token. - **Launch and buy** — Resolve the factory's forwarder and call `launchAndBuy` to create the token and execute its first curve buy in one transaction. ## Current Robinhood deployment | Setting | Current value | | ----------------- | -------------------------------------------- | | Chain ID | `4663` | | Factory | `0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2` | | Launch config ID | `0` | | Native pair token | Zero address | | Launch fee | Free | Read the configuration and forwarder from the factory at runtime. Factory settings can change for future launches, while an existing curve keeps the economics snapshotted when it was created. ## Prerequisites This guide uses [Viem](https://viem.sh/) and assumes your app already has a Robinhood Chain `publicClient` and a connected `walletClient`. The code blocks below are sections of the same `launch-token.ts` module. Choose either **Launch only** or **Launch and buy**; do not execute both with the same parameters and salt. ## Define the launch Use the zero address for a native-ETH `pairToken`, or an ERC-20 address approved by the factory. The current factory requires `creatorTaxBps` to be `0` and `buybackEnabled` to be `false`. ```ts title="launch-token.ts" import { erc20Abi, keccak256, parseAbi, parseEther, parseUnits, toBytes, zeroAddress, type Address, type PublicClient, type WalletClient, } from "viem"; const ROBINHOOD_CHAIN_ID = 4663; const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2"; const LAUNCH_CONFIG_ID = 0n; const factoryAbi = parseAbi([ "function canLaunch(address launcher) view returns (bool)", "function getLaunchConfig(uint256 id) view returns ((uint256 supply,uint256 curveFeeBps,uint256 phantomQuote,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,bool enabled))", "function approvedPairTokens(address pairToken) view returns (bool)", "function previewLaunchEconomics(uint256 launchConfigId,address pairToken) view returns (bytes32)", "function launchFee() view returns (uint256)", "function launchForwarder() view returns (address)", "function predictLaunch(address creator,(string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken) view returns (address launchToken,address curve,bytes32 launchId)", "function launchToken((string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken) payable returns (address launchToken,address curve)", "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)", ]); const launchAndBuyAbi = parseAbi([ "function factory() view returns (address)", "function launchAndBuy((string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken,uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient,address[] snipeTaxExemptions) payable returns (address launchToken,address curve,uint256 launchTokensOut)", ]); const pairToken: Address = zeroAddress; const launchDetails = { name: "Example Token", symbol: "EXAMPLE", logo: "https://example.com/token.png", description: "An example token launched with Clank V2.", socials: { twitter: "", telegram: "", discord: "", website: "https://example.com", farcaster: "", }, creatorTaxBps: 0, buybackEnabled: false, // Change this value before retrying an identical launch. salt: keccak256(toBytes("example-token-v1")), } as const; ``` Metadata limits are measured in UTF-8 bytes, not characters. | Field | Requirement | | ---------------- | ----------------- | | `name` | 1–64 bytes | | `symbol` | 1–16 bytes | | `logo` | 1–256 bytes | | `description` | Up to 1,024 bytes | | Each social link | Up to 256 bytes | | `creatorTaxBps` | Must be `0` | | `buybackEnabled` | Must be `false` | ## Validate the config and predict addresses Read `previewLaunchEconomics` immediately before prediction and pass its result back in `expectedEconomics`. This pins the terms so an intervening factory configuration update makes the transaction revert instead of silently changing the launch. ```ts title="launch-token.ts" const prepareLaunch = async ({ publicClient, walletClient, }: { publicClient: PublicClient; walletClient: WalletClient; }) => { const [[creator], walletChainId, rpcChainId] = await Promise.all([ walletClient.getAddresses(), walletClient.getChainId(), publicClient.getChainId(), ]); if (!creator) throw new Error("Connect a wallet before launching"); if ( walletChainId !== ROBINHOOD_CHAIN_ID || rpcChainId !== ROBINHOOD_CHAIN_ID ) { throw new Error("Connect the wallet and RPC to Robinhood Chain"); } const [config, canLaunch, pairTokenApproved, expectedEconomics, launchFee] = await Promise.all([ publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "getLaunchConfig", args: [LAUNCH_CONFIG_ID], }), publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "canLaunch", args: [creator], }), pairToken === zeroAddress ? Promise.resolve(true) : publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "approvedPairTokens", args: [pairToken], }), publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "previewLaunchEconomics", args: [LAUNCH_CONFIG_ID, pairToken], }), publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "launchFee", }), ]); if (!canLaunch) throw new Error("New launches are currently disabled"); if (!config.enabled) throw new Error("The launch config is disabled"); if (!pairTokenApproved) throw new Error("The pair token is not approved"); const params = { ...launchDetails, creatorFeeRecipient: creator, expectedEconomics, } as const; const [launchToken, curve, launchId] = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "predictLaunch", args: [creator, params, LAUNCH_CONFIG_ID, pairToken], }); const [launchTokenCode, curveCode] = await Promise.all([ publicClient.getCode({ address: launchToken }), publicClient.getCode({ address: curve }), ]); if ( (launchTokenCode && launchTokenCode !== "0x") || (curveCode && curveCode !== "0x") ) { throw new Error("This launch already exists; change the salt"); } return { creator, params, pairToken, launchFee, launchToken, curve, launchId, } as const; }; const launchPlan = await prepareLaunch({ publicClient, walletClient }); ``` `predictLaunch` is deterministic for the creator, metadata, pinned economics, and salt. You can display the addresses before asking the user to sign, but always confirm them from the successful transaction and factory record. ## Launch only Simulate the exact call before opening the wallet. Viem returns the predicted addresses from the simulation, then reuses the validated request for the write. ```ts title="launch-token.ts" const executeLaunch = async ({ publicClient, walletClient, }: { publicClient: PublicClient; walletClient: WalletClient; }) => { const { request, result } = await publicClient.simulateContract({ account: launchPlan.creator, address: CLANK_FACTORY, abi: factoryAbi, functionName: "launchToken", args: [launchPlan.params, LAUNCH_CONFIG_ID, launchPlan.pairToken], value: launchPlan.launchFee, }); const [launchToken, curve] = result; if ( launchToken.toLowerCase() !== launchPlan.launchToken.toLowerCase() || curve.toLowerCase() !== launchPlan.curve.toLowerCase() ) { throw new Error("Simulated launch addresses changed"); } const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error("Launch reverted"); return { launchToken, curve, hash, receipt } as const; }; const launched = await executeLaunch({ publicClient, walletClient }); ``` No ERC-20 approval is necessary for a launch-only transaction. The `pairToken` selects the curve's denomination; the factory does not spend it during launch. ## Verify the launch Read the canonical record after confirmation instead of trusting an address returned by a wallet or external API. ```ts title="launch-token.ts" const launchRecord = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [launched.launchToken], }); if (!launchRecord.exists) throw new Error("Factory launch record not found"); if (launchRecord.curve.toLowerCase() !== launched.curve.toLowerCase()) { throw new Error("Factory returned a different curve"); } ``` The token begins in the curve's `Trading` state. From here, use [Trade on the bonding curve](/guides/trade-on-the-bonding-curve) to quote and execute buys or sells. ## Launch and buy in one transaction The atomic path uses the forwarder currently registered by the factory. The quote below is a read-only simulation with `minLaunchTokensOut` set to zero. It returns the opening buy output without deploying anything. Execution then applies slippage and simulates the protected call again before submission. ```ts title="launch-token.ts" const quoteLaunchAndBuy = async ({ publicClient, pairTokenIn, }: { publicClient: PublicClient; pairTokenIn: bigint; }) => { if (pairTokenIn <= 0n) throw new Error("Enter an opening buy amount"); const launchForwarder = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "launchForwarder", }); if (launchForwarder === zeroAddress) { throw new Error("Launch-and-buy is not configured"); } const forwarderFactory = await publicClient.readContract({ address: launchForwarder, abi: launchAndBuyAbi, functionName: "factory", }); if (forwarderFactory.toLowerCase() !== CLANK_FACTORY.toLowerCase()) { throw new Error("The launch forwarder belongs to another factory"); } const value = launchPlan.launchFee + (launchPlan.pairToken === zeroAddress ? pairTokenIn : 0n); const { result } = await publicClient.simulateContract({ account: launchPlan.creator, address: launchForwarder, abi: launchAndBuyAbi, functionName: "launchAndBuy", args: [ launchPlan.params, LAUNCH_CONFIG_ID, launchPlan.pairToken, pairTokenIn, 0n, launchPlan.creator, [], ], value, }); const [launchToken, curve, launchTokensOut] = result; if ( launchToken.toLowerCase() !== launchPlan.launchToken.toLowerCase() || curve.toLowerCase() !== launchPlan.curve.toLowerCase() ) { throw new Error("Simulated launch addresses changed"); } return { launchForwarder, launchToken, curve, launchTokensOut, pairTokenIn, value, } as const; }; const openingQuote = await quoteLaunchAndBuy({ publicClient, pairTokenIn: parseEther("0.1"), }); ``` Display `openingQuote.launchTokensOut` before asking for confirmation. When the user accepts it, apply their chosen slippage tolerance and execute the exact protected call. ```ts title="launch-token.ts" const executeLaunchAndBuy = async ({ publicClient, walletClient, slippageBps = 100n, }: { publicClient: PublicClient; walletClient: WalletClient; slippageBps?: bigint; }) => { if (slippageBps < 0n || slippageBps > 10_000n) { throw new Error("Slippage must be between 0 and 10,000 basis points"); } const minLaunchTokensOut = (openingQuote.launchTokensOut * (10_000n - slippageBps)) / 10_000n; const { request, result } = await publicClient.simulateContract({ account: launchPlan.creator, address: openingQuote.launchForwarder, abi: launchAndBuyAbi, functionName: "launchAndBuy", args: [ launchPlan.params, LAUNCH_CONFIG_ID, launchPlan.pairToken, openingQuote.pairTokenIn, minLaunchTokensOut, launchPlan.creator, [], ], value: openingQuote.value, }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error("Launch and buy reverted"); const [launchToken, curve, launchTokensOut] = result; return { launchToken, curve, launchTokensOut, hash, receipt } as const; }; const launchedAndBought = await executeLaunchAndBuy({ publicClient, walletClient, }); ``` > **Refresh before signing** > > Configuration can change between the displayed quote and wallet confirmation. > Re-run `prepareLaunch` and the quote when the account, chain, pair token, > launch config, metadata, salt, amount, or slippage changes. The pinned > economics and final simulation make a stale transaction revert rather than > launch with different terms. ## Use an ERC-20 pair token For launch-only, set `pairToken` to an approved ERC-20 and follow the same flow; no approval is needed. For an atomic opening buy, approve the resolved forwarder for `pairTokenIn` before calling `quoteLaunchAndBuy`, because its simulation executes the token transfer. ```ts title="launch-token.ts" const pairTokenDecimals = await publicClient.readContract({ address: pairToken, abi: erc20Abi, functionName: "decimals", }); const pairTokenIn = parseUnits("100", pairTokenDecimals); const launchForwarder = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "launchForwarder", }); const allowance = await publicClient.readContract({ address: pairToken, abi: erc20Abi, functionName: "allowance", args: [launchPlan.creator, launchForwarder], }); if (allowance < pairTokenIn) { const { request } = await publicClient.simulateContract({ account: launchPlan.creator, address: pairToken, abi: erc20Abi, functionName: "approve", args: [launchForwarder, pairTokenIn], }); const approvalHash = await walletClient.writeContract(request); const approvalReceipt = await publicClient.waitForTransactionReceipt({ hash: approvalHash, }); if (approvalReceipt.status !== "success") { throw new Error("Pair-token approval reverted"); } } ``` Then pass `pairTokenIn` to `quoteLaunchAndBuy`. For ERC-20 launches, the atomic call sends only `launchFee` as native value; the forwarder pulls the approved pair tokens from the creator. ## Common launch failures | Error | What to check | | ------------------------- | -------------------------------------------------- | | `InvalidMetadata` | Required fields and UTF-8 byte limits. | | `LaunchEconomicsMismatch` | Refresh the config and `expectedEconomics`. | | `LaunchAlreadyExists` | Change the salt or other committed launch details. | | `LaunchConfigDisabled` | Select an enabled factory config. | | `PairTokenNotApproved` | Use native ETH or a currently approved ERC-20. | | `UnsupportedCreatorTax` | Set `creatorTaxBps` to `0`. | | `UnsupportedBuyback` | Set `buybackEnabled` to `false`. | At creation, the curve receives the full launch-token supply. An atomic opening buy immediately transfers its purchased tokens to the recipient. See [Token lifecycle](/concepts/token-lifecycle) for how the remaining curve supply progresses into permanently locked Uniswap V4 liquidity. # Trade after graduation > Quote and execute swaps in a graduated token's locked Uniswap V4 pool. Source: https://developer.clank.trade/guides/trade-after-graduation/ · Markdown: https://developer.clank.trade/guides/trade-after-graduation/index.md After graduation, curve trading stays closed and liquidity moves to a full-range Uniswap V4 position that is permanently locked by Clank. Route new buys and sells through that V4 pool instead of calling the bonding curve. - **Quote without a wallet** — Resolve the canonical pool key and simulate the V4 Quoter before asking the user to connect or sign. - **Execute after confirmation** — Apply slippage and a deadline, complete any Permit2 approvals, simulate the exact router call, and then submit it. ## Current Robinhood contracts | Contract | Address | | ---------------- | -------------------------------------------- | | Clank factory | `0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2` | | V4 Quoter | `0x8dc178efb8111bb0973dd9d722ebeff267c98f94` | | Universal Router | `0x8876789976decbfcbbbe364623c63652db8c0904` | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | The current graduated pools charge **0.3%**. Read the pool fee, tick spacing, pair token, and hook from the launch record and factory instead of assuming those values for every launch. ## Prerequisites This guide calls the contracts directly with [Viem](https://viem.sh/). It assumes your app already has a Robinhood Chain `publicClient`, a connected `walletClient`, and the `launchToken` address. The code blocks below are sections of the same `graduated-trade.ts` module. Quotes and wallet execution are intentionally separate so an interface can display the expected output before requesting approvals or a swap signature. ## Resolve the graduated pool Read the curve state rather than relying only on the compatibility `phase` field. State `3` is `PoolCreated`; states `1` and `2` are still moving through graduation and cannot trade on either venue. ```ts title="graduated-trade.ts" import { encodeAbiParameters, erc20Abi, maxUint256, parseAbi, parseEther, zeroAddress, type Address, type Hex, type PublicClient, type WalletClient, } from "viem"; const ROBINHOOD_CHAIN_ID = 4663; const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2"; const V4_QUOTER = "0x8dc178efb8111bb0973dd9d722ebeff267c98f94"; const UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904"; const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; const MAX_UINT128 = (1n << 128n) - 1n; const MAX_UINT160 = (1n << 160n) - 1n; const MAX_UINT48 = (1n << 48n) - 1n; const factoryAbi = parseAbi([ "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)", "function initializationHook() view returns (address)", ]); const curveAbi = parseAbi(["function state() view returns (uint8)"]); const quoterAbi = parseAbi([ "function quoteExactInputSingle(((address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks) poolKey,bool zeroForOne,uint128 exactAmount,bytes hookData) params) returns (uint256 amountOut,uint256 gasEstimate)", ]); const universalRouterAbi = parseAbi([ "function execute(bytes commands,bytes[] inputs,uint256 deadline) payable", ]); const permit2Abi = parseAbi([ "function allowance(address user,address token,address spender) view returns (uint160 amount,uint48 expiration,uint48 nonce)", "function approve(address token,address spender,uint160 amount,uint48 expiration)", ]); type PoolKey = { currency0: Address; currency1: Address; fee: number; tickSpacing: number; hooks: Address; }; const resolveGraduatedPool = async ({ publicClient, launchToken, }: { publicClient: PublicClient; launchToken: Address; }) => { const launch = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [launchToken], }); if (!launch.exists) throw new Error("Not a Clank launch token"); const [state, hooks] = await Promise.all([ publicClient.readContract({ address: launch.curve, abi: curveAbi, functionName: "state", }), publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "initializationHook", }), ]); if (state !== 3) throw new Error("The token has not completed graduation"); const launchTokenFirst = BigInt(launchToken) < BigInt(launch.pairToken); const poolKey: PoolKey = { currency0: launchTokenFirst ? launchToken : launch.pairToken, currency1: launchTokenFirst ? launch.pairToken : launchToken, fee: launch.poolFee, tickSpacing: launch.tickSpacing, hooks, }; return { launchToken, curve: launch.curve, pairToken: launch.pairToken, poolKey, } as const; }; const market = await resolveGraduatedPool({ publicClient, launchToken }); ``` The zero address is native ETH in a V4 pool. Currency order is numeric address order, not a fixed “token first” or “pair token first” convention. ## Quote buys and sells The V4 Quoter is called through `simulateContract` because its quote function is not marked `view`. This is still a read-only RPC simulation and does not require a transaction. ```ts title="graduated-trade.ts" type SwapQuote = { side: "buy" | "sell"; amountIn: bigint; amountOut: bigint; minimumAmountOut: bigint; }; const quoteExactInput = async ({ publicClient, trader, inputToken, amountIn, slippageBps, }: { publicClient: PublicClient; trader?: Address; inputToken: Address; amountIn: bigint; slippageBps: bigint; }) => { if (amountIn <= 0n || amountIn > MAX_UINT128) { throw new Error("Input must fit the Uniswap V4 uint128 range"); } if (slippageBps < 0n || slippageBps >= 10_000n) { throw new Error("Slippage must be between 0 and 9,999 basis points"); } const zeroForOne = inputToken.toLowerCase() === market.poolKey.currency0.toLowerCase(); const { result } = await publicClient.simulateContract({ account: trader ?? zeroAddress, address: V4_QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle", args: [ { poolKey: market.poolKey, zeroForOne, exactAmount: amountIn, hookData: "0x", }, ], }); const [amountOut] = result; const minimumAmountOut = (amountOut * (10_000n - slippageBps)) / 10_000n; if (minimumAmountOut > MAX_UINT128) { throw new Error("Minimum output exceeds the router's uint128 range"); } return { amountIn, amountOut, minimumAmountOut } as const; }; const quoteBuy = async ({ publicClient, pairTokenIn, trader, slippageBps = 100n, }: { publicClient: PublicClient; pairTokenIn: bigint; trader?: Address; slippageBps?: bigint; }): Promise => ({ side: "buy", ...(await quoteExactInput({ publicClient, trader, inputToken: market.pairToken, amountIn: pairTokenIn, slippageBps, })), }); const quoteSell = async ({ publicClient, launchTokensIn, trader, slippageBps = 100n, }: { publicClient: PublicClient; launchTokensIn: bigint; trader?: Address; slippageBps?: bigint; }): Promise => ({ side: "sell", ...(await quoteExactInput({ publicClient, trader, inputToken: market.launchToken, amountIn: launchTokensIn, slippageBps, })), }); ``` The returned `amountOut` already reflects the pool's swap fee and price impact; V4 does not return the fee as a separate output. Display `amountOut` and `minimumAmountOut` before requesting wallet confirmation. ### Quote a buy For a native-ETH pool, the buy input uses 18 decimals: ```ts title="graduated-trade.ts" const [trader] = await walletClient.getAddresses(); if (!trader) throw new Error("Connect a wallet before trading"); const buyQuote = await quoteBuy({ publicClient, trader, pairTokenIn: parseEther("0.1"), }); ``` For an ERC-20 pair token, read its decimals and replace `parseEther` with `parseUnits(amount, pairTokenDecimals)`. ### Quote a sell Clank launch tokens use 18 decimals: ```ts title="graduated-trade.ts" const sellQuote = await quoteSell({ publicClient, trader, launchTokensIn: parseEther("1000"), }); ``` ## Approvals and Permit2 The Universal Router spends ERC-20 inputs through Permit2. An ERC-20 input may therefore require two one-time transactions: approve the token for Permit2, then approve the Universal Router inside Permit2. Native-ETH buys need neither. ```ts title="graduated-trade.ts" const approveRouterInput = async ({ publicClient, walletClient, trader, inputToken, amountIn, }: { publicClient: PublicClient; walletClient: WalletClient; trader: Address; inputToken: Address; amountIn: bigint; }) => { if (inputToken === zeroAddress) return; if (amountIn > MAX_UINT160) { throw new Error("Input exceeds the Permit2 uint160 limit"); } const [tokenAllowance, permitAllowance, latestBlock] = await Promise.all([ publicClient.readContract({ address: inputToken, abi: erc20Abi, functionName: "allowance", args: [trader, PERMIT2], }), publicClient.readContract({ address: PERMIT2, abi: permit2Abi, functionName: "allowance", args: [trader, inputToken, UNIVERSAL_ROUTER], }), publicClient.getBlock({ blockTag: "latest" }), ]); if (tokenAllowance < amountIn) { const { request } = await publicClient.simulateContract({ account: trader, address: inputToken, abi: erc20Abi, functionName: "approve", args: [PERMIT2, maxUint256], }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { throw new Error("Permit2 token approval reverted"); } } const [permitAmount, permitExpiration] = permitAllowance; const blockTimestamp = Number(latestBlock.timestamp); if (permitAmount < amountIn || permitExpiration <= blockTimestamp) { const { request } = await publicClient.simulateContract({ account: trader, address: PERMIT2, abi: permit2Abi, functionName: "approve", args: [inputToken, UNIVERSAL_ROUTER, MAX_UINT160, Number(MAX_UINT48)], }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { throw new Error("Universal Router approval reverted"); } } }; ``` ## Encode the Robinhood V4 route Robinhood's deployed Universal Router includes `minHopPriceX36` in its V4 exact-input tuple. Generic V4 examples that omit this field encode incompatible calldata for this router. ```ts title="graduated-trade.ts" const exactInputSingleType = { type: "tuple", components: [ { name: "poolKey", type: "tuple", components: [ { name: "currency0", type: "address" }, { name: "currency1", type: "address" }, { name: "fee", type: "uint24" }, { name: "tickSpacing", type: "int24" }, { name: "hooks", type: "address" }, ], }, { name: "zeroForOne", type: "bool" }, { name: "amountIn", type: "uint128" }, { name: "amountOutMinimum", type: "uint128" }, { name: "minHopPriceX36", type: "uint256" }, { name: "hookData", type: "bytes" }, ], } as const; const currencyAmountTypes = [ { name: "currency", type: "address" }, { name: "amount", type: "uint256" }, ] as const; const encodeExactInputRoute = ({ poolKey, inputToken, amountIn, minimumAmountOut, }: { poolKey: PoolKey; inputToken: Address; amountIn: bigint; minimumAmountOut: bigint; }) => { const zeroForOne = inputToken.toLowerCase() === poolKey.currency0.toLowerCase(); const outputToken = zeroForOne ? poolKey.currency1 : poolKey.currency0; const swap = encodeAbiParameters( [exactInputSingleType], [ { poolKey, zeroForOne, amountIn, amountOutMinimum: minimumAmountOut, minHopPriceX36: 0n, hookData: "0x", }, ] ); const settleAll = encodeAbiParameters(currencyAmountTypes, [ inputToken, amountIn, ]); const takeAll = encodeAbiParameters(currencyAmountTypes, [ outputToken, minimumAmountOut, ]); const input = encodeAbiParameters( [ { name: "actions", type: "bytes" }, { name: "params", type: "bytes[]" }, ], ["0x060c0f", [swap, settleAll, takeAll]] ); return { commands: "0x10" as Hex, inputs: [input] as const }; }; ``` `0x10` executes a V4 swap. The action sequence `0x060c0f` performs an exact-input single swap, settles the entire input, and takes the output back to the caller. ## Execute a buy or sell Execution consumes a previously displayed quote. It handles the input approval, builds a short deadline from the latest chain time, and simulates the exact router call before opening the wallet. ```ts title="graduated-trade.ts" const executeSwap = async ({ publicClient, walletClient, trader, quote, }: { publicClient: PublicClient; walletClient: WalletClient; trader: Address; quote: SwapQuote; }) => { const [walletAddresses, walletChainId, rpcChainId] = await Promise.all([ walletClient.getAddresses(), walletClient.getChainId(), publicClient.getChainId(), ]); if (walletAddresses[0]?.toLowerCase() !== trader.toLowerCase()) { throw new Error("The connected wallet account changed"); } if ( walletChainId !== ROBINHOOD_CHAIN_ID || rpcChainId !== ROBINHOOD_CHAIN_ID ) { throw new Error("Connect the wallet and RPC to Robinhood Chain"); } const inputToken = quote.side === "buy" ? market.pairToken : market.launchToken; await approveRouterInput({ publicClient, walletClient, trader, inputToken, amountIn: quote.amountIn, }); const route = encodeExactInputRoute({ poolKey: market.poolKey, inputToken, amountIn: quote.amountIn, minimumAmountOut: quote.minimumAmountOut, }); const latestBlock = await publicClient.getBlock({ blockTag: "latest" }); const wallTime = BigInt(Math.floor(Date.now() / 1_000)); const currentTime = latestBlock.timestamp > wallTime ? latestBlock.timestamp : wallTime; const deadline = currentTime + 60n; const value = inputToken === zeroAddress ? quote.amountIn : 0n; const { request } = await publicClient.simulateContract({ account: trader, address: UNIVERSAL_ROUTER, abi: universalRouterAbi, functionName: "execute", args: [route.commands, [...route.inputs], deadline], value, }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error("Swap reverted"); return { hash, receipt } as const; }; ``` ### Buy ```ts title="graduated-trade.ts" const buyResult = await executeSwap({ publicClient, walletClient, trader, quote: buyQuote, }); ``` A native-ETH buy sends `buyQuote.amountIn` as transaction value. An ERC-20 buy uses the confirmed token and Permit2 approvals instead. ### Sell ```ts title="graduated-trade.ts" const sellResult = await executeSwap({ publicClient, walletClient, trader, quote: sellQuote, }); ``` Every sell uses the launch token as its ERC-20 input, so the first sell normally needs both approval steps. The output is native ETH when `pairToken` is the zero address, otherwise it is the configured ERC-20 pair token. > **Quotes are snapshots** > > Pool price and liquidity may change before confirmation. Refresh the quote > when the amount, side, slippage, account, chain, or token changes. The minimum > output and deadline protect execution, but your interface should still mark an > older quote as stale. ## Routing summary | Curve state | Value | Route trades to | | ------------- | ----: | ----------------------------------- | | `Trading` | 0 | Clank bonding curve | | `Ready` | 1 | Nowhere; wait for reserve sweeping | | `Swept` | 2 | Nowhere; wait for V4 pool creation | | `PoolCreated` | 3 | The launch's locked Uniswap V4 pool | | `Rescued` | 4 | No automatic venue | See [Token lifecycle](/concepts/token-lifecycle) for graduation mechanics and [Fees](/concepts/fees) for the current curve and V4 fee configuration. # Trade on the bonding curve > Quote, buy, and sell a launch token while it trades on its bonding curve. Source: https://developer.clank.trade/guides/trade-on-the-bonding-curve/ · Markdown: https://developer.clank.trade/guides/trade-on-the-bonding-curve/index.md Every Clank launch token starts on a bonding curve. While the curve is in its `Trading` state, buyers exchange the configured pair token for launch tokens and sellers exchange launch tokens back for that pair token. The pair token is either native ETH or an approved ERC-20. This guide calls the contracts directly with Viem. It assumes your app already has a `publicClient`, a connected `walletClient`, and the launch-token address. See the [quickstart](/quickstart) for a smaller native-ETH buy example. The examples below are sections of the same `bonding-curve.ts` module. - **Quote without a wallet** — Read the curve and show expected outputs, fees, and refunds before asking the user to connect or sign. - **Execute after confirmation** — Apply slippage, handle approval, simulate, and submit only after the user accepts the displayed quote. - **Pons V2 compatible** — Existing Pons V2 integrations can keep their core execution and migration flow while adopting Clank V2's richer reads.

Reusable flow

`buy`, `sell`, `getReserves`, `graduate`, `createGraduatedPool`, and their core events retain the canonical interfaces.

Richer reads

`quoteBuyFor`, `quoteSell`, and `state`, used below, are Clank V2 additions.

Improved curve math

Maintains the 1 billion token supply through migration and a consistent market cap across graduation.

## Resolve the curve and check its state On Robinhood Chain, resolve the curve through the canonical factory instead of accepting an untrusted curve address. Then read `state()` from that curve. Only state `0` (`Trading`) accepts buys and sells. ```ts title="bonding-curve.ts" import { erc20Abi, parseAbi, zeroAddress, type Address, type PublicClient, type WalletClient, } from "viem"; const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2" as const; const factoryAbi = parseAbi([ "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)", ]); const curveAbi = parseAbi([ "function state() view returns (uint8)", "function quoteBuyFor(address recipient,uint256 grossPairTokenIn) view returns (uint256 grossPairTokenUsed,uint256 netPairTokenIn,uint256 fee,uint256 launchTokensOut,uint256 pairTokenRefund)", "function quoteSell(uint256 launchTokensIn) view returns (uint256 grossPairTokenOut,uint256 netPairTokenOut,uint256 fee)", "function buy(uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient) payable returns (uint256 launchTokensOut)", "function sell(uint256 launchTokensIn,uint256 minPairTokenOut,address recipient) returns (uint256 pairTokenOut)", ]); const getTradingCurve = async ( publicClient: PublicClient, launchToken: Address ) => { const launch = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [launchToken], }); if (!launch.exists) throw new Error("Not a Clank launch token"); const state = await publicClient.readContract({ address: launch.curve, abi: curveAbi, functionName: "state", }); if (state !== 0) throw new Error("The bonding curve is no longer trading"); return { curve: launch.curve, pairToken: launch.pairToken, } as const; }; ``` The complete curve lifecycle is: | State | Value | Trading behavior | | ------------- | ----: | ------------------------------------------------------------ | | `Trading` | 0 | Buys and sells are enabled. | | `Ready` | 1 | The sale allocation is exhausted; curve trading is closed. | | `Swept` | 2 | Reserves have moved to the factory for graduation. | | `PoolCreated` | 3 | Trade through the graduated Uniswap V4 pool. | | `Rescued` | 4 | Graduation recovery completed; curve trading remains closed. | > **Read the curve state** > > Do not use the factory's `phase` field for this check. It is a > graduation-compatibility field and does not distinguish `Trading` from > `Ready`. ## Quote a buy Keep quoting separate from execution so your app can display the expected output, fees, and refund before asking for a wallet signature. `quoteBuyFor` accepts the total amount of pair tokens the buyer is offering, including fees. Quote for the actual launch-token recipient: during the launch window, the total fee can depend on whether that recipient is exempt from the decaying anti-snipe tax. ```ts title="bonding-curve.ts" const quoteBondingCurveBuy = async ({ publicClient, launchToken, recipient, requestedPairTokenIn, }: { publicClient: PublicClient; launchToken: Address; recipient: Address; requestedPairTokenIn: bigint; }) => { const { curve, pairToken } = await getTradingCurve(publicClient, launchToken); const [pairTokenUsed, netPairTokenIn, fee, launchTokensOut, pairTokenRefund] = await publicClient.readContract({ address: curve, abi: curveAbi, functionName: "quoteBuyFor", args: [recipient, requestedPairTokenIn], }); return { curve, pairToken, recipient, requestedPairTokenIn, pairTokenUsed, netPairTokenIn, fee, launchTokensOut, pairTokenRefund, } as const; }; type BondingCurveBuyQuote = Awaited>; ``` Call `quoteBondingCurveBuy` whenever the input or recipient changes and render fields such as `launchTokensOut`, `fee`, and `pairTokenRefund`. ## Buy Pass the displayed quote unchanged to `buyOnBondingCurve` only after the user confirms it. The function handles a required pair-token approval before submitting the buy. ```ts title="bonding-curve.ts" const BPS = 10_000n; const minimumOutput = (amount: bigint, slippageBps: bigint) => (amount * (BPS - slippageBps)) / BPS; const buyOnBondingCurve = async ({ publicClient, walletClient, quote, slippageBps = 100n, }: { publicClient: PublicClient; walletClient: WalletClient; quote: BondingCurveBuyQuote; slippageBps?: bigint; }) => { const [trader] = await walletClient.getAddresses(); if (!trader) throw new Error("Connect a wallet"); if (slippageBps < 0n || slippageBps >= BPS) { throw new Error("Invalid slippage"); } const minimumLaunchTokensOut = minimumOutput( quote.launchTokensOut, slippageBps ); // A terminal buy can use only part of the offered input and refund the rest. // Scale the contract argument so its proportional partial-fill check still // enforces minimumLaunchTokensOut for the amount that is actually used. const minimumForOfferedAmount = quote.pairTokenUsed === quote.requestedPairTokenIn ? minimumLaunchTokensOut : (minimumLaunchTokensOut * quote.requestedPairTokenIn + quote.pairTokenUsed - 1n) / quote.pairTokenUsed; if (quote.pairToken !== zeroAddress) { const allowance = await publicClient.readContract({ address: quote.pairToken, abi: erc20Abi, functionName: "allowance", args: [trader, quote.curve], }); if (allowance < quote.requestedPairTokenIn) { const { request } = await publicClient.simulateContract({ account: trader, address: quote.pairToken, abi: erc20Abi, functionName: "approve", args: [quote.curve, quote.requestedPairTokenIn], }); const hash = await walletClient.writeContract(request); await publicClient.waitForTransactionReceipt({ hash }); } } const { request } = await publicClient.simulateContract({ account: trader, address: quote.curve, abi: curveAbi, functionName: "buy", args: [ quote.requestedPairTokenIn, minimumForOfferedAmount, quote.recipient, ], value: quote.pairToken === zeroAddress ? quote.requestedPairTokenIn : 0n, }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error("Buy reverted"); return { hash, receipt } as const; }; ``` > **Pair-token handling** > > For a native-ETH curve, `msg.value` must exactly equal `requestedPairTokenIn`. > For an ERC-20 curve, approve that pair token for the curve and send no native > value. A terminal buy may consume less than the offered amount; the curve > returns the unused `pairTokenRefund` in the same pair token. The final buy can also move the curve out of `Trading`. Re-resolve and re-quote after any failed simulation instead of submitting a stale transaction. ## Quote a sell `quoteSell` returns the gross pair-token amount removed from the curve, the net pair-token amount paid to the recipient, and the fee. Return those amounts without mixing display data with execution-time slippage. ```ts title="bonding-curve.ts" const quoteBondingCurveSell = async ({ publicClient, launchToken, launchTokensIn, }: { publicClient: PublicClient; launchToken: Address; launchTokensIn: bigint; }) => { const { curve, pairToken } = await getTradingCurve(publicClient, launchToken); const [grossPairTokenOut, netPairTokenOut, fee] = await publicClient.readContract({ address: curve, abi: curveAbi, functionName: "quoteSell", args: [launchTokensIn], }); return { curve, pairToken, launchToken, launchTokensIn, grossPairTokenOut, netPairTokenOut, fee, } as const; }; type BondingCurveSellQuote = Awaited>; ``` Call `quoteBondingCurveSell` whenever the launch-token input changes. Display `netPairTokenOut`, `fee`, and any other quote fields your interface needs. ## Sell A sell needs an allowance for the launch token. Pass the confirmed sell quote to `sellOnBondingCurve`; the function approves the curve when necessary before submitting the sell. ```ts title="bonding-curve.ts" const sellOnBondingCurve = async ({ publicClient, walletClient, quote, slippageBps = 100n, }: { publicClient: PublicClient; walletClient: WalletClient; quote: BondingCurveSellQuote; slippageBps?: bigint; }) => { const [trader] = await walletClient.getAddresses(); if (!trader) throw new Error("Connect a wallet"); if (slippageBps < 0n || slippageBps >= BPS) { throw new Error("Invalid slippage"); } const minimumPairTokenOut = minimumOutput(quote.netPairTokenOut, slippageBps); const allowance = await publicClient.readContract({ address: quote.launchToken, abi: erc20Abi, functionName: "allowance", args: [trader, quote.curve], }); if (allowance < quote.launchTokensIn) { const { request } = await publicClient.simulateContract({ account: trader, address: quote.launchToken, abi: erc20Abi, functionName: "approve", args: [quote.curve, quote.launchTokensIn], }); const hash = await walletClient.writeContract(request); await publicClient.waitForTransactionReceipt({ hash }); } const { request } = await publicClient.simulateContract({ account: trader, address: quote.curve, abi: curveAbi, functionName: "sell", args: [quote.launchTokensIn, minimumPairTokenOut, trader], }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error("Sell reverted"); return { hash, receipt } as const; }; ``` The curve pays native ETH when `quote.pairToken` is the zero address; otherwise it pays that ERC-20. No pair-token approval is needed for a sell. ## Slippage, fees, and transaction safety > **Quotes are snapshots** > > Always simulate immediately before requesting a wallet signature. Another > trade can move the curve or complete it first; request a fresh quote after a > failed simulation. - Slippage is expressed in basis points: `100n` is 1%. Apply it to `launchTokensOut` for a buy and `netPairTokenOut` for a sell. - Buy quotes include the bonding fee and any recipient-specific anti-snipe tax. Use `quoteBuyFor`, not `quoteBuy`, when the caller and recipient can differ. - Wait for an approval receipt before submitting the trade that depends on it. A submitted transaction hash alone does not mean the allowance is available. - Treat all amounts as raw integer units. Use `parseEther` only for native ETH or an 18-decimal asset; use `parseUnits` with the ERC-20's actual decimals. - Set a finite slippage limit. Passing zero as the minimum output removes price protection. Once `state()` returns `3`, route new trades through the launch token's Uniswap V4 pool. See [Trade after graduation](/guides/trade-after-graduation). # Quickstart > Quote and execute your first bonding-curve trade without installing a Clank package. Source: https://developer.clank.trade/quickstart/ · Markdown: https://developer.clank.trade/quickstart/index.md ## Install Viem Install Viem in your application if it is not already a dependency. ```sh npm install viem pnpm add viem yarn add viem bun add viem ``` ## Prerequisites This example assumes your app already has: - a Viem `publicClient` connected to the token's chain - a Viem `walletClient` connected to the trader's wallet - the Clank token address you want to trade ## Quote and execute a buy Use the copy button on this block and replace `token` with the Clank token address. The example resolves its curve through the canonical factory before quoting the buy. No Clank package is required. ```ts title="buy-on-bonding-curve.ts" import { parseAbi, parseEther, type Address } from "viem"; // Trade inputs — edit these values. const tradeInputs = { token: "0x..." as Address, // token you get after swap amountIn: parseEther("0.0001"), // buy token with 0.0001 ETH slippageBps: 100n, // 1% } as const; const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2" as const; const [trader] = await walletClient.getAddresses(); const { token, amountIn, slippageBps } = tradeInputs; const factoryAbi = parseAbi([ "function getLaunchedToken(address token) view returns ((address token,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptQuote,uint256 sweptTokens,uint256 sweptAt,bool exists) launched)", ]); const curveAbi = parseAbi([ "function state() view returns (uint8)", "function quoteBuyFor(address recipient,uint256 grossQuoteIn) view returns (uint256 grossQuoteUsed,uint256 netQuoteIn,uint256 fee,uint256 tokensOut,uint256 refund)", "function buy(uint256 quoteIn,uint256 minTokensOut,address recipient) payable returns (uint256 tokensOut)", ]); const launch = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token], }); if (!launch.exists) throw new Error("Not a Clank token"); const curve = launch.curve; const state = await publicClient.readContract({ address: curve, abi: curveAbi, functionName: "state", }); if (state !== 0) throw new Error("Bonding curve is closed"); const [, , , tokensOut] = await publicClient.readContract({ address: curve, abi: curveAbi, functionName: "quoteBuyFor", args: [trader, amountIn], }); const minimumTokensOut = (tokensOut * (10_000n - slippageBps)) / 10_000n; const { request } = await publicClient.simulateContract({ account: trader, address: curve, abi: curveAbi, functionName: "buy", args: [amountIn, minimumTokensOut, trader], value: amountIn, }); const hash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash }); ``` `getLaunchedToken` returns the curve recorded for the token by the current Clank factory on Robinhood Chain. `quoteBuyFor` then includes recipient-specific launch tax in the quote. The example applies 1% slippage protection, simulates the exact transaction, and only after a successful simulation opens the wallet confirmation. This direct call applies while the token is trading on its bonding curve. After graduation, route the trade through the token's locked Uniswap V4 pool instead. # Contracts > Public Clank V2 contracts, interfaces, and events for integrations. Source: https://developer.clank.trade/reference/contracts/ · Markdown: https://developer.clank.trade/reference/contracts/index.md Clank V2 is deployed on Robinhood Chain. Start from the canonical factory and discover each launch token, bonding curve, and graduated pool through factory reads and events. - **Canonical factory** — `0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2` - **Current launch config** — Robinhood Chain ID `4663`, launch config ID `0`. > **Use the V2 factory** > > Resolve launches through the factory above. Older Clank deployments may still > contain active tokens, but new V2 launches and current configuration reads use > this address. ## Public Clank V2 addresses These are the fixed entry points an integration may call directly. Protocol operations contracts are intentionally omitted because apps do not need to interact with them. The zero address, `0x0000000000000000000000000000000000000000`, represents native ETH when used as a `pairToken`. | Contract | Address | Role | | ------------------------ | -------------------------------------------- | ----------------------------------------------------------------------- | | Launch factory | `0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2` | Creates launches, stores canonical records, and coordinates graduation. | | Launch-and-buy forwarder | `0xac20d0d5cc0F89ee4f6d5f8E6870a36EAF2E0dD7` | Atomically launches a token and executes its opening curve buy. | The launch-and-buy address should still be read from `launchForwarder()` before use. The owner can rotate that forwarder without replacing the factory. ## Robinhood Uniswap contracts | Contract | Address | | ---------------- | -------------------------------------------- | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | | WETH | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` | | V4 PoolManager | `0x8366a39CC670B4001A1121B8F6A443A643e40951` | | Universal Router | `0x8876789976decbfcbbbe364623c63652db8c0904` | | V4 Quoter | `0x8dc178efb8111bb0973dd9d722ebeff267c98f94` | Robinhood's Universal Router uses the extended V4 exact-input tuple documented in [Trade after graduation](/guides/trade-after-graduation). Do not substitute a generic router ABI that omits `minHopPriceX36`. ## Per-launch contracts Every launch creates two deterministic contracts. Their addresses are not listed globally because they depend on the creator, metadata, economics, and salt. | Contract | Discovery | Role | | -------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `ClankLauncherToken` | `TokenLaunched.token` or `getLaunchedToken(launchToken)` | Burnable 18-decimal ERC-20 with immutable factory, curve, and deployer references. | | `ClankBondingCurve` | `TokenLaunched.curve` or `getLaunchedToken(launchToken).curve` | Quotes and executes pre-graduation buys and sells, then tracks migration state. | | Uniswap V4 pool | Build its `PoolKey` from the launch record and `initializationHook()` | Executes swaps after curve state `PoolCreated`. | The V4 pool does not have a standalone contract address. Its identity is the hash of its currencies, fee, tick spacing, and hook inside the singleton PoolManager. ## Canonical discovery Do not accept a curve address supplied by an untrusted client. Resolve it from the factory and verify the bidirectional references when indexing or handling a high-value transaction. ```ts title="resolve-clank-launch.ts" import { parseAbi, type Address, type PublicClient } from "viem"; const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2"; const factoryDiscoveryAbi = parseAbi([ "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)", "function initializationHook() view returns (address)", ]); const tokenDiscoveryAbi = parseAbi([ "function launchFactory() view returns (address)", "function curve() view returns (address)", ]); const curveDiscoveryAbi = parseAbi([ "function factory() view returns (address)", "function token() view returns (address)", "function state() view returns (uint8)", ]); export const resolveClankLaunch = async ({ publicClient, launchToken, }: { publicClient: PublicClient; launchToken: Address; }) => { const launch = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryDiscoveryAbi, functionName: "getLaunchedToken", args: [launchToken], }); if (!launch.exists) throw new Error("Not a Clank V2 launch token"); const [tokenFactory, tokenCurve, curveFactory, curveToken, state, hook] = await Promise.all([ publicClient.readContract({ address: launchToken, abi: tokenDiscoveryAbi, functionName: "launchFactory", }), publicClient.readContract({ address: launchToken, abi: tokenDiscoveryAbi, functionName: "curve", }), publicClient.readContract({ address: launch.curve, abi: curveDiscoveryAbi, functionName: "factory", }), publicClient.readContract({ address: launch.curve, abi: curveDiscoveryAbi, functionName: "token", }), publicClient.readContract({ address: launch.curve, abi: curveDiscoveryAbi, functionName: "state", }), publicClient.readContract({ address: CLANK_FACTORY, abi: factoryDiscoveryAbi, functionName: "initializationHook", }), ]); const matches = tokenFactory.toLowerCase() === CLANK_FACTORY.toLowerCase() && curveFactory.toLowerCase() === CLANK_FACTORY.toLowerCase() && tokenCurve.toLowerCase() === launch.curve.toLowerCase() && curveToken.toLowerCase() === launchToken.toLowerCase(); if (!matches) throw new Error("Clank launch references do not match"); return { ...launch, state, hook } as const; }; ``` ## Factory interface ### Reads | Function | Returns or purpose | | ----------------------------------------------- | ------------------------------------------------------------------------------------- | | `launchEnabled()` | Global gate for new launches. Existing trading and graduation continue when disabled. | | `canLaunch(address)` | Whether an account may launch through the public gate. | | `launchConfigCount()` | Number of zero-indexed launch configurations. | | `getLaunchConfig(id)` | Supply, curve fee, native economics, V4 fee, tick spacing, and enabled flag. | | `approvedPairTokens(pairToken)` | Whether an ERC-20 may be used for new launches. | | `pairTokenEconomics(pairToken)` | ERC-20 virtual reserve, graduation threshold, and expected decimals. | | `previewLaunchEconomics(id, pairToken)` | Commitment hash used to pin launch economics. | | `predictLaunch(creator, params, id, pairToken)` | Predicted token, curve, and launch ID. | | `getLaunchedToken(launchToken)` | Compact Pons V2-compatible launch record. | | `getLaunch(launchToken)` | Full Clank launch record, including snapshotted economics and graduation result. | | `launchForwarder()` | Current trusted atomic launch-and-buy contract. | | `initializationHook()` / `memeHook()` | Clank and Pons-compatible getters for the V4 hook. | | `launchFee()` | Native launch fee; currently zero. | ### Public writes | Function | Purpose | | ------------------------------------------------ | --------------------------------------------------------------- | | `launchToken(params, id, pairToken)` | Create a token and curve. | | `launchToken(params, id, pairToken, exemptions)` | Create a launch with additional snipe-tax exemptions. | | `graduate(launchToken)` | Permissionlessly move a `Ready` curve to `Swept`. | | `createGraduatedPool(launchToken)` | Permissionlessly create and lock the pool for a `Swept` launch. | Configuration and protocol operations are managed by Clank. A regular integration should only request signatures for the public launch, trading, and graduation methods documented here. ## Bonding-curve interface | Function | Purpose | | -------------------------------------------------- | --------------------------------------------------------------------------------- | | `state()` | Exact lifecycle state used for routing. | | `token()` / `pairToken()` / `factory()` | Immutable launch references. | | `feeBps()` | Snapshotted curve fee in basis points. | | `currentSnipeTaxBps(recipient)` | Current recipient-specific total buy tax. | | `getReserves()` | Effective pair-token and launch-token reserves used for pricing. | | `realPairTokenReserve()` | Not exposed under this name; use `realQuoteReserve()` in the Pons-compatible ABI. | | `realQuoteReserve()` / `realTokenReserve()` | Tracked real reserves, excluding virtual reserves and donations. | | `quoteBuyFor(recipient, pairTokenIn)` | Buy quote including recipient-specific tax and a possible terminal refund. | | `quoteSell(launchTokensIn)` | Gross pair-token output, net output, and fee. | | `buy(pairTokenIn, minLaunchTokensOut, recipient)` | Execute a curve buy. Native launches require matching `msg.value`. | | `sell(launchTokensIn, minPairTokenOut, recipient)` | Execute a curve sell after approving the curve for launch tokens. | The ABI retains `quote` names such as `realQuoteReserve` for Pons V2 compatibility. The guides use `pairToken` terminology for the asset itself. ### Curve state | State | Value | Meaning | | ------------- | ----: | ------------------------------------------------------- | | `Trading` | `0` | Curve buys and sells are enabled. | | `Ready` | `1` | Sale allocation is exhausted; reserves await sweeping. | | `Swept` | `2` | Factory holds reserves for pool creation. | | `PoolCreated` | `3` | Trading routes through Uniswap V4. | | `Rescued` | `4` | Delayed recovery completed; no automatic venue remains. | > **Curve state and compatibility phase differ** > > `getLaunchedToken().phase` uses the compact Pons-compatible mapping: `0` > covers both `Trading` and `Ready`, `1` is `Swept`, `2` is `PoolCreated`, and > `3` is `Rescued`. Read `state()` from the curve for routing decisions. ## Launch-token interface Every launch token implements the standard ERC-20 and ERC-20 burnable methods. It uses 18 decimals and has no owner, mint function, transfer tax, pause, proxy, or upgrade path. | Function | Purpose | | -------------------------------------------- | ------------------------------------------- | | `deployer()` | Original launch creator. | | `launchFactory()` | Factory that created the launch. | | `curve()` | Token's dedicated bonding curve. | | `logo()` / `description()` / `socials()` | Onchain launch metadata. | | `getTokenInfo()` | Pons-compatible creator and metadata tuple. | | `burn(amount)` / `burnFrom(account, amount)` | Standard ERC-20 burnable functions. | The initial fixed supply is minted entirely to the curve. Burning is voluntary and reduces total supply; there is no method to mint replacement tokens. ## Launch-and-buy interface `launchAndBuy(params, configId, pairToken, pairTokenIn, minLaunchTokensOut, recipient, exemptions)` creates the launch and executes its first curve buy atomically. - Native launches send `launchFee + pairTokenIn` as `msg.value`. - ERC-20 launches approve the current forwarder for `pairTokenIn` and send only `launchFee` as native value. - The forwarder refunds any terminal overpayment to the launcher. - It appends the opening-buy recipient to the snipe-tax exemption list. Resolve the address with `launchForwarder()` and verify its `factory()` before approval. See [Launch a token](/guides/launch-a-token) for a complete flow. ## Integration ABI Use focused ABIs for the methods your integration calls. These fragments cover the main launch, trade, graduation, and indexing surfaces. ```ts title="clank-abi.ts" import { parseAbi } from "viem"; export const clankFactoryAbi = parseAbi([ "function launchEnabled() view returns (bool)", "function canLaunch(address launcher) view returns (bool)", "function launchConfigCount() view returns (uint256)", "function getLaunchConfig(uint256 id) view returns ((uint256 supply,uint256 curveFeeBps,uint256 phantomQuote,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,bool enabled))", "function approvedPairTokens(address pairToken) view returns (bool)", "function pairTokenEconomics(address pairToken) view returns (uint256 phantomQuote,uint256 graduationThreshold,uint8 decimals)", "function previewLaunchEconomics(uint256 launchConfigId,address pairToken) view returns (bytes32)", "function predictLaunch(address creator,(string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken) view returns (address launchToken,address curve,bytes32 launchId)", "function launchToken((string name,string symbol,string logo,string description,(string twitter,string telegram,string discord,string website,string farcaster) socials,address creatorFeeRecipient,uint16 creatorTaxBps,bool buybackEnabled,bytes32 expectedEconomics,bytes32 salt) params,uint256 launchConfigId,address pairToken) payable returns (address launchToken,address curve)", "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)", "function launchForwarder() view returns (address)", "function initializationHook() view returns (address)", "function graduate(address launchToken)", "function createGraduatedPool(address launchToken) returns (uint256 positionId)", ]); export const clankCurveAbi = parseAbi([ "function factory() view returns (address)", "function token() view returns (address)", "function pairToken() view returns (address)", "function state() view returns (uint8)", "function feeBps() view returns (uint256)", "function getReserves() view returns (uint256 pairTokenReserve,uint256 launchTokenReserve)", "function realQuoteReserve() view returns (uint256)", "function realTokenReserve() view returns (uint256)", "function quoteBuyFor(address recipient,uint256 grossPairTokenIn) view returns (uint256 grossPairTokenUsed,uint256 netPairTokenIn,uint256 fee,uint256 launchTokensOut,uint256 pairTokenRefund)", "function quoteSell(uint256 launchTokensIn) view returns (uint256 grossPairTokenOut,uint256 netPairTokenOut,uint256 fee)", "function buy(uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient) payable returns (uint256 launchTokensOut)", "function sell(uint256 launchTokensIn,uint256 minPairTokenOut,address recipient) returns (uint256 pairTokenOut)", ]); export const clankTokenAbi = parseAbi([ "function name() view returns (string)", "function symbol() view returns (string)", "function decimals() view returns (uint8)", "function totalSupply() view returns (uint256)", "function balanceOf(address account) view returns (uint256)", "function allowance(address owner,address spender) view returns (uint256)", "function approve(address spender,uint256 amount) returns (bool)", "function transfer(address recipient,uint256 amount) returns (bool)", "function burn(uint256 amount)", "function deployer() view returns (address)", "function launchFactory() view returns (address)", "function curve() view returns (address)", "function logo() view returns (string)", "function description() view returns (string)", "function socials() view returns (string twitter,string telegram,string discord,string website,string farcaster)", ]); ``` ## Events ### Factory | Event | Use | | -------------------------------------------------------- | ------------------------------------------------------------------------- | | `TokenLaunched` | Discover the token, curve, creator, pair token, config ID, and threshold. | | `LaunchSwept` | Record reserves moved from a `Ready` curve into the factory. | | `PoolGraduated` | Mark the V4 pool as created and store its position and seeded amounts. | | `LaunchForceSwept` | Record exceptional delayed reserve sweeping. | | `LaunchGraduationRescued` | Record exceptional delayed recovery. | | `LaunchConfigAdded` / `LaunchConfigUpdated` | Refresh factory configuration for future launches. | | `LaunchConfigEnabledUpdated` / `LaunchEnabledUpdated` | Refresh launch availability. | | `PairTokenApprovalUpdated` / `PairTokenEconomicsUpdated` | Refresh pair-token support and economics. | | `LaunchForwarderSet` | Refresh the atomic launch-and-buy target. | ### Curve | Event | Use | | ---------------------- | ------------------------------------------------------------------------------ | | `CurveBuy` | Record buyer, recipient, pair-token input, launch-token output, fee, and tax. | | `CurveBuyRefunded` | Record pair tokens returned by a terminal buy. | | `CurveSell` | Record seller, recipient, launch-token input, pair-token output, fee, and tax. | | `StateChanged` | Track the exact lifecycle transition. | | `CurveCompleted` | Record reserve transfer out of the curve. | | `AutoGraduationFailed` | Queue a permissionless graduation retry. | ### Launch-and-buy forwarder | Event | Use | | ---------- | ----------------------------------------------------------------------------------- | | `Launched` | Record the atomic launch recipient, launcher, requested spend, and received tokens. | Use `(chainId, transactionHash, logIndex)` as an event's durable identity and handle chain reorgs. See [Index the protocol](/guides/index-the-protocol) for the recommended indexing flow. # Errors > The errors you are most likely to hit, what they mean and how to fix them. Source: https://developer.clank.trade/reference/errors/ · Markdown: https://developer.clank.trade/reference/errors/index.md These are the errors people hit most. Viem shows an error's name only if the error is in the ABI you call with. Add lines like `"error SlippageExceeded(uint256 actual, uint256 minimum)"` to your ABIs to get readable errors. ## Launching | Error | Meaning | Fix | | --- | --- | --- | | `InvalidMetadata()` | A required field is empty, or a field is too long. | Check the metadata limits in [Launch a token](/guides/launch-a-token). Lengths are in bytes. | | `LaunchFeeNotPaid()` | You sent ETH to `launchToken`. | Send `value: 0`. To buy at launch, use [`launchAndBuy`](/guides/launch-a-token#launch-and-buy-in-one-transaction). | | `LaunchEconomicsMismatch(bytes32 expected, bytes32 actual)` | The launch settings changed after you read `expectedEconomics`. | Read `previewLaunchEconomics` again and retry. | | `UnsupportedCreatorTax()` | `creatorTaxBps` isn't `0`. | Set it to `0`. | | `UnsupportedBuyback()` | `buybackEnabled` is `true`. | Set it to `false`. | | `PairTokenNotApproved()` | The pair token isn't allowed. | Use ETH, or check `approvedPairTokens(pairToken)` on the factory. | | `InvalidLaunchConfigId()` / `LaunchConfigDisabled()` | Wrong or turned-off launch config. | Use `0`. | | `NotWhitelisted()` | Launches are paused. | Check `launchEnabled()` on the factory and try later. | | `ZeroAddress()` | An address argument is zero, like `recipient` or `creatorFeeRecipient`. | Pass a real address. | | `NativeValueMismatch(uint256 sent, uint256 expected)` | `launchAndBuy` got a `value` that doesn't match the buy amount. | For ETH, send `value` equal to the buy amount. For ERC-20, send `0`. | | `LaunchAlreadyExists()` | A token with the same creator, params and salt already exists. | Change the `salt`. | ## Trading on the curve | Error | Meaning | Fix | | --- | --- | --- | | `CurveGraduated()` | The curve stopped trading. It sold out and is graduating or has graduated. | Trade in the [Uniswap V4 pool](/guides/trade-after-graduation) once it's live. | | `SlippageExceeded(uint256 actual, uint256 minimum)` | The price moved and you'd get less than your minimum. | Get a new quote and retry. | | `NativeValueMismatch(uint256 supplied, uint256 expected)` | `value` doesn't equal `quoteIn`. | Send exactly `quoteIn`. | | `UnexpectedNativeValue()` | You sent ETH to a curve that uses an ERC-20 pair token. | Send `value: 0` and approve the curve instead. | | `ZeroAmount()` | The amount is `0`, or nothing is left after fees. | Use a bigger amount. | | `ZeroOutput()` | The trade is too small to get anything back. | Use a bigger amount. | | `ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)` | The curve can't take your tokens. | `approve` the curve before selling. | ## Graduation | Error | Meaning | Fix | | --- | --- | --- | | `InvalidState(uint8 expected, uint8 actual)` | The curve isn't in the state this call needs. | Read `state()` on the curve. Call `graduate` in state `1` (`Ready`) and `createGraduatedPool` in state `2` (`Swept`). See [Token lifecycle](/concepts/token-lifecycle). | ## Uniswap V4 swaps If a swap through the Universal Router reverts with no clear reason, check these first: - Did you include the extra `minHopPriceX36` field in the route? See [Trade after graduation](/guides/trade-after-graduation). - Is the pool key right? `hooks` must be the Clank hook, and ETH must be `currency0`. - For sells, did you do both [Permit2 approvals](/guides/trade-after-graduation#approvals-and-permit2)? - Is the `deadline` in the future? # TypeScript client > Copy one self-contained Viem client for Clank bonding-curve and Uniswap V4 trades. Source: https://developer.clank.trade/reference/typescript-client/ · Markdown: https://developer.clank.trade/reference/typescript-client/index.md Copy the file below into your application. It resolves the launch from the canonical factory, selects the bonding curve or graduated Uniswap V4 pool, and returns unsigned transaction instructions for your wallet flow. There is no Clank package to install. The file depends only on Viem. > **Quotes and wallet actions stay separate** > > `quoteBuy` and `quoteSell` only read or simulate. `buildBuy` and `buildSell` > return the approvals and trade in submission order without opening a wallet or > sending a transaction. ## Copy the client ```ts title="clank-client.ts" import { encodeAbiParameters, encodeFunctionData, erc20Abi, maxUint256, parseAbi, zeroAddress, type Address, type Hex, type PublicClient, } from "viem"; const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2" as const; const V4_QUOTER = "0x8dc178efb8111bb0973dd9d722ebeff267c98f94" as const; const UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904" as const; const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3" as const; const BPS = 10_000n; const MAX_UINT128 = (1n << 128n) - 1n; const MAX_UINT160 = (1n << 160n) - 1n; const MAX_UINT48 = (1n << 48n) - 1n; const factoryAbi = parseAbi([ "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)", "function initializationHook() view returns (address)", ]); const curveAbi = parseAbi([ "function state() view returns (uint8)", "function quoteBuyFor(address recipient,uint256 grossPairTokenIn) view returns (uint256 grossPairTokenUsed,uint256 netPairTokenIn,uint256 fee,uint256 launchTokensOut,uint256 pairTokenRefund)", "function quoteSell(uint256 launchTokensIn) view returns (uint256 grossPairTokenOut,uint256 netPairTokenOut,uint256 fee)", "function buy(uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient) payable returns (uint256 launchTokensOut)", "function sell(uint256 launchTokensIn,uint256 minPairTokenOut,address recipient) returns (uint256 pairTokenOut)", ]); const quoterAbi = parseAbi([ "function quoteExactInputSingle(((address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks) poolKey,bool zeroForOne,uint128 exactAmount,bytes hookData) params) returns (uint256 amountOut,uint256 gasEstimate)", ]); const routerAbi = parseAbi([ "function execute(bytes commands,bytes[] inputs,uint256 deadline) payable", ]); const permit2Abi = parseAbi([ "function allowance(address user,address token,address spender) view returns (uint160 amount,uint48 expiration,uint48 nonce)", "function approve(address token,address spender,uint160 amount,uint48 expiration)", ]); export type ClankTradeVenue = "bonding-curve" | "uniswap-v4"; export type ClankTradeSide = "buy" | "sell"; export type ClankTradeQuote = { readonly venue: ClankTradeVenue; readonly side: ClankTradeSide; readonly launchToken: Address; readonly pairToken: Address; readonly requestedAmountIn: bigint; readonly amountIn: bigint; readonly amountOut: bigint; readonly minimumAmountOut: bigint; readonly fee: bigint | null; readonly refund: bigint; readonly recipient: Address | null; }; export type ClankTradeInstruction = { readonly label: string; readonly to: Address; readonly data: Hex; readonly value?: bigint; }; export type ClankTradeClient = { readonly venue: ClankTradeVenue; readonly launchToken: Address; readonly pairToken: Address; quoteBuy(options: { readonly pairTokenIn: bigint; readonly recipient: Address; readonly slippageBps?: number; }): Promise; buildBuy(options: { readonly quote: ClankTradeQuote; readonly trader: Address; readonly recipient?: Address; readonly deadline?: bigint; }): Promise; quoteSell(options: { readonly launchTokensIn: bigint; readonly trader?: Address; readonly slippageBps?: number; }): Promise; buildSell(options: { readonly quote: ClankTradeQuote; readonly trader: Address; readonly recipient?: Address; readonly deadline?: bigint; }): Promise; }; type PoolKey = { readonly currency0: Address; readonly currency1: Address; readonly fee: number; readonly tickSpacing: number; readonly hooks: Address; }; type Market = { readonly launchToken: Address; readonly pairToken: Address; readonly curve: Address; readonly state: number; readonly poolFee: number; readonly tickSpacing: number; }; const minimumOutput = (amount: bigint, slippageBps = 100): bigint => { if (slippageBps < 0 || slippageBps >= Number(BPS)) { throw new Error("Slippage must be between 0 and 9,999 basis points"); } return (amount * (BPS - BigInt(slippageBps))) / BPS; }; const requirePositiveInput = (amount: bigint) => { if (amount <= 0n) throw new Error("Input amount must be positive"); }; const requireQuote = ({ quote, venue, side, launchToken, }: { readonly quote: ClankTradeQuote; readonly venue: ClankTradeVenue; readonly side: ClankTradeSide; readonly launchToken: Address; }) => { if ( quote.venue !== venue || quote.side !== side || quote.launchToken.toLowerCase() !== launchToken.toLowerCase() ) { throw new Error(`Expected a ${venue} ${side} quote for this launch`); } }; const resolveMarket = async ({ publicClient, launchToken, }: { readonly publicClient: PublicClient; readonly launchToken: Address; }): Promise => { const launch = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [launchToken], }); if (!launch.exists) throw new Error("Not a Clank V2 launch token"); const state = await publicClient.readContract({ address: launch.curve, abi: curveAbi, functionName: "state", }); return { launchToken, pairToken: launch.pairToken, curve: launch.curve, state, poolFee: launch.poolFee, tickSpacing: launch.tickSpacing, }; }; const buildCurveClient = ({ publicClient, market, }: { readonly publicClient: PublicClient; readonly market: Market; }): ClankTradeClient => ({ venue: "bonding-curve", launchToken: market.launchToken, pairToken: market.pairToken, async quoteBuy({ pairTokenIn, recipient, slippageBps = 100 }) { requirePositiveInput(pairTokenIn); const [amountIn, , fee, amountOut, refund] = await publicClient.readContract({ address: market.curve, abi: curveAbi, functionName: "quoteBuyFor", args: [recipient, pairTokenIn], }); if (amountIn <= 0n || amountOut <= 0n) { throw new Error("The bonding curve returned no buy output"); } return { venue: "bonding-curve", side: "buy", launchToken: market.launchToken, pairToken: market.pairToken, requestedAmountIn: pairTokenIn, amountIn, amountOut, minimumAmountOut: minimumOutput(amountOut, slippageBps), fee, refund, recipient, }; }, async buildBuy({ quote, trader, recipient = trader }) { requireQuote({ quote, venue: "bonding-curve", side: "buy", launchToken: market.launchToken, }); if (quote.recipient?.toLowerCase() !== recipient.toLowerCase()) { throw new Error("Buy recipient differs from the quoted recipient"); } const minimumForOfferedAmount = quote.amountIn === quote.requestedAmountIn ? quote.minimumAmountOut : (quote.minimumAmountOut * quote.requestedAmountIn + quote.amountIn - 1n) / quote.amountIn; const approvals = market.pairToken === zeroAddress ? [] : await buildErc20Approval({ publicClient, owner: trader, token: market.pairToken, spender: market.curve, amount: quote.requestedAmountIn, label: "Approve pair token", }); return [ ...approvals, { label: "Buy on bonding curve", to: market.curve, data: encodeFunctionData({ abi: curveAbi, functionName: "buy", args: [quote.requestedAmountIn, minimumForOfferedAmount, recipient], }), ...(market.pairToken === zeroAddress ? { value: quote.requestedAmountIn } : {}), }, ]; }, async quoteSell({ launchTokensIn, slippageBps = 100 }) { requirePositiveInput(launchTokensIn); const [, amountOut, fee] = await publicClient.readContract({ address: market.curve, abi: curveAbi, functionName: "quoteSell", args: [launchTokensIn], }); return { venue: "bonding-curve", side: "sell", launchToken: market.launchToken, pairToken: market.pairToken, requestedAmountIn: launchTokensIn, amountIn: launchTokensIn, amountOut, minimumAmountOut: minimumOutput(amountOut, slippageBps), fee, refund: 0n, recipient: null, }; }, async buildSell({ quote, trader, recipient = trader }) { requireQuote({ quote, venue: "bonding-curve", side: "sell", launchToken: market.launchToken, }); const approvals = await buildErc20Approval({ publicClient, owner: trader, token: market.launchToken, spender: market.curve, amount: quote.amountIn, label: "Approve launch token", }); return [ ...approvals, { label: "Sell on bonding curve", to: market.curve, data: encodeFunctionData({ abi: curveAbi, functionName: "sell", args: [quote.amountIn, quote.minimumAmountOut, recipient], }), }, ]; }, }); const buildErc20Approval = async ({ publicClient, owner, token, spender, amount, label, }: { readonly publicClient: PublicClient; readonly owner: Address; readonly token: Address; readonly spender: Address; readonly amount: bigint; readonly label: string; }): Promise => { const allowance = await publicClient.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [owner, spender], }); return allowance >= amount ? [] : [ { label, to: token, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amount], }), }, ]; }; const exactInputSingleType = { type: "tuple", components: [ { name: "poolKey", type: "tuple", components: [ { name: "currency0", type: "address" }, { name: "currency1", type: "address" }, { name: "fee", type: "uint24" }, { name: "tickSpacing", type: "int24" }, { name: "hooks", type: "address" }, ], }, { name: "zeroForOne", type: "bool" }, { name: "amountIn", type: "uint128" }, { name: "amountOutMinimum", type: "uint128" }, { name: "minHopPriceX36", type: "uint256" }, { name: "hookData", type: "bytes" }, ], } as const; const currencyAmountTypes = [ { name: "currency", type: "address" }, { name: "amount", type: "uint256" }, ] as const; const encodeV4Route = ({ poolKey, inputToken, amountIn, minimumAmountOut, }: { readonly poolKey: PoolKey; readonly inputToken: Address; readonly amountIn: bigint; readonly minimumAmountOut: bigint; }) => { const zeroForOne = inputToken.toLowerCase() === poolKey.currency0.toLowerCase(); const swap = encodeAbiParameters( [exactInputSingleType], [ { poolKey, zeroForOne, amountIn, amountOutMinimum: minimumAmountOut, minHopPriceX36: 0n, hookData: "0x", }, ] ); const outputToken = zeroForOne ? poolKey.currency1 : poolKey.currency0; const settleAll = encodeAbiParameters(currencyAmountTypes, [ inputToken, amountIn, ]); const takeAll = encodeAbiParameters(currencyAmountTypes, [ outputToken, minimumAmountOut, ]); const input = encodeAbiParameters( [ { name: "actions", type: "bytes" }, { name: "params", type: "bytes[]" }, ], ["0x060c0f", [swap, settleAll, takeAll]] ); return { commands: "0x10" as Hex, inputs: [input] as const }; }; const quoteV4 = async ({ publicClient, poolKey, inputToken, amountIn, account, slippageBps, }: { readonly publicClient: PublicClient; readonly poolKey: PoolKey; readonly inputToken: Address; readonly amountIn: bigint; readonly account: Address; readonly slippageBps: number; }) => { requirePositiveInput(amountIn); if (amountIn > MAX_UINT128) { throw new Error("Input exceeds the Uniswap V4 uint128 limit"); } const zeroForOne = inputToken.toLowerCase() === poolKey.currency0.toLowerCase(); const { result } = await publicClient.simulateContract({ account, address: V4_QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle", args: [ { poolKey, zeroForOne, exactAmount: amountIn, hookData: "0x", }, ], }); const [amountOut] = result; const minimumAmountOut = minimumOutput(amountOut, slippageBps); if (minimumAmountOut > MAX_UINT128) { throw new Error("Minimum output exceeds the Uniswap V4 uint128 limit"); } return { amountOut, minimumAmountOut, } as const; }; const buildPermit2Approvals = async ({ publicClient, trader, token, amount, }: { readonly publicClient: PublicClient; readonly trader: Address; readonly token: Address; readonly amount: bigint; }): Promise => { if (token === zeroAddress) return []; if (amount > MAX_UINT160) { throw new Error("Input exceeds the Permit2 uint160 limit"); } const [tokenAllowance, permitAllowance, block] = await Promise.all([ publicClient.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [trader, PERMIT2], }), publicClient.readContract({ address: PERMIT2, abi: permit2Abi, functionName: "allowance", args: [trader, token, UNIVERSAL_ROUTER], }), publicClient.getBlock({ blockTag: "latest" }), ]); const tokenApproval = tokenAllowance >= amount ? [] : [ { label: "Approve Permit2", to: token, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [PERMIT2, maxUint256], }), }, ]; const [permitAmount, permitExpiration] = permitAllowance; const routerApproval = permitAmount >= amount && permitExpiration > Number(block.timestamp) ? [] : [ { label: "Approve Universal Router", to: PERMIT2, data: encodeFunctionData({ abi: permit2Abi, functionName: "approve", args: [token, UNIVERSAL_ROUTER, MAX_UINT160, Number(MAX_UINT48)], }), }, ]; return [...tokenApproval, ...routerApproval]; }; const resolveDeadline = async ( publicClient: PublicClient, deadline?: bigint ): Promise => { if (deadline !== undefined) return deadline; const block = await publicClient.getBlock({ blockTag: "latest" }); return block.timestamp + 60n; }; const buildV4Client = ({ publicClient, market, poolKey, }: { readonly publicClient: PublicClient; readonly market: Market; readonly poolKey: PoolKey; }): ClankTradeClient => { const buildSwap = async ({ quote, trader, recipient, deadline, side, }: { readonly quote: ClankTradeQuote; readonly trader: Address; readonly recipient: Address; readonly deadline: bigint | undefined; readonly side: ClankTradeSide; }): Promise => { requireQuote({ quote, venue: "uniswap-v4", side, launchToken: market.launchToken, }); if ( side === "buy" && quote.recipient?.toLowerCase() !== recipient.toLowerCase() ) { throw new Error("Buy recipient differs from the quoted recipient"); } if (recipient.toLowerCase() !== trader.toLowerCase()) { throw new Error("Uniswap V4 output recipient must be the trader"); } const inputToken = side === "buy" ? market.pairToken : market.launchToken; const [approvals, transactionDeadline] = await Promise.all([ buildPermit2Approvals({ publicClient, trader, token: inputToken, amount: quote.amountIn, }), resolveDeadline(publicClient, deadline), ]); const route = encodeV4Route({ poolKey, inputToken, amountIn: quote.amountIn, minimumAmountOut: quote.minimumAmountOut, }); return [ ...approvals, { label: side === "buy" ? "Buy on Uniswap V4" : "Sell on Uniswap V4", to: UNIVERSAL_ROUTER, data: encodeFunctionData({ abi: routerAbi, functionName: "execute", args: [route.commands, [...route.inputs], transactionDeadline], }), ...(inputToken === zeroAddress ? { value: quote.amountIn } : {}), }, ]; }; return { venue: "uniswap-v4", launchToken: market.launchToken, pairToken: market.pairToken, async quoteBuy({ pairTokenIn, recipient, slippageBps = 100 }) { const { amountOut, minimumAmountOut } = await quoteV4({ publicClient, poolKey, inputToken: market.pairToken, amountIn: pairTokenIn, account: recipient, slippageBps, }); return { venue: "uniswap-v4", side: "buy", launchToken: market.launchToken, pairToken: market.pairToken, requestedAmountIn: pairTokenIn, amountIn: pairTokenIn, amountOut, minimumAmountOut, fee: null, refund: 0n, recipient, }; }, async buildBuy({ quote, trader, recipient = trader, deadline }) { return buildSwap({ quote, trader, recipient, deadline, side: "buy", }); }, async quoteSell({ launchTokensIn, trader = zeroAddress, slippageBps = 100, }) { const { amountOut, minimumAmountOut } = await quoteV4({ publicClient, poolKey, inputToken: market.launchToken, amountIn: launchTokensIn, account: trader, slippageBps, }); return { venue: "uniswap-v4", side: "sell", launchToken: market.launchToken, pairToken: market.pairToken, requestedAmountIn: launchTokensIn, amountIn: launchTokensIn, amountOut, minimumAmountOut, fee: null, refund: 0n, recipient: null, }; }, async buildSell({ quote, trader, recipient = trader, deadline }) { return buildSwap({ quote, trader, recipient, deadline, side: "sell", }); }, }; }; export const createClankClient = async ({ publicClient, launchToken, }: { readonly publicClient: PublicClient; readonly launchToken: Address; }): Promise => { const market = await resolveMarket({ publicClient, launchToken }); if (market.state === 0) { return buildCurveClient({ publicClient, market }); } if (market.state === 3) { const hooks = await publicClient.readContract({ address: CLANK_FACTORY, abi: factoryAbi, functionName: "initializationHook", }); const launchTokenFirst = BigInt(launchToken) < BigInt(market.pairToken); const poolKey: PoolKey = { currency0: launchTokenFirst ? launchToken : market.pairToken, currency1: launchTokenFirst ? market.pairToken : launchToken, fee: market.poolFee, tickSpacing: market.tickSpacing, hooks, }; return buildV4Client({ publicClient, market, poolKey }); } if (market.state === 1 || market.state === 2) { throw new Error("This launch is graduating; try again shortly"); } if (market.state === 4) { throw new Error("Trading is unavailable for this launch"); } throw new Error(`Unknown Clank curve state: ${market.state}`); }; ``` ## Create client `createClankClient` verifies the launch through the factory and reads the exact curve state. It returns the same client interface for both trading venues. ```ts import { createClankClient } from "./clank-client"; const clank = await createClankClient({ publicClient, launchToken, }); console.log(clank.venue); // "bonding-curve" or "uniswap-v4" ``` States `1` and `2` throw a temporary graduation error. State `4` throws because there is no automatic trading venue. ## Quote buy `quoteBuy` accepts the amount of `pairToken` offered and the final `launchToken` recipient. It applies 1% slippage by default without requesting a wallet signature. ```ts const buyQuote = await clank.quoteBuy({ pairTokenIn: 100000000000000000n, recipient: trader, slippageBps: 100, }); ``` Display `amountOut`, `minimumAmountOut`, `fee`, and `refund`. The bonding curve returns its fee separately; graduated V4 quotes return `fee: null` because the pool fee is already reflected in `amountOut`. ## Build buy `buildBuy` returns zero or more approval instructions followed by the buy. It does not submit them. ```ts const buyInstructions = await clank.buildBuy({ quote: buyQuote, trader, }); ``` Native-ETH buys need no approval. ERC-20 curve buys may add one approval; graduated buys may add the required ERC-20 and Permit2 approvals. ## Quote sell `quoteSell` accepts an amount of `launchToken`. Passing `trader` gives the V4 Quoter the same simulation account that will execute the swap. ```ts const sellQuote = await clank.quoteSell({ launchTokensIn: 1000000000000000000000n, trader, slippageBps: 100, }); ``` Display the quote before asking for wallet access. A new quote is required when the input, slippage, or trading venue changes. ## Build sell `buildSell` returns the launch-token approval path and the sell instruction in submission order. ```ts const sellInstructions = await clank.buildSell({ quote: sellQuote, trader, }); ``` Simulate and submit each instruction sequentially, waiting for its successful receipt before continuing. See the execution sections in [Trade on the bonding curve](/guides/trade-on-the-bonding-curve) and [Trade after graduation](/guides/trade-after-graduation) for the wallet loop. > **Recreate after graduation** > > A client represents the venue resolved when `createClankClient` runs. Recreate > it after a graduation event or after any stale simulation so the next quote > uses the current venue.