# Otomate developer documentation (full text) Source: https://www.otomate.fun/docs/developers. API reference: https://www.otomate.fun/docs/kit/openapi.v1.json. # Build on Otomate Every Otomate launch is a plain ERC-20 token and its own Uniswap v4 pool. The pool's hook-owned liquidity is the launch curve, so the token trades from its first block through standard routers, on **Ink (chain ID 57073)** and **Robinhood Chain (chain ID 4663)**. This guide is for bots, trading terminals, wallets, data providers, AI agents and anyone who wants to run their own launchpad on Otomate. | You want to… | Use | Start with | | --- | --- | --- | | Follow launches and trades as they happen | The real-time feed (SSE or WebSocket) | [Real-time feed](/docs/developers#real-time-feed) | | Read a token, its market, trades, candles, holders | The public API v1 | [API v1](/docs/developers#api-v1) | | Quote and trade from your wallet | The SDK, or the API's swap transaction | [Trade launch pools](/docs/developers#trade-launch-pools) | | Create tokens from code | The SDK's `createToken`, or the factory directly | [Create tokens](/docs/developers#create-tokens) | | Earn a fee on the trades you send | Builder codes | [Builder codes](/docs/developers#builder-codes) | | Index every launch yourself | Contract events and the indexing checklist | [Data providers](/docs/developers#data-providers) | | Let an AI agent do it | The MCP server and `llms.txt` | [AI agents](/docs/developers#ai-agents) | | Run your own launchpad | The starter launchpad | [Your own launchpad](/docs/developers#your-own-launchpad) | The fastest way in: [follow launches and trade in 30 lines](/docs/developers#follow-launches-and-trade-in-30-lines). ## Two ways to build **Level 1, open to everyone.** Use the SDK, the API and the feed, and add your builder code to the trades you send. You need no account and no approval. **Level 2, partners.** Partners get higher limits and closer support. Otomate admits partners on an allow-list at first. A bond in OTO may be used later. Otomate decides the terms. ## Free to start Every read, the feed and the API work without a key. Limits apply per client. | Access | How | Reads per minute | Feed connections | | --- | --- | --- | --- | | No key | Nothing to do | 300 | 5 | | Free key | A wallet signs a message ([API keys](/docs/developers#api-keys)); counted per client address | 600 | 10 | | Partner | Otomate's list: terminals, aggregators, data providers | 6,000 | 100 | | OTO lock (later) | OTO the wallet locks: 100,000 / 1,000,000 OTO | 3,000 / 15,000 | 50 / 200 | | Pay per request | x402: a premium request answers `402` with its price; pay it in USDC on Base and send it again | One request per payment | — | Otomate sets each tier's numbers. Locking OTO only raises your limits; it shares no revenue. The `RateLimit-Policy` header and `GET /api/v1/{chain}/keys/me` show yours. ## Chains and contracts One API host serves both chains: `https://api.otomate.fun`, with the chain in the path. Every answer names its chain. | | Ink | Robinhood Chain | | --- | --- | --- | | Chain ID | 57073 | 4663 | | API | `https://api.otomate.fun/api/v1/ink/…` | `https://api.otomate.fun/api/v1/robinhood/…` | | Fallback origin (its chain only) | `https://otomate-launcher-api-production.up.railway.app` | `https://otomate-launcher-api-rh4663-production.up.railway.app` | | Uniswap v4 PoolManager | `0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32` | `0x8366a39CC670B4001A1121B8F6A443A643e40951` | | Universal Router | `0x112908daC86e20e7241B0927479Ea3Bf935d1fa0` | `0x8876789976dEcBfCbBbe364623C63652db8C0904` | | V4 Quoter | `0x3972C00f7ed4885e145823eb7C655375d275A1C5` | `0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94` | | StateView | `0x76Fd297e2D437cd7f76d50F01AfE6160f86e9990` | `0xF3334192D15450CdD385c8B70e03f9A6bD9E673b` | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | | WETH | `0x4200000000000000000000000000000000000006` | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` | ### Pairs Each launch pairs its token with one ERC-20, its pair. A pair is open once the chain's launch config enables it: the config's `quotes()` lists its pairs, `quoteConfig(pair)` answers whether one is enabled and its presets, and `QuoteConfigured` and `QuoteDisabled` log each change. Otomate's site, SDK (`otomatePairs(chain)`) and MCP server name the pairs below; any other ERC-20 the config enables works the same way. All of these have 18 decimals. | Chain | Pair | What it is | Address | | --- | --- | --- | --- | | Ink | WETH, the default | Wrapped Ether; pay and receive ETH through the router | `0x4200000000000000000000000000000000000006` | | Ink | OTO | Otomate's token | `0x0e04Ca25B5cd15047946d89E21cBcDBaE43A323B` | | Ink | wAAPLx | Apple xStock, in an ERC-4626 wrapper | `0x943BF64D566c32A2Bcd41AC92FB63C111cC9De8f` | | Ink | wNVDAx | NVIDIA xStock, in an ERC-4626 wrapper | `0xa8ddb5Cd96b5222AFe198316E9A57CAA642850D5` | | Ink | wSPYx | S&P 500 ETF xStock, in an ERC-4626 wrapper | `0xE7E553Cd128F0011777323A0b44a7b96EA1CB540` | | Ink | wTSLAx | Tesla xStock, in an ERC-4626 wrapper | `0xc3FdBe3A68EE5dE461D30415a8165cf9Aefe1171` | | Ink | WIRED | A 404 token (ERC-20 and ERC-721), not a stock | `0xC7d264d906B488ef6660005af5450c979F58006c` | | Robinhood Chain | WETH, the default | Wrapped Ether; pay and receive ETH through the router | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` | | Robinhood Chain | OTO | Otomate's token | `0xAAed1a6A716Bb12c6037e8B170aB6dA29f28fD04` | | Robinhood Chain | NVDA | NVIDIA Stock Token (ERC-8056) | `0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC` | | Robinhood Chain | AAPL | Apple Stock Token (ERC-8056) | `0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9` | | Robinhood Chain | SPCX | SpaceX Stock Token (ERC-8056) | `0x4a0E65A3EcceC6dBe60AE065F2e7bb85Fae35eEa` | | Robinhood Chain | SPY | S&P 500 ETF Stock Token (ERC-8056) | `0x117cc2133c37B721F49dE2A7a74833232B3B4C0C` | | Robinhood Chain | QQQ | Nasdaq-100 ETF Stock Token (ERC-8056) | `0xD5f3879160bc7c32ebb4dC785F8a4F505888de68` | | Robinhood Chain | TSLA | Tesla Stock Token (ERC-8056) | `0x322F0929c4625eD5bAd873c95208D54E1c003b2d` | The issuers of the stock tokens (the xStock wrappers and the Stock Tokens) can pause them or blocklist addresses, which would stop trading on the pools of that pair. WIRED's issuer can upgrade its contract at any time. Each whole WIRED a wallet receives mints an NFT of its collection and each whole WIRED it gives up burns one (a wallet that goes under a whole WIRED, a sell or a payment included, loses one of its WIRED NFTs), with the gas paid by the transaction's sender, and a transfer of more than 64 whole WIRED at once to or from a wallet that has not called `setSkipNFT(true)` reverts: split a larger sell, or opt the wallet out first. ### Launch contracts **Deployed on Ink, Robinhood Chain, Base and HyperEVM** (read on each chain on 2026-10-11). Each contract has the **same address on every one of these chains**: | Contract | Ink, Robinhood Chain, Base and HyperEVM | | --- | --- | | Router (trades) | `0x173b1e9eCC06d53b239754602177F1707BFE76A1` | | Factory | `0xd8661806D749a6360348a0D4f94f8db36B43420e` | | Config | `0x6Ab344Dcf639E99DB656cC8DeA76A9b5262ee545` | | Hook | `0xF8b5e7A4F3cEb6ba9F9a619611360750A7D728EC` | | Distributor | `0x021851048acE99Bc0fc3F064Ad699a335de9d35c` | | Lens | `0x9f9Ef622f5dafDFd69C54CAb1bcCe4E8E71B3f2b` | | NFT vault | `0x5Ad95E0CEA3210f1F1E391f3A77405cBed128C9E` | | NFT gate | `0x5CD814311dD91B5fF7f4712Ae6c8A1BFb8319D70` | | Forge | `0xdb82153b0A0C9D31Bc4D0F85fc0F40E6753b0D5e` | | Collection beacon | `0x8A6a2BCbA7Ea5371d2eba795b67f207D0fd2f49a` | | Royalty list (OpenSea) | `0xE83248a0Af0fb083E0091c1E867b5b81accF45f9` | | Token deployer | `0x208518704cC6b27c34A3ECd78A9ecf20c4959aE6` | | Upgrade timelock | `0xd14837BF79D73A4C73270889f8D822Bf7Fc77A5a` | | Otomate's Safe | `0xfA0ffF20DB7A2939cc402e395c1916Ba42f5E850` | | Global NFT sink | Ink `0x31E1c790C933acb9F8F2f9C1dE2FFd1F2834aB60`, Robinhood Chain `0xe4973A293570C054AdCbb6769501D249Af67Aed5` (none on Base and HyperEVM) | Each chain's runtime code hashes are in the SDK (`LAUNCH_POOL_DEPLOYMENT.live`) and in the OpenAPI document (`info.x-otomate-contracts`). Each chain opens to new launches on its own date: the config's `creationEnabled()` tells. The other chains of the hook's table are not deployed yet: BNB Smart Chain (56), Arbitrum One (42161), OP Mainnet (10), Unichain (130), Ethereum (1). New launches use the config's current hook; earlier pools keep their hook. Follow the config's `LaunchHookSet` event to learn a new hook version. ## Versioning and stability - The API, the feed schema (`otomate.launch-pool-feed.v1`) and the SDK follow one rule: within a version, fields may be added; none is removed, renamed or changes type. A breaking change ships as a new version (`/api/v2`, `…feed.v2`) next to the old one. - Amounts are integers as decimal strings (`raw`), often with their exact decimal value (`decimal`). Never floats. - Every record says whether it is `mined` (in the latest block) or `final` (its block is finalized). - The machine-readable reference is the OpenAPI document: [openapi.v1.json](/docs/kit/openapi.v1.json), also served at `/api/v1/openapi.json`. ## Your own launchpad The starter launchpad is a Next.js site built on the SDK: Explore, token pages with a live chart and trades, Create, a buy and sell ticket and creator profiles. Brand, colors and your builder code live in one configuration file. Copy it, set your name and builder address, deploy it anywhere, and earn your builder fee from the first trade. It is a template: Otomate hosts nothing for you. # Quickstart Two complete programs that follow new launches in real time and buy each new launch paired with WETH, paying in ETH. Both run as they are; read [Trade launch pools](/docs/developers#trade-launch-pools) before risking funds. ## Follow launches and trade in 30 lines **TypeScript** ([quickstart.ts](/docs/kit/quickstart.ts)), with the SDK and viem. Node 18 or later. ```bash npm install @otomate/sdk viem PRIVATE_KEY=0x… npx tsx quickstart.ts ``` ```ts import { createOtomate, otomateViemChain } from '@otomate/sdk'; import { createWalletClient, http, type Address } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const walletClient = createWalletClient({ account, chain: otomateViemChain('ink'), transport: http() }); // Your builder code earns you a fee on top of each trade you send (0 here; at most 200 bps). const otomate = createOtomate({ chain: 'ink', walletClient, builder: null, apiKey: process.env.OTOMATE_API_KEY }); otomate.feed({ topics: ['launch', 'trade'], cursor: process.env.CURSOR }) .on('launch', async e => { console.log(`new launch ${e.data.symbol} ${e.token} by ${e.data.creator}`); if (e.data.quote !== otomate.chain.weth) return; // WETH pairs: we pay in ETH // The launch protection charges buys in the first seconds: wait it out. const until = (e.data.launchedAt + e.data.surcharge.duration + 1) * 1000; await new Promise(r => setTimeout(r, Math.max(0, until - Date.now()))); const r = await otomate.buy(e.token as Address, 10n ** 15n); // 0.001 ETH, 1 % slippage console.log(`bought ${r.tokenAmount} for ${r.quoteAmount} wei (fee ${r.quoteFee}, burned ${r.burn}) ${r.hash}`); }) .on('trade', e => console.log(`${e.data.side} ${e.token} ${e.data.quoteRaw} wei, progress ${e.data.progressBps / 100} %`)) .on('reorg', e => console.log(`reorg: drop what you stored above block ${e.afterBlock}`)) .on('final', e => console.log(`final up to block ${e.block}`)) .on('error', e => console.error(e.message)) .start(); ``` What it does: - `feed()` opens the real-time feed. If the connection drops, the SDK reconnects from the last event it received: you never miss or repeat an event. Store `feed.cursor` to resume after a restart (`CURSOR`). - `buy()` quotes on the chain's V4 Quoter, checks your allowances (an approval for exactly the amount when an ERC-20 is short), sends the swap through Otomate's router with a 1 % minimum (the Universal Router with Permit2 where the router is not deployed), and returns the hook's record of the trade: what you paid, received, the fee and the tokens burned. **Python** ([quickstart.py](/docs/kit/quickstart.py)), with web3.py only. The API quotes on chain and builds the router call; your script signs it. ```bash pip install web3 PRIVATE_KEY=0x… python quickstart.py ``` ```python import json, os, time, urllib.request from web3 import Web3 API = os.environ.get("OTOMATE_API", "https://api.otomate.fun") + "/api/v1/ink" w3 = Web3(Web3.HTTPProvider(os.environ.get("RPC_URL", "https://rpc-qnd.inkonchain.com"))) me = w3.eth.account.from_key(os.environ["PRIVATE_KEY"]) def send(built): # sign and send one transaction the API built, wait for its receipt tx = {"to": Web3.to_checksum_address(built["to"]), "data": built["data"], "value": int(built["value"]), "from": me.address, "chainId": 57073, "nonce": w3.eth.get_transaction_count(me.address), "maxFeePerGas": w3.eth.gas_price * 2, "maxPriorityFeePerGas": w3.eth.max_priority_fee} tx["gas"] = int(built["gas"]) if built.get("gas") else w3.eth.estimate_gas(tx) * 13 // 10 # the API's limit, else the estimate + 30 % return w3.eth.wait_for_transaction_receipt(w3.eth.send_raw_transaction(me.sign_transaction(tx).raw_transaction)) def buy(token, wei): # the API quotes on chain and builds the router call (ETH in on WETH pairs) body = json.dumps({"token": token, "side": "buy", "amountIn": str(wei), "account": me.address, "nativeEth": True}).encode() built = json.load(urllib.request.urlopen(urllib.request.Request(API + "/swap-transaction", body, {"content-type": "application/json"}))) for approval in built["approvals"]: send(approval) return built["quote"]["amountOut"]["decimal"], send(built["transaction"]) with urllib.request.urlopen(API + "/feed?topics=launch") as stream: # server-sent events, one JSON per data line for line in stream: if not line.startswith(b"data: "): continue e = json.loads(line[6:]) if e["type"] == "reorg": print("reorg: drop what you stored above block", e["afterBlock"]) if e["type"] != "launch" or e["data"]["quote"] != "0x4200000000000000000000000000000000000006": continue print("new launch", e["data"]["symbol"], e["token"]) time.sleep(max(0, e["data"]["launchedAt"] + e["data"]["surcharge"]["duration"] + 1 - time.time())) # launch protection out, receipt = buy(e["token"], 10**15) print("bought", out, "tokens, tx", receipt["transactionHash"].hex()) ``` On Robinhood Chain, use `chain: 'robinhood'` (TypeScript), or the Robinhood API origin with `/api/v1/robinhood`, chain ID 4663 and the chain's WETH (Python). The routers differ between the chains; the SDK and the API handle it. ## Before you trade - **Launch protection.** Buys in the first seconds after a launch pay an extra quote fee that decreases over time (`surcharge` in the launch event). Both programs wait it out. - **Fees.** Each pool's rates are in its launch event and token sheet (`fees.buyBps`, `fees.sellBps`). The quote you get already includes them. - **Slippage.** The SDK sends a 1 % minimum by default (`slippageBps`). A swap that would end below the minimum reverts. - **Reorgs.** Events arrive at the mined block. If the chain reorganizes, a `reorg` marker tells you which events to drop; `final` markers tell you what can no longer change. # Real-time feed The feed pushes every new launch, trade, 50 % mint gate and graduation of one chain as soon as its block is mined. It is public, free to start, and resumable: a client that reconnects with its last cursor gets every event it missed, once. | Transport | URL | Use it from | | --- | --- | --- | | Server-sent events | `GET /api/v1/{chain}/feed` | Any HTTP client, browsers (`EventSource`), the SDK | | WebSocket | `GET /api/v1/{chain}/feed/ws` | WebSocket clients; JSON text frames | | Pages | `GET /api/v1/{chain}/feed/events` | Polling, or catching up before you connect | `{chain}` is `ink` or `robinhood`, on that chain's API origin ([Chains and contracts](/docs/developers#chains-and-contracts)). Schema: `otomate.launch-pool-feed.v1`. ## Choose what you receive | Query | Values | Default | | --- | --- | --- | | `topics` | Comma-separated: `launch`, `trade`, `graduation`, `mint_gate` | All | | `tokens` | Comma-separated token addresses, at most 100 | All tokens | | `cursor` | An event's `id`: resume after it. A browser's `EventSource` may send `Last-Event-ID` instead. | — | | `fromBlock` | Start at this block | The newest event | Without `cursor` or `fromBlock`, the feed starts with what happens next. Markers (`ready`, `final`, `reorg`) always come, whatever the filter. ```bash curl -N "https://api.otomate.fun/api/v1/ink/feed?topics=launch,graduation" ``` ## Events Every event has the same envelope; `data` depends on its `type`. | Field | Meaning | | --- | --- | | `v` | `1` | | `type` | `launch`, `trade`, `graduation` or `mint_gate` | | `id` | The cursor. Store the last one you processed; send it back to resume. Treat it as an opaque string. | | `chainId` | 57073 or 4663 | | `block` | `number` (decimal string), `hash`, `timestamp` (Unix seconds) | | `transactionHash`, `logIndex` | Where the event happened | | `status` | `mined` (the latest block) or `final` (its block is finalized) | | `confirmed` | `false` only in the first seconds, while a trader's smart-account wallet may still be corrected | | `token`, `poolId` | The launch | | `type` | `data` | | --- | --- | | `launch` | `creator`, `quote` (the pair), `hook`, `collection` (or null), `name`, `symbol`, `metadataURI`, `launchedAt`, `supplyRaw`, `thresholdRaw` (quote needed to graduate), `minimumTradeRaw`, `mintGateBps`, `poolKey`, `quoteIsCurrency0`, `launchTick`, `mintGateTick`, `graduationTick`, `initialSqrtPriceX96`, `fees` (`quoteFeeBps`, `shareBps`, `burnBps`, `creatorBurnBps`, and the rates after graduation `marketQuoteFeeBps`, `marketBurnBps`), `feeRate` (`buyBps`, `sellBps`, `creatorBps`, `nftBps`, `protocolBps`, `burnBps`), `surcharge` (`startBps`, `duration`, `halvings`), `initialBuy` (or null) | | `trade` | `side` (`buy` or `sell`), `trader` (the attributed wallet), `traderBasis`, `router`, `tokenRaw`, `quoteRaw` (the trader's gross amounts), `quoteFeeRaw`, `surchargeRaw`, `burnRaw`, `sqrtPriceX96`, `tick` and `progressBps` after the trade, `sequence` (per pool, +1 each trade), `builder` (the trade's builder code: `address`, null for a tag alone, `feeRaw` in the pair on top of `quoteFeeRaw`, `feeBps` applied, `tag`; null without one) | | `graduation`, `mint_gate` | `at` (Unix seconds), `l2Block`, `sqrtPriceX96` | Fields may be added within v1; none will be removed or renamed. ## Markers | Marker | Meaning | What to do | | --- | --- | --- | | `ready` | Sent first: `resumeFrom`, `confirmedBlock`, `finalizedBlock`, your filter | Nothing | | `reorg` | `{afterBlock}`: the chain reorganized | Drop every event you stored with a block above `afterBlock`. The replacements follow. | | `final` | `{block}`: every event at or below it is final | Settle anything you kept provisional | | `error` | `{code}` just before the server closes the stream | Reconnect with your cursor (the SDK does it) | Markers carry an `id` too, so `Last-Event-ID` always points to where you are. **A mirror that stays exact:** 1. On each event, store it with its block number, and store its `id` as your cursor. 2. On `reorg`, delete what you stored above `afterBlock`. 3. On `final`, mark what is at or below `block` as final. 4. After any disconnection, reconnect with your cursor. If your cursor names a block that changed while you were away, the feed first sends a `reorg` marker down to the last block it had told you was final, then replays from there. ## WebSocket Same query, same JSON (each frame is one event or marker, with its `type`). The server pings every 15 seconds; a client that stops answering is disconnected. To send an API key from Node, use a client that sets headers (the `ws` package); browsers send none. ```ts const ws = new WebSocket('wss://api.otomate.fun/api/v1/ink/feed/ws?topics=trade&tokens=0x…'); ws.onmessage = m => console.log(JSON.parse(m.data)); ``` ## Pages `GET /api/v1/{chain}/feed/events?cursor=…&limit=500` answers up to 500 events after the cursor, and `next`, the cursor to continue from. `complete: true` means you reached the newest event. A page can start with `reorg` (`{afterBlock}`) when your cursor's block changed. ## Limits and slow clients - Connections and connection attempts are limited per client and tier ([Free to start](/docs/developers#free-to-start)); a refused connection answers `429` or `503` with `Retry-After`. - A client that reads slower than events arrive is disconnected once its buffer fills; it reconnects with its cursor and loses nothing. - A far cursor replays at a steady pace, then switches to live events. # API v1 Everything a front end, a bot or a data pipeline needs about Otomate's launches, in plain JSON. `https://api.otomate.fun/api/v1/{chain}` serves both chains ([Chains and contracts](/docs/developers#chains-and-contracts)). Each chain's fallback origin serves only its own chain: a request there for the other chain answers `421` with the right origin in `origin`. The full reference is the OpenAPI document: [openapi.v1.json](/docs/kit/openapi.v1.json). ## Endpoints | Method and path | Answers | | --- | --- | | `GET /tokens?sort="e=&collection=&q=&limit=&cursor=` | Launches. `sort`: `new`, `graduating` (closest to graduation), `graduated`, `trending` (24 h volume), `gainers` (24 h change). `q`: name, symbol or the start of an address. | | `GET /tokens/{token}` | One launch in one read: identity (name, symbol, metadata and profile, logo, banner, creator, moderation), market (price in the pair and in USD, market cap, FDV, liquidity, holders, 5 min / 1 h / 24 h volume, buys, sells and change), curve (progress, graduated, mint gate), fees (the rates that apply now, and both the curve's and the after-graduation pair), collection | | `GET /tokens/{token}/trades?limit=&cursor=` | Trades, newest first, with the attributed wallet and the trade's builder code (`builder`, or null) | | `GET /tokens/{token}/candles?interval=¤cy=&limit=&to=` | OHLCV. `interval`: `1s`, `1m`, `5m`, `15m`, `1h`, `4h`, `1d`. `currency`: `quote` or `usd` | | `GET /tokens/{token}/holders` | The 50 largest holders and the holder count | | `GET /creators/{address}` | A creator's launches, how many graduated, and their creator earnings | | `GET /wallets/{address}` | A wallet's positions (balance, value, cost basis, profit and loss) and what it can expect: creator earnings, builder fees (`claimable.builder`, per currency and hook) | | `GET /quote?token=&side=&amountIn=&builder=&builderFeeBps=&builderTag=` | A quote from the chain's V4 Quoter, with every fee: quote fee, launch surcharge, burn, builder fee (`builder.feeBps` is the rate applied), price impact | | `POST /swap-transaction` | A swap ready to sign through Otomate's router (or the chain's Universal Router with Permit2), and the approvals to send first | | `GET /feed`, `/feed/ws`, `/feed/events` | The [real-time feed](/docs/developers#real-time-feed) | | `GET /tokenlist.json` | The chain's launches as a token list (tokenlists.org) | | `GET /status` | The indexer's latest and finalized blocks and the feed's health | Creating tokens uses `POST /metadata` and `POST /create-transaction`: see [Create tokens](/docs/developers#create-tokens). ```bash curl "https://api.otomate.fun/api/v1/ink/tokens?sort=trending&limit=5" curl "https://api.otomate.fun/api/v1/ink/quote?token=0x…&side=buy&amountIn=10000000000000000" ``` ## Conventions | | | | --- | --- | | Amounts | `{"raw": "1500000000000000000", "decimal": "1.5"}`: `raw` in the smallest unit, `decimal` exact. Never floats. | | Prices | Decimal strings, the token's price in its pair (and in USD when known) | | Time | `timestamp`: Unix seconds of the block | | Finality | `status`: `mined` or `final` | | Pages | Send `cursor`; read `next` (`null` on the last page) | | Addresses | Lower case in answers; any case in requests | | Rates | Basis points | | Moderation | Lists show only launches the moderation shows. A hidden launch's sheet answers `moderation: "hidden"` with no name, symbol or metadata. | ## Swap transactions ```bash curl -X POST "https://api.otomate.fun/api/v1/ink/swap-transaction" \ -H 'content-type: application/json' \ -d '{"token":"0x…","side":"buy","amountIn":"10000000000000000","account":"0xYou","nativeEth":true,"slippageBps":100}' ``` Add `builder`, `builderFeeBps` and `builderTag` to carry your [builder code](/docs/developers#builder-codes) (a tag alone needs no builder). The swap goes through Otomate's router where it serves the pool (`route: "router"`), else the chain's Universal Router with Permit2 (`route: "permit2"`). Send `"route": "permit2"` to always take the Universal Router, or `"route": "router"` to refuse anything else (`400`, `field: "route"`). The answer holds `quote`, `minimumOut`, `deadline`, `route`, `approvals` (send them first, in order; each for exactly the amount in: on the router route, the input token to the router; on the Permit2 route, the token to Permit2, then Permit2 to the Universal Router; empty for ETH in) and `transaction` (`to`, `data`, `value`, and `gas`: the gas limit to send it with, the node's estimate plus 30 %; it is left out while an approval comes first, or when the node gives no estimate: then estimate the swap yourself, after the approvals, and add 30 %), all from and to `account`. Nothing is signed or stored. Check that `to` is Otomate's router (`0x173b1e9eCC06d53b239754602177F1707BFE76A1`) or the chain's Universal Router before you sign. ## Errors Errors answer a closed code: `{"error": "API_QUERY", "field": "…"}`. | Status | Codes | Meaning | | --- | --- | --- | | 400 | `API_QUERY`, `FEED_QUERY`, `FEED_CURSOR_CHAIN` | A malformed request; `field` names the culprit when known | | 401 | `FEED_KEY` | An unknown, revoked or malformed key | | 404 | `API_NOT_FOUND` | No such launch | | 421 | `API_CHAIN` | The other chain: use the API in `origin` | | 422 | `QUOTE_REVERTED` | The chain would refuse the trade; `reason` is the contract error (`PartialFillUnsupported`, `PriceBelowLaunch`, `TradeBelowMinimum`, …) | | 429 | `API_RATE_LIMITED`, `FEED_RATE_LIMITED`, `FEED_CLIENT_LIMIT` | Over your tier's budget; wait `Retry-After` seconds | | 503 | `API_UNAVAILABLE`, `FEED_UNAVAILABLE`, `FEED_FULL` | Temporarily unavailable; retry after `Retry-After` | ## API keys A key raises your limits and is free. A wallet signs a message, the API answers a key bound to that wallet. The API stores nothing: the key carries its wallet and a signature, and every key of one wallet shares its limits. ```ts const api = 'https://api.otomate.fun/api/v1/ink'; // a key works on both chains const { message } = await (await fetch(`${api}/keys/challenge?address=${account.address}`)).json(); const signature = await account.signMessage({ message }); // Sign-In with Ethereum (EIP-4361); moves no funds const { key, tier } = await (await fetch(`${api}/keys`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message, signature }), })).json(); // Then: Authorization: Bearer ``` - The key is shown once: keep it secret. Lost or leaked, sign again for a new one; Otomate can revoke a wallet's keys. - Accounts whose own key signs (EOAs, EIP-7702 accounts) are accepted; contract wallets that sign through ERC-1271 are not, yet. - `GET /api/v1/{chain}/keys/me` with the key answers its tier and limits. - Free keys are counted per client address: several free keys from one address share one free budget. ## Limits Limits are per client: your key's wallet, or your address without a key. Each answer carries `RateLimit-Policy: ;w=60`. | Budget | Counts | | --- | --- | | Reads per minute | Each `/api/v1` request; a quote or a swap transaction counts ten (they read the chain) | | Feed connections | Open SSE and WebSocket connections | | Feed connections per minute | New connections | | Feed pages per minute | `/feed/events` requests | The numbers per tier are in [Free to start](/docs/developers#free-to-start). Otomate sets each tier's numbers; the free tier is meant to let a bot start. ## Pay per request (x402) Where enabled, a request above your budget answers `402 Payment Required` instead of `429`, with its price and terms in the `PAYMENT-REQUIRED` header (x402 version 2): USDC on Base (`eip155:8453`), paid to the Otomate treasury. Your x402 client signs the USDC transfer and sends the request again with `PAYMENT-SIGNATURE`; x402 client libraries do this for you. The payment is settled only when the answer succeeds (`PAYMENT-RESPONSE` carries the transaction): a refused request costs nothing. One payment pays one request. Locking OTO (later) and x402 only raise limits. ## Stability Within v1, fields may be added; none is removed, renamed or changes type. Breaking changes ship as `/api/v2` next to v1. Fee rates are data: read them from each pool, never hard-code them. # Trade launch pools A launch pool is a Uniswap v4 pool with LP fee 0 and Otomate's hook. You trade it through **Otomate's router** (one approval of the input token to it, then one transaction; no Permit2) or through each chain's Universal Router with Permit2, with **empty hookData** (or your [builder code](/docs/developers#builder-codes)), and you quote it with the chain's V4 Quoter. Both run the same PoolManager swap: same price, same fees. There is no witness, signature or allow-list. Three ways, from the simplest: | Way | You do | Best for | | --- | --- | --- | | SDK | `otomate.buy(token, amount)` / `otomate.sell(…)` / `otomate.buyExactOut(token, tokens)` | TypeScript bots and fronts | | API | `POST /api/v1/{chain}/swap-transaction`, then sign | Any language | | Direct | Call Otomate's router, or encode the Universal Router call yourself (below) | Routers, aggregators, existing bots | The SDK and the API take Otomate's router by default and fall back to the Universal Router with Permit2 where the router is not deployed or does not serve the pool. Every approval they prepare is for exactly the amount of the trade, never unlimited. **Pairs.** A launch pairs its token with WETH or with another ERC-20 the config enables, such as OTO or a stock token (Ink's xStock wrappers, Robinhood Chain's Stock Tokens): the pairs per chain are in [Chains and contracts](/docs/developers#chains-and-contracts). Some of those tokens (the stock tokens, for example) let their issuer freeze balances or pause transfers; the issuer's rules then apply to the pair side of every trade in that pool, and a pause would stop trading on the pools of that pair. ## How a swap is charged Each pool stores its rates once, in its registration snapshot (`LaunchRegistered`, the launch event, the token sheet): a quote fee `q` and a burn `b`, in basis points, for the curve, and a second pair (`marketQuoteFeeBps`, `marketBurnBps`) that applies from the swap after graduation on. `b` is the part of the creator's share the creator chose to burn, 0 when they chose none. A [builder code](/docs/developers#builder-codes) adds its rate `r` on top (0 without one). Every swap has one leg in the pair (the quote) and one in the token: | Swap | Taken on the leg you specify | Taken on the other leg | | --- | --- | --- | | Buy, exact in (quote `Q` in) | quote fee `floor(Q·q / 10⁴)`, builder fee `floor(Q·r / 10⁴)` | burn `floor(T·b / (10⁴ − q − r))` of the tokens out `T` | | Buy, exact out (tokens `X` out) | burn `ceil(X·b / (10⁴ − q − r − b))` | quote fee `ceil(A·q / (10⁴ − q − r))`, builder fee `ceil(A·r / (10⁴ − q − r))` on the quote in `A` | | Sell, exact in (tokens `T` in) | burn `floor(T·b / (10⁴ − q))` | quote fee `floor(Qo·q / 10⁴)`, builder fee `floor(Qo·r / 10⁴)` on the quote out `Qo` | | Sell, exact out (quote `Q` out) | quote fee `ceil(Q·q / (10⁴ − q − r))`, builder fee `ceil(Q·r / (10⁴ − q − r))` | burn `ceil(A·b / (10⁴ − q − b))` on the tokens in `A` | The token leg is grossed up so that the total cost is `q + r + b` of the trade's value, to rounding. The burned tokens leave the supply. The quote fee is shared between the creator, NFT holders, the protocol, OTO burns and global NFT holders, as each pool's snapshot says: read the shares from the snapshot or the token sheet, never from these pages. **Launch protection.** On buys during the first `duration` seconds (at most 3,600), `q` includes a surcharge that decreases with the time `elapsed` since the launch: `startBps >> floor(elapsed × halvings / duration)`, or, when `halvings` is 0, `floor(startBps × (duration − elapsed) / duration)` (a straight line down to 0). It is capped so that `q + b` stays at most 99 %; a builder rate is then lowered to fit (the surcharge never is). `startBps`, `halvings` and `duration` are in each pool's snapshot: read them, and quote right before you trade. The creator's own buy in the creation transaction is exempt; anyone else pays it. As the surcharge falls between your gas estimate and the block, a swap in the first seconds can use more gas than estimated: send swaps with the estimate plus 30 % (the SDK does, and the API's `transaction.gas`, when it is there, is that limit; only the gas used is paid). **What the hook refuses** (the swap reverts): | Error | When | What to do | | --- | --- | --- | | `PartialFillUnsupported` | The swap cannot be filled entirely: a sell larger than the quote the pool holds, or a swap that would stop at your price limit | Sell less; use the router's default limits | | `PriceBelowLaunch` | A sell would end below the launch price | Sell less | | `TradeBelowMinimum` | Under the pair's minimum trade (0 on today's presets) | Trade more | Graduation does not move liquidity: the same pool keeps trading. ## Quote The V4 Quoter simulates the hook, so its answer is what executes, both fee legs included. Use `quoteExactInputSingle` with the pool key, `zeroForOne` and empty hookData (or your builder code). The pool key: `currency0` and `currency1` are the token and its pair in address order, `fee` is 0, `tickSpacing` is the pool's (60 on today's presets), `hooks` is the pool's hook. A buy is `zeroForOne` when the pair is `currency0`. The SDK can also quote without a node round trip: `quoteLocal(snapshot, slot0, side, amount, now, {graduated, builder})` runs the v4 swap and the hook's fees exactly, both rate pairs, the surcharge and a builder code included (checked to the wei against the final hook on 220 swaps, in TypeScript and Go). It is exact while the pool holds only the hook's liquidity; after graduation, pass `graduated: true` and any outside positions you know. ## Route: Otomate's router `LaunchPoolRouter` at `0x173b1e9eCC06d53b239754602177F1707BFE76A1`, the same address on Ink, Robinhood Chain, Base and HyperEVM (each chain's runtime code hash is in the SDK, `LAUNCH_POOL_DEPLOYMENT.live`). It has no owner, no storage and keeps no funds. It trades the pools of its hook only; any other pool reverts `InvalidPool`. You pass the pool's key as it is; the router finds the token's side. | Call | `value` | Approval first | | --- | --- | --- | | `buy(key, amountIn, minTokensOut, recipient, deadline, hookData)`, paying ETH on a WETH pair | `amountIn` | none | | `buy(…)`, paying the pair's ERC-20 (WETH, OTO, …) | 0 | the pair token to the router, `amountIn` | | `buyExactOut(key, tokensOut, maxAmountIn, recipient, deadline, hookData)`, paying ETH | `maxAmountIn` (the rest comes back) | none | | `buyExactOut(…)`, paying the ERC-20 | 0 | the pair token to the router, `maxAmountIn` | | `sell(key, tokensIn, minAmountOut, recipient, deadline, hookData, nativeOut)` | 0 | the launch token to the router, `tokensIn` | - The payer is always the sender; the output goes to `recipient`. The router refuses as recipient address(0) and every contract of the trade (itself, the PoolManager, the hook, the factory, the wrapped native token, the token and its pair): `InvalidRecipient`. - `nativeOut: true` pays a sell out in ETH (WHYPE pools on HyperEVM: HYPE). Native in and out work on wrapped-native pairs only. - Amounts are raw units, above 0 and below 2¹²⁷. The deadline is a unix time; its own second is valid. - `hookData` goes to the hook unchanged: `0x`, or your builder code. Never put the router's address as the builder: it cannot claim. - The router uses the allowance to the unit, so an approval of exactly the trade's amount is enough; an exact-out buy leaves what it did not spend of `maxAmountIn`. - After the hook's `SwapFee`, it emits `Bought(payer, recipient, token, quoteIn, tokensOut, quoteRefund)` or `Sold(payer, recipient, token, tokensIn, quoteOut)`. On an exact-out buy paid in ETH, `quoteIn` is what was sent and `quoteRefund` what came back. - A call from inside another PoolManager unlock reverts (`AlreadyUnlocked`), as with the Universal Router; two router calls in one transaction work. - Its errors: `Expired`, `InvalidPool`, `InvalidAmount`, `InvalidRecipient`, `InvalidNativeValue`, `NotWrappedNative`, `TooLittleReceived(amountOut, minAmountOut)`, `TooMuchRequested(amountIn, maxAmountIn)`, `Reentrancy`, `Unauthorized`, `NativeTransferFailed`. The hook's errors arrive wrapped by the PoolManager. ```ts import { encodeRouterBuy, launchPool, launchPoolRouter, quoteOnchain } from '@otomate/sdk'; const pool = launchPool('ink', token, weth, hook); const router = launchPoolRouter('ink')!; // { address, codeHash, hook } const out = await quoteOnchain(publicClient, pool, 'buy', amountIn); const tx = encodeRouterBuy({ pool, pin: router, amountIn, minTokensOut: out * 99n / 100n, recipient: account.address, deadline: BigInt(Math.floor(Date.now() / 1000) + 120), hookData: '0x', nativeEth: true }); await walletClient.sendTransaction(tx); // { to: router, data, value } ``` `readLaunchPoolRouterCode(publicClient, router)` reads the code at the router's address and answers `ok` only when it is the pinned runtime: the SDK checks it before every router trade. ## Route: the Universal Router with Permit2 | | Ink | Robinhood Chain | | --- | --- | --- | | Router | `0x112908daC86e20e7241B0927479Ea3Bf935d1fa0` | `0x8876789976dEcBfCbBbe364623C63652db8C0904` | | v4 single-swap tuple | `(PoolKey, zeroForOne, amountIn, amountOutMinimum, hookData)` | `(PoolKey, zeroForOne, amountIn, amountOutMinimum, minHopPriceX36, hookData)` with `minHopPriceX36 = 0` | | Commands | `0x10` (V4_SWAP); `0x0b10` (WRAP_ETH, V4_SWAP) to pay ETH; `0x100c` (V4_SWAP, UNWRAP_WETH) to receive ETH | Same | | Actions | `0x060c0f` (SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL) | Same | ERC-20 inputs go through Permit2: approve the token to Permit2, then give the router a Permit2 allowance; the SDK and the API prepare both for exactly the amount in. A launch token's own `permit` (EIP-2612) takes 65-byte signatures only: EOAs, EIP-7702 EOAs, and ERC-1271 wallets whose signature is 65 bytes. Other smart wallets (multi-owner Safes, passkey wallets) use the approval or Permit2. ETH works on WETH pairs: the router wraps your ETH, or unwraps the WETH it receives. A swap costs about 163,000 gas on Ink and 173,000 on Robinhood Chain. The SDK's encoding functions, if you send transactions yourself (`swap({…, route: 'permit2'})` takes this route too): ```ts import { encodeSwap, launchPool, quoteOnchain } from '@otomate/sdk'; const pool = launchPool('ink', token, weth, hook); // key from token, pair and hook (tick spacing 60) const out = await quoteOnchain(publicClient, pool, 'buy', amountIn); const tx = encodeSwap({ pool, side: 'buy', amountIn, minimumOut: out * 99n / 100n, deadline: BigInt(Math.floor(Date.now() / 1000) + 120), recipient: account.address, nativeEth: true }); await walletClient.sendTransaction(tx); // { to: router, data, value } ``` With an ERC-20 input, the router pays the pool from the sender and sends the output to the sender: the recipient is always the sender. ## Read the result The hook emits one `SwapFee` per swap, right after the PoolManager's `Swap`: ``` SwapFee(bytes32 indexed poolId, address indexed sender, bool isBuy, uint256 quoteAmount, uint256 tokenAmount, uint256 quoteFee, uint256 surcharge, uint256 burn, uint160 sqrtPriceX96After, int24 tickAfter, uint32 seq) ``` `quoteAmount` and `tokenAmount` are the trader's: paid and received (a builder fee included). `quoteFee` is the pool's, surcharge included, never the builder fee. `seq` counts the pool's swaps from 1 without gaps. A swap with a builder code also emits `BuilderFee(poolId, builder, tag, fee, feeBps, seq)` right after. A trade through Otomate's router then emits its `Bought` or `Sold`. The SDK's `readSwapFee(receipt, pool)` decodes them; `swap()` and `buyExactOut()` return them, with the route taken. ## Slippage, deadlines, MEV - Set `amountOutMinimum` from a fresh quote; the SDK and the API use 1 % by default. - Set a deadline of a minute or two. - Inside the launch protection window, a buy's price moves fast with time: quote again just before sending. # Create tokens Anyone can create a token, a bot included: call the factory and pay its creation fee in ETH. One transaction deploys the token, its pool with the whole supply, an NFT collection if you ask for one, and your optional first buy. Otomate moderates what its own site and API show, not who may create. ## In one call ```ts import { createOtomate } from '@otomate/sdk'; import { readFile } from 'node:fs/promises'; const otomate = createOtomate({ chain: 'ink', walletClient }); // the SDK carries the launch contracts, as deployed const created = await otomate.createToken( { name: 'Moon Cat', symbol: 'MCAT', description: 'A cat on the moon.', image: await readFile('logo.png'), banner: await readFile('banner.png'), links: { website: 'https://mooncat.example', x: 'https://x.com/mooncat' } }, { quote: otomate.chain.weth, creatorBps: 60, creatorBurnBps: 20, initialBuy: 10n ** 16n }, ); console.log(created.token, created.poolId, created.hash); ``` `createToken` uploads the metadata to Otomate's point of upload, asks the API to build the factory call, **checks that it goes to the official factory and that every field is your choice** (name, symbol, pair, shares, your address as payout and token recipient, initial buy, metadata, the creation fee read on chain, the ETH sent), then signs with your wallet (the approval first, for an ERC-20 pair) and waits for the receipt. ## The choices | Parameter | Meaning | | --- | --- | | `quote` | The pair: WETH or another ERC-20 the config enables, such as OTO or a stock token (the list per chain is in [Chains and contracts](/docs/developers#chains-and-contracts); `otomatePairs(chain)` in the SDK). On WETH pairs you may pay in ETH. A pair the config does not enable is refused (`CREATE_QUOTE_DISABLED` from the API). | | `creatorBps` | Your share of the trading fee, within the config's bounds | | `creatorBurnBps` | The part of your share burned in token at each trade instead of paid to you (at most `creatorBps`) | | `withCollection` | An NFT collection now, or never: it cannot be added later | | `artwork` | With a collection: `'deferred'`, the default and the only value while automatic artwork is off ([Collection art](/docs/developers#collection-art)) | | `initialBuy` | Your first buy, in the same transaction, without the launch protection. It is bounded by the pair's preset amounts (`minInitialBuy` and `maxInitialBuy` of `quoteConfig(pair)` on the config; a `maxInitialBuy` of 0 means no cap): the cap is an amount in the pair, set per pair at about 10 % of the supply at the launch price, not a percentage the contract computes | | `salt` | Picks the token address (`predictToken`); random by default | The creation fee depends only on `withCollection`: read `creationFee(withCollection)` on the factory, or `creationFee` in the API's answer. Besides that fee, the factory takes your initial buy and the hook's `QUOTE_DUST` of the pair, which starts the pool, even without a buy: | Pair | `dustCovered(pair)` false or reverting (today's factory) | `dustCovered(pair)` true (factory upgrade, not deployed yet) | | --- | --- | --- | | WETH, paid in ETH | `msg.value` = fee + initial buy + `QUOTE_DUST` | `msg.value` = fee + initial buy (fee + initial buy + `QUOTE_DUST` is still accepted) | | Another ERC-20 | `msg.value` = fee; allowance of initial buy + `QUOTE_DUST` | `msg.value` = fee; allowance of the initial buy, none without a buy | The SDK reads `dustCovered` on the factory and applies the right column (`readDustCovered`, `creatorPayment`); a revert reads as false. ## Collection art A collection's art direction is the `artworkCommitment` of the factory call (`artwork` in the SDK, `artworkCommitment` in the API's create-transaction body). It is set at creation: | SDK `artwork` | `artworkCommitment` | What happens | | --- | --- | --- | | `'deferred'` (the default) | `0x0000…0000` | After the launch, the creator picks the collection's design in Otomate's Studio: three designs to choose from, then one confirmation from the creator payout wallet. The NFT mint opens once the design is chosen. | Automatic artwork is off for now, so a collection is created deferred only: any other `artworkCommitment` would open the mint with no art ever made. The API answers it with `409 CREATE_ARTWORK_AUTOMATIC_DISABLED` (field `artworkCommitment`), and the SDK and the MCP server refuse it before any upload. They build no `finalizeArtworkAutomatic()` call either. The SDK exports `LAUNCH_POOL_DEFERRED_ARTWORK_DIRECTION`. ### A launch that never graduates A collection's NFT share waits in the NFT vault until its first reveal, and a reveal needs graduation. If the pool has not graduated `neverGraduatedDelay()` after its creation while no NFT of the collection exists (30 days by default), or `neverGraduatedMintedDelay()` once NFTs exist (365 days by default), anyone may send that waiting share to Otomate's treasury with `sweepNeverGraduated(collection, asset)` on the vault. Graduation closes it for good. Read the delays on the vault, never assume them: `neverGraduatedSweep(collection)` answers when it opens, whether it is open and which rule applies (the SDK's `readNeverGraduatedSweep`, `readNeverGraduatedDelays` and `sweepNeverGraduatedCall`). ## Metadata The token's metadata is a JSON file on IPFS; its URI is stored at creation and never changes. Its format, `otomate.token-metadata.v1`: ```json { "schema": "otomate.token-metadata.v1", "name": "Moon Cat", "symbol": "MCAT", "description": "A cat on the moon.", "image": "ipfs://bafkrei…", "banner": "ipfs://bafkrei…", "external_url": "https://mooncat.example", "website": "https://mooncat.example", "twitter": "https://x.com/mooncat", "telegram": "https://t.me/mooncat", "discord": "https://discord.gg/…" } ``` `name` and `symbol` must equal the ones you pass to the factory. The symbol is 1 to 16 letters and digits. The logo is square; the banner is 1600 × 600 (8:3). Links are public `https` URLs. **Two ways to publish it:** | | Otomate's point of upload | Your own IPFS | | --- | --- | --- | | How | `POST /api/v1/{chain}/metadata` (the SDK's `uploadMetadata`) | Pin the JSON and images yourself | | Images | Bytes as `data:image/…;base64,` (PNG, JPEG, WebP, GIF; 4 MiB each), or an `https` URL the API downloads | Yours, as `ipfs://` raw CIDs | | What you get | `metadataURI`, `projectCommitment`, the drawn logo (512 px) and banner, and for a collection its hidden URI | Your URI | | Moderation | Before pinning; a refused upload answers `422 METADATA_CONTENT_REVIEW` | After launch, on display | | Limits | 20 uploads per hour per client; unused uploads are unpinned after 24 hours | — | **No banner?** Otomate generates one after the launch from the token's name and logo. ## Build the call yourself The API builds it: `POST /api/v1/{chain}/create-transaction` with your address, the metadata URI and your choices. It simulates the call and answers `transaction` (`to`, `data`, `value`, `gas`), `predictedToken` and `minTokensOut`. For an ERC-20 pair whose allowance is short, it answers `approval` first and no transaction: send the approval, wait for it, and ask again with the same `salt` (the token address stays the same). Or build it locally from the ABI with the SDK's `buildCreateTransactionLocal`: it reads the fee, simulates the creation and sets the initial buy's minimum. **Gas limit.** `gas` is null on Ink and Robinhood Chain: your wallet estimates. On HyperEVM a creation is sent with a gas limit of exactly 3,000,000, the size of its small blocks (a worst-case creation uses about 2.64 M; a wallet's usual margin over its estimate would not fit). The SDK, the MCP server and otomate.fun set it for you; if you sign by hand, use the `gas` the API answers and add no margin. **Before signing, check that `to` is the official factory.** Its address is the same on every chain and is listed in [Chains and contracts](/docs/developers#chains-and-contracts); the SDK and the MCP server check it for you. ## After the launch - **Same address on both chains:** the same creator, salt, name, symbol and pair give the same token address on Ink and Robinhood Chain, as long as both chains create with the same hook and their configs give the pair the same token supply (`predictToken` tells you before you sign). - **`tokenInfo(token)`** on the factory answers in one read: metadata URI, profile URI, creator, profile authority, pair, pool, hook, collection, creation time. - **Profile.** The description, banner and links can be updated through the token's profile: upload a profile (`kind: "profile"`, the SDK's `uploadProfile`), then the profile authority calls `setProfileURI(token, uri)`. The authority starts as the creator and moves in two steps (`proposeProfileAuthority`, then `acceptProfileAuthority`). The name, symbol and logo never change. - **Creator earnings** are paid by the distributor to your payout address when anyone calls `distribute(poolIds)`, the creator included: there is nothing to claim. The payout address moves in two steps (propose, then accept). # Builder codes A builder code lets the front end, bot or terminal that sends a trade earn a fee on it. You add your code to the swaps you send; the trader pays your fee on top of the pool's fees, in the pool's pair; the hook keeps it for you; you claim it whenever you want. This is Hyperliquid's model. ## Add your code The code is the swap's hookData, exactly 96 bytes: `abi.encode(address builder, uint256 feeBps, bytes32 tag)`. | Rule | Value | | --- | --- | | Highest fee | 200 bps (2 %) of the trade | | Paid by | The trader, on top of the pool's fees, in the pool's pair, on buys and sells | | The pool's own fee and burn | Exactly what they are without a code | | Inside the launch protection window | Your rate is lowered so that the total stays at most 99 %; the protection itself is not. `BuilderFee` shows the rate applied | | A code with `feeBps` 0 | Attribution only: the trade is credited to you, with no fee | | `tag` | Any 32 bytes you choose (a referral, a partner id, a campaign). The hook only copies it into `BuilderFee`; nothing checks it | | A tag alone | `builder` = address(0), `feeBps` 0 and your tag: no fee, no account, the tag is logged | | A malformed hookData | The swap reverts (`InvalidHookData`): another length, `feeBps` above 200, a fee without a builder, or the hook or the PoolManager as builder | | Through Otomate's router or the Universal Router | The same: both pass the hookData to the hook unchanged | | Otomate's router as builder | Never: it cannot claim. The SDK and the API refuse it | ```ts // SDK: every swap this object sends carries your code. const otomate = createOtomate({ chain: 'ink', walletClient, builder: { address: '0xYourBuilder', feeBps: 50, tag: '0x…32 bytes' } }); // Or by hand: the same bytes in the router's v4 swap. import { builderHookData } from '@otomate/sdk'; const hookData = builderHookData({ address: '0xYourBuilder', feeBps: 50 }); // tag: 32 zero bytes const tagOnly = builderHookData({ tag: '0x…32 bytes' }); // no fee, no builder ``` The API takes it too: `builder`, `builderFeeBps` and `builderTag` on `/quote` and `/swap-transaction`. A quote shows your fee apart (`fees.builder`) and the rate applied (`builder.feeBps`, with `requestedFeeBps` when the protection window lowered it). ## See what you earned - Each swap with a code emits `BuilderFee(poolId, builder, tag, fee, feeBps, seq)` right after its `SwapFee` (same pool, same `seq`). `builder` and `tag` are indexed. - `builderFees(builder, currency)` on the hook answers what you can claim in one currency (WETH, OTO, any enabled pair). - `GET /api/v1/{chain}/wallets/{builder}` lists it under `claimable.builder` (per currency and hook), and each trade of `/tokens/{token}/trades` and of the live feed carries its `builder`. - On otomate.fun, the builder dashboard shows earnings per pair and a claim button. ## Claim Call `claimBuilderFees(currencies, recipient)` on the hook from your builder address. It sends what you earned in each listed currency to `recipient`, less one raw unit that stays in your account (so your next fee costs less gas). Nothing expires. ```ts import { claimBuilderFeesCall } from '@otomate/sdk'; await walletClient.sendTransaction(claimBuilderFeesCall(hook, [weth, oto], recipient)); ``` ### A builder that is a contract A builder that cannot call the hook itself (a contract without that function) is paid by `claimBuilderFeesFor(builder, currencies)`: anyone may send it, and it always pays the builder itself, with the same one-unit rule. Such a contract must be able to hold and move ERC-20s, and must not let anyone sweep its balance. A builder that can call may refuse these claims on its account: `setClaimForBlocked(true)` (and `false` to accept them again). A refused claim reverts `ClaimForBlocked()`; `claimForBlocked(builder)` reads the setting. ```ts import { claimBuilderFeesForCall, readClaimForBlocked, setClaimForBlockedCall } from '@otomate/sdk'; await walletClient.sendTransaction(setClaimForBlockedCall(hook, true)); // sent by the builder await readClaimForBlocked(publicClient, hook, builder); // true ``` ## Builder code in the starter launchpad The starter launchpad puts your code on every trade it sends: set your address, rate and tag in its configuration file. See [Your own launchpad](/docs/developers#your-own-launchpad). # Data providers This page is for data providers and indexers: screeners, aggregators, analytics, wallets and terminals that list Otomate launches from their own pipelines. Everything here is read from the chain. Otomate's API and feed are optional shortcuts ([Otomate's own data](/docs/developers#otomates-own-data)). ## Launch model for indexers An Otomate launch is two things: - **A plain ERC-20 token**, 18 decimals, with no owner, no pause and no mint after creation. - **Its own Uniswap v4 pool**, whose hook is Otomate's launch pool hook. At creation the hook puts the whole supply into the pool, in two positions it owns. Those positions are the bonding curve and the market beyond it. There is no separate curve contract and no migration. The token trades in the same pool from its first block, before and after graduation. | Property | Value | | --- | --- | | Chains | Ink (57073) and Robinhood Chain (4663) | | AMM | Uniswap v4, the chain's standard PoolManager | | Pools per launch | One: currencies are the token and its pair in address order, `fee` 0, the snapshot's tick spacing (60 on today's presets), `hooks` = the pool's hook | | Pair | WETH, or another ERC-20 the config enables, such as OTO or a stock token ([the pairs per chain](/docs/developers#chains-and-contracts)). Follow the config's `QuoteConfigured` and `QuoteDisabled` events. No native ETH pool: routers wrap ETH on WETH pairs. | | LP fee | 0. Every fee is the hook's. | | Liquidity at creation | The whole supply, in two hook-owned positions (bonding and market) | | Graduation | A one-way latch in the hook. The pool stays the same. | | Quotes | The chain's V4 Quoter equals execution, with the swap's own hookData (empty, or a builder code) | ## Network and contracts for indexers **Ink (57073).** Explorer: [explorer.inkonchain.com](https://explorer.inkonchain.com). | Contract | Address | | --- | --- | | Uniswap v4 PoolManager | [`0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32`](https://explorer.inkonchain.com/address/0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32) | | Universal Router | [`0x112908daC86e20e7241B0927479Ea3Bf935d1fa0`](https://explorer.inkonchain.com/address/0x112908daC86e20e7241B0927479Ea3Bf935d1fa0) | | V4 Quoter | [`0x3972C00f7ed4885e145823eb7C655375d275A1C5`](https://explorer.inkonchain.com/address/0x3972C00f7ed4885e145823eb7C655375d275A1C5) | | StateView | [`0x76Fd297e2D437cd7f76d50F01AfE6160f86e9990`](https://explorer.inkonchain.com/address/0x76Fd297e2D437cd7f76d50F01AfE6160f86e9990) | | Permit2 | [`0x000000000022D473030F116dDEE9F6B43aC78BA3`](https://explorer.inkonchain.com/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | | WETH | [`0x4200000000000000000000000000000000000006`](https://explorer.inkonchain.com/address/0x4200000000000000000000000000000000000006) | **Robinhood Chain (4663).** Explorer: [robinhoodchain.blockscout.com](https://robinhoodchain.blockscout.com). | Contract | Address | | --- | --- | | Uniswap v4 PoolManager | [`0x8366a39CC670B4001A1121B8F6A443A643e40951`](https://robinhoodchain.blockscout.com/address/0x8366a39CC670B4001A1121B8F6A443A643e40951) | | Universal Router (hop-price tuple) | [`0x8876789976dEcBfCbBbe364623C63652db8C0904`](https://robinhoodchain.blockscout.com/address/0x8876789976dEcBfCbBbe364623C63652db8C0904) | | V4 Quoter | [`0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94`](https://robinhoodchain.blockscout.com/address/0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94) | | StateView | [`0xF3334192D15450CdD385c8B70e03f9A6bD9E673b`](https://robinhoodchain.blockscout.com/address/0xF3334192D15450CdD385c8B70e03f9A6bD9E673b) | | Permit2 | [`0x000000000022D473030F116dDEE9F6B43aC78BA3`](https://robinhoodchain.blockscout.com/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | | WETH | [`0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73`](https://robinhoodchain.blockscout.com/address/0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73) | **Otomate's launch contracts** have the same address on both chains. They are published here at deployment, with their ABIs. Check the signatures on this page against those ABIs. | Contract | Kind | Address | | --- | --- | --- | | Launch pool hook | Plain contract, not a proxy | To be published | | Factory | Upgradeable proxy (ERC-1967) | To be published | | Config | Upgradeable proxy (ERC-1967) | To be published | | Distributor | Upgradeable proxy (ERC-1967) | To be published | | Lens (batched views) | Upgradeable proxy (ERC-1967) | To be published | | Token deployer | Plain contract, not a proxy | To be published | **Track hook versions and factory versions separately.** - **Hooks.** The config names the hook for new launches and emits `LaunchHookSet(address indexed hook, uint64 indexed revision)`. A pool keeps the hook it was created with for its whole life; a newer hook serves only later launches. Index every hook the config ever named (`config.isLaunchHook(hook)`). The hook of one pool is `LaunchCreated.hook` or `factory.hookOf(poolId)`. A hook is not a proxy: pin its address and its runtime code hash, per chain (the runtime differs per chain). - **Factory and other proxies.** The factory, config, distributor and lens keep their proxy address. A new implementation emits `Upgraded(address indexed implementation)` on the proxy. Follow it. Otomate's rule for these contracts: an existing event never changes its fields; a changed field set gets a new event name. ## Discover launches One creation is one transaction. Anyone can send it, a bot or another app included. Its logs come in this order: 1. **Token deployer.** First the token's mint, `Transfer` from the zero address to the hook. Then `TokenDeployed(address indexed token, address indexed creator, address indexed recipient, uint256 supply)`. The recipient is the hook. 2. **PoolManager** `Initialize`, then two `ModifyLiquidity` from the hook (its two positions). Any rounding dust is burned (`Transfer` from the hook to the zero address). 3. **Hook** `LaunchRegistered(bytes32 indexed poolId, address indexed token, address indexed quote, PoolSnapshot snapshot, address creatorPayout, uint160 initialSqrtPriceX96)`. This registers the pool, with its full snapshot and its first price. 4. **Optional initial buy** by the creator: PoolManager `Swap`, hook `SwapFee` with `seq` 1, factory `InitialBuy(bytes32 indexed poolId, address indexed recipient, uint256 quoteIn, uint256 tokensOut)`. It carries no builder code, so it has no `BuilderFee`. 5. **Factory** `LaunchCreated`, last: ```solidity event LaunchCreated(bytes32 indexed poolId, address indexed token, address indexed creator, address hook, address quote, address collection, uint64 quoteRevision, uint64 globalRevision, bytes32 projectCommitment, string name, string symbol, string metadataURI, uint256 creationFee, bytes32 artworkCommitment); ``` With an NFT collection, the collection's own logs and the Forge's `SourceRegistered` come between steps 3 and 4. Transfers of the pair (the creator's payment, the hook's small pre-funding and the unused rest coming back) also appear in the transaction; they are not trades. `LaunchRegistered` and `LaunchCreated` are in the same transaction. Key the pool on `LaunchRegistered`: it comes before any trade of the pool. Take the name, symbol, creator, metadata URI and collection from `LaunchCreated`. - `creator` is the address that called `create`. - `collection` is the zero address for a token without an NFT collection. - `creationFee` is the fee paid in the chain's native currency. - `artworkCommitment` is the collection's first artwork direction. It is zero when the creator deferred the direction (the collection's `ArtworkFinalized` gives it later), and for a token without a collection. The snapshot, as the hook declares it: ```solidity struct FeeSnapshot { uint16 quoteFeeBps; uint16[5] shareBps; uint16 burnBps; uint16 creatorBurnBps; uint16 marketQuoteFeeBps; uint16 marketBurnBps; } struct SurchargeSnapshot { uint16 startBps; uint16 duration; uint8 halvings; } struct PoolSnapshot { address token; address quote; bool quoteIsCurrency0; int24 tickSpacing; int24 launchTick; int24 mintGateTick; int24 graduationTick; uint128 bondingLiquidity; uint128 marketLiquidity; uint256 threshold; uint256 minimumTrade; uint16 mintGateBps; uint40 launchedAt; FeeSnapshot fees; SurchargeSnapshot surcharge; } ``` **One read per token.** `factory.tokenInfo(token)` answers `metadataURI`, `profileURI`, `creator`, `profileAuthority`, `proposedProfileAuthority`, `quote`, `poolId`, `hook`, `collection` and `createdAt`. An unknown token answers `poolId` 0; it does not revert. `hook.poolIdOf(token)` and `hook.snapshot(poolId)` give the pool and its snapshot. **Without running an indexer:** the API's `GET /api/v1/{chain}/tokens?sort=new` lists launches, and the [real-time feed](/docs/developers#real-time-feed) pushes each one as a `launch` event at its mined block. ## Price from the pool Read the price from `sqrtPriceX96`, in the pool's real currency order: - `currency0` is the lower of the two addresses. - `sqrtPriceX96² / 2¹⁹²` is the price of `currency0` in `currency1`, in raw units. - When the token is `currency0`, that is the token's price in the pair. When the token is `currency1`, take its inverse. - Then adjust decimals: the token has 18; the pair has its own (`decimals()`; 18 for WETH). One whole token costs `raw × 10^(18 − pairDecimals)` whole pair units. `snapshot.quoteIsCurrency0` says which order a pool uses. Take the price after each trade from `SwapFee.sqrtPriceX96After` (equal to the PoolManager `Swap` event's price), and the current price from `StateView.getSlot0(poolId)`. The pool's first price is in the PoolManager's `Initialize` event and in `LaunchRegistered.initialSqrtPriceX96`. It is `sqrtAt(launchTick)` when the token is `currency0`, and one unit below it when the token is `currency1`. TypeScript, exact with bigints: ```ts const Q192 = 1n << 192n; /** One whole token in whole pair units, as an exact fraction. */ export function tokenPrice(sqrtPriceX96: bigint, token: string, quote: string, quoteDecimals: number) { const tokenIsCurrency0 = BigInt(token) < BigInt(quote); const square = sqrtPriceX96 * sqrtPriceX96; // currency1 per currency0, × 2^192 const num = (tokenIsCurrency0 ? square : Q192) * 10n ** 18n; // the token has 18 decimals const den = (tokenIsCurrency0 ? Q192 : square) * 10n ** BigInt(quoteDecimals); return { num, den }; } /** A fraction as a decimal string, rounded down. */ export function toDecimal({ num, den }: { num: bigint; den: bigint }, digits = 18) { const s = ((num * 10n ** BigInt(digits)) / den).toString().padStart(digits + 1, '0'); return `${s.slice(0, -digits)}.${s.slice(-digits)}`.replace(/\.?0+$/, ''); } ``` Python, exact with fractions: ```python from fractions import Fraction def token_price(sqrt_price_x96: int, token: str, quote: str, quote_decimals: int) -> Fraction: """One whole token in whole pair units.""" token_is_currency0 = int(token, 16) < int(quote, 16) ratio = Fraction(sqrt_price_x96 * sqrt_price_x96, 1 << 192) # currency1 raw per currency0 raw raw = ratio if token_is_currency0 else 1 / ratio # pair raw per token raw return raw * Fraction(10 ** 18, 10 ** quote_decimals) # the token has 18 decimals ``` Both give the same value for a pool and its mirror order: at tick −207,960 (token as `currency0`, WETH pair) and at tick 207,960 (token as `currency1`), one token costs 0.000000000930815279 WETH. **Do not derive the price from a trade's amounts.** `quoteAmount / tokenAmount` is what the trader paid per token, fees on both legs included. Use it for an average execution price, not for the pool price. Otomate's API computes the market cap from the circulating supply and the FDV from the initial supply, both times the price ([Supply and burns](/docs/developers#supply-and-burns)). For USD, multiply by your USD price of the pair. On a stock pair, price the pair token itself, not one share of the stock: an xStock wrapper's share is worth `convertToAssets` of its xStock, and a Robinhood Chain Stock Token keeps its raw balances through dividends and splits while its `uiMultiplier()` moves (Chainlink's feed of each Stock Token on Robinhood Chain prices the raw token, multiplier included). ## Trades from SwapFee The hook emits one `SwapFee` per swap. It is a complete trade record: ```solidity event SwapFee(bytes32 indexed poolId, address indexed sender, bool isBuy, uint256 quoteAmount, uint256 tokenAmount, uint256 quoteFee, uint256 surcharge, uint256 burn, uint160 sqrtPriceX96After, int24 tickAfter, uint32 seq); ``` | Field | Meaning | | --- | --- | | `poolId` | The pool (topic 1) | | `sender` | The PoolManager's swap caller: the router, not the trader | | `isBuy` | `true`: pair in, token out. `false`: token in, pair out | | `quoteAmount` | The trader's pair amount, as their balance change. Buy: paid, quote fee and builder fee included. Sell: received, after the quote fee and the builder fee. | | `tokenAmount` | The trader's token amount. Buy: received, after the burn. Sell: paid, burn included. | | `quoteFee` | The pool's quote fee taken, in the pair, launch surcharge included. It never includes the builder fee. | | `surcharge` | The launch surcharge part of `quoteFee` | | `burn` | Tokens taken from this swap, to be burned | | `sqrtPriceX96After`, `tickAfter` | The pool's price and tick after the swap | | `seq` | The pool's swap number: 1 for its first swap, then +1 on every swap, with no gaps | **Builder codes.** A swap whose hookData is not empty carries a [builder code](/docs/developers#builder-codes). The hook then emits one `BuilderFee` right after its `SwapFee`, with the same `poolId` and `seq`: ```solidity event BuilderFee(bytes32 indexed poolId, address indexed builder, bytes32 indexed tag, uint256 fee, uint16 feeBps, uint32 seq); ``` | Field | Meaning | | --- | --- | | `builder` | The builder's account (topic 2). The zero address for a tag alone, with `fee` and `feeBps` 0 | | `tag` | 32 bytes chosen by whoever built the swap (topic 3). The hook only copies it; nothing checks it | | `fee` | The builder fee, in the pair, kept by the hook for the builder. It is inside `quoteAmount`, never inside `quoteFee` | | `feeBps` | The rate applied: the code's rate, lowered inside the launch surcharge window when needed ([Fee rates in the snapshot](/docs/developers#fee-rates-in-the-snapshot)). 0 is attribution only | | `seq` | The `seq` of the swap's `SwapFee` | **Order of the logs.** For one swap: 1. The PoolManager's `Swap`. 2. The PoolManager's ERC-6909 `Transfer` logs that mint the hook's claims: the quote fee together with any builder fee, then the burn (each only when it is not zero). 3. The hook's `SwapFee`. It is the hook's first log after the `Swap`. 4. The hook's `BuilderFee`, when the swap carries a builder code. 5. `MintGateOpened`, then `Graduated`, when that swap latches them. **Rules for indexers:** - **Several swaps of one pool in one transaction are separate trades.** Each has its own `SwapFee` and `seq`. Do not merge them; do not drop them as ambiguous. - **A gap in `seq` means a missed log.** Expect `(last + 1) mod 2³²`: after 4,294,967,295 comes 0, then 1. `hook.swapSeq(poolId)` answers the last one. - **The `Swap` amounts are the AMM leg only**, before the hook's fees. A buy pays `Swap` pair amount + `quoteFee` + builder fee and receives `Swap` tokens − `burn`. A sell pays `Swap` tokens + `burn` and receives `Swap` pair amount − `quoteFee` − builder fee. The builder fee is `BuilderFee.fee`, 0 without a code. `SwapFee` already gives the trader's side; you never need the PoolManager's `Swap` firehose. - **Zero amounts happen.** A sell of a few wei can return 0 of the pair. Accept it. - **Volume** in the pair is the sum of `quoteAmount`. Otomate's API counts it that way. **Who traded.** `sender` is the router. Attribute the trade to a wallet in this order, and never with `tx.from` alone: 1. **The token flow.** On a buy, follow the token's `Transfer` out of the PoolManager after the `Swap`: the trader is the wallet that ends up with the tokens. On a sell, follow the `Transfer` into the PoolManager before it: the trader is the wallet the tokens came from, wherever the pair goes. Look through routers, Permit2, aggregator executors, the hook and addresses that pass the tokens on (net zero in the transaction), up to 8 hops, following the largest transfer at each one: an interface fee paid in the token before the rest never names the fee's recipient. The flow amount is the `Swap` amount minus the burn on buys, plus the burn on sells. This holds whatever the path: a smart account or a router's caller buying for another wallet credits that wallet. 2. **ERC-4337**, only when the flow names no wallet (the tokens are burned, stay in a router or a protocol contract, or are still being passed on after 8 hops). When a `UserOperationEvent` follows the trade in the transaction, the trader is its `sender`, never the bundler. Read the flow inside that operation's logs: a bundle's operations never mix. EntryPoints v0.6 [`0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`](https://explorer.inkonchain.com/address/0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789), v0.7 [`0x0000000071727De22E5E9d8BAf0edAc6f37da032`](https://explorer.inkonchain.com/address/0x0000000071727De22E5E9d8BAf0edAc6f37da032) and v0.8 [`0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`](https://explorer.inkonchain.com/address/0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108), the same on both chains. 3. **A router's payer**, only when the flow names no wallet. Otomate's router (`0x173b1e9eCC06d53b239754602177F1707BFE76A1`, the same on every chain) emits `Bought(address indexed payer, address indexed recipient, address indexed token, uint256 quoteIn, uint256 tokensOut, uint256 quoteRefund)` or `Sold(address indexed payer, address indexed recipient, address indexed token, uint256 tokensIn, uint256 quoteOut)` after the hook's `SwapFee`. The payer is the router's caller (a wallet or a contract); tokens never pass through the router. 4. **Fallback:** without any transfer of the trade's tokens, the `Swap` sender (for example an arbitrage contract), unless it is a router. Otomate's API and feed give the result as `trader`, with `traderBasis` (`user-operation`, `router`, `caller`, `token-flow`, `event` or `unknown`); on launch pools, `user-operation` means the token flow named no wallet. They also give each trade's builder code as `builder`: `address` (`null` for a tag alone), the fee (`fee` in the API, `feeRaw` in the feed), `feeBps` and `tag`. It is `null` for a trade without a code. ## Liquidity of a launch pool The hook creates two positions in its own pool (salts 1 and 2) and owns them. Their ticks are in the snapshot, already in the pool's orientation: | Position | Token is `currency0` | Token is `currency1` | Holds at creation | | --- | --- | --- | --- | | Bonding | `[launchTick, graduationTick]` | `[graduationTick, launchTick]` | The bonding supply | | Market | `[graduationTick, maxUsableTick]` | `[minUsableTick, graduationTick]` | The market supply | - **Amounts at a price.** Use the snapshot's `bondingLiquidity` and `marketLiquidity` with the standard v4 amount formulas (`SqrtPriceMath.getAmount0Delta` and `getAmount1Delta`, rounded down) over each range. The SDK's `rangeAmounts(launchPoolRanges(snapshot), sqrtPriceX96)` does it. - **The hook never removes its liquidity.** It has no function that does. Its positions change only through swaps. - **Outside liquidity** is refused during bonding (`LiquidityLockedDuringBonding`). After graduation it is allowed only on the token-expensive side of the launch price: `tickLower ≥ launchTick` when the token is `currency0`, `tickUpper ≤ launchTick` when it is `currency1` (`LiquidityBelowLaunchPrice` otherwise). Donations are always refused. Outside positions appear as PoolManager `ModifyLiquidity` from another sender. - **LP fee is 0.** An outside LP earns no fee: every fee is the hook's. Outside liquidity is depth for traders, not income. - **TVL.** The PoolManager holds the tokens of every v4 pool on the chain, so its balances are not one pool's TVL. Compute from positions. Otomate's API reports the pair and tokens in the hook's own positions at the current price (`market.liquidity`); it does not count outside positions. - **Other pools.** Anyone can open another pool for a launch token elsewhere (v2, v3, another v4 hook). Only the pool registered by `LaunchRegistered` is the launch pool. Progress and graduation read only that pool. ## Progress and graduation **Progress** is the pair held by the bonding position, over the threshold: - `progressBps = min(10,000, bondingQuote × 10,000 / threshold)`, with `threshold` from the snapshot. - On chain: `hook.bondingQuote(poolId)` and `hook.bondingProgressBps(poolId)`. - Off chain, from any `sqrtPriceX96` (P), with L = `bondingLiquidity`, rounded down: - token is `currency0`: `bondingQuote = getAmount1Delta(sqrtAt(launchTick), min(P, sqrtAt(graduationTick)), L)`, or 0 when P ≤ `sqrtAt(launchTick)`; - token is `currency1`: `bondingQuote = getAmount0Delta(max(P, sqrtAt(graduationTick)), sqrtAt(launchTick), L)`, or 0 when P ≥ `sqrtAt(launchTick)`. **Two one-way latches** in the hook: | Milestone | Where | Event | | --- | --- | --- | | Mint gate | `mintGateBps` of the threshold (`mintGateTick`); 5,000 on today's presets | `MintGateOpened` | | Graduation | The threshold (`graduationTick`) | `Graduated` | ```solidity event MintGateOpened(bytes32 indexed poolId, uint40 at, uint64 l2Block, uint160 sqrtPriceX96, int24 tick, uint32 seq, uint256 feesOrdinary, uint256 feesSurcharge, uint128 liquidity); event Graduated(bytes32 indexed poolId, uint40 at, uint64 l2Block, uint160 sqrtPriceX96, int24 tick, uint32 seq, uint256 feesOrdinary, uint256 feesSurcharge, uint128 liquidity); ``` - The first swap that ends at or beyond a milestone latches it. Nothing unlatches it. Graduation also latches the mint gate: when one swap latches both, `MintGateOpened` comes first, then `Graduated`, with the same values. - `sqrtPriceX96`, `tick` and `seq` are those of the latching swap (its `SwapFee` has the same `seq`). `feesOrdinary` and `feesSurcharge` are the pool's cumulative quote fees after that swap, as `hook.feesAccrued(poolId)` answers them. `liquidity` is the pool's active liquidity after it. - **The protocol flag is authoritative:** the `Graduated` event, or `hook.graduated(poolId)`. Never recompute graduation from the price after the fact. A buy and a sell in one transaction can latch it and bring the price back; the price can also fall after graduation. After graduation, `bondingProgressBps` answers 10,000 whatever the price. - `l2Block` is the chain's own block: `block.number` on Ink, the Arbitrum L2 block on Robinhood Chain (the log's block number on both). - **Graduation does not move liquidity.** No new pool, no new pair, no LP tokens. The same pool keeps trading; the market position takes over above the graduation price. - **Graduation switches the rates** from the next swap on: see [Fee rates in the snapshot](/docs/developers#fee-rates-in-the-snapshot). ## Fee rates in the snapshot Each pool's rates are numbers in its snapshot (`LaunchRegistered`, or `hook.snapshot(poolId)`). Read them per pool; show them as they are. | Snapshot field | Meaning | | --- | --- | | `fees.quoteFeeBps` | The curve's quote fee, taken in the pair on every swap until graduation. It is the sum of the five shares. | | `fees.shareBps[5]` | The shares of the quote fee, in this order: creator, NFT holders, treasury, OTO burn, global NFT holders. After graduation they keep their proportions. | | `fees.burnBps` | The curve's token burn, taken in the token on every swap until graduation, and burned | | `fees.creatorBurnBps` | The creator's burn choice, the part of `burnBps` the creator chose to burn instead of receiving | | `fees.marketQuoteFeeBps`, `fees.marketBurnBps` | The quote fee and the token burn after graduation. Each is at most its curve rate. | | `surcharge.startBps`, `duration`, `halvings` | The launch surcharge on buys, from `launchedAt` | - **Which rates apply.** The curve's pair (`quoteFeeBps`, `burnBps`) applies to every swap up to and including the swap that graduates the pool. From the next swap on, the pair after graduation (`marketQuoteFeeBps`, `marketBurnBps`) applies. Below, `q` and `b` are the pair that applies to a swap. - **What a trader pays.** `q + b` of the trade's value, on buys and sells, to rounding. The token leg is grossed up so that this holds whichever leg is specified. A builder fee comes on top. - **The token-leg burn.** On a buy, the burn is taken from the tokens out. On a sell, from the tokens in. `SwapFee.burn` gives the amount. - **The launch surcharge** applies to buys during the first `duration` seconds after `launchedAt` (`duration` is at most 3,600). With `elapsed` the seconds since `launchedAt`, it is `startBps >> floor(elapsed × halvings / duration)`, or, when `halvings` is 0, `floor(startBps × (duration − elapsed) / duration)` (a straight line down to 0). It is 0 once `elapsed ≥ duration`. It is capped at `9,900 − q − b`, so that the swap's total stays at most 99 %, then added to `q` for that buy. The creator's initial buy in the creation transaction pays none. `SwapFee.surcharge` is its part of `quoteFee`; `hook.surchargeBps(poolId, sender)` answers the rate now. - **Builder fee.** A swap may carry a [builder code](/docs/developers#builder-codes). The trader pays the builder fee on top of the pool's fees, in the pair, on buys and sells, at the code's rate on the same pair amount as the quote fee: at most 200 bps (`MAX_BUILDER_FEE_BPS`). The pool's own fee and burn are the same as without a code. Inside the surcharge window, the hook lowers the builder rate when needed so that the swap's total stays at most 99 %; it never lowers the surcharge. `BuilderFee.feeBps` is the rate applied. A code with `builder` = the zero address and `feeBps` 0 is a tag alone. The tag is declarative: the hook only copies it. - **LP fee is 0.** The PoolManager's `Swap.fee` is Uniswap's own fee field; Otomate's rates are not in it. - **Where fees go.** Quote fees accrue in the hook as PoolManager claims. Anyone calls `distributor.distribute(poolIds)`, which calls the hook's `release` (also open to anyone). The hook sends what each share is owed to the distributor, burns the pending tokens and emits `Released(bytes32 indexed poolId, uint256[5] amounts, uint256 burned, uint256 releasedOrdinary, uint256 releasedSurcharge)`. `amounts` follow the shares' order; the treasury amount includes the surcharge. `releasedOrdinary` and `releasedSurcharge` are the pool's fees released so far, cumulative. When the PoolManager refuses the transfer, the hook emits `ReleaseFailed(bytes32 indexed poolId, address indexed quote, uint256 amount)` and the amounts stay owed for the next release. The distributor then pays each share (`Accounted`, `Paid`, `PaymentFailed`). - **Builder fees** stay in the hook, in each builder's account per currency (`hook.builderFees(builder, currency)`), apart from the pool's shares. `distribute` does not move them. The builder claims them: `BuilderFeesClaimed(address indexed builder, address indexed currency, address indexed recipient, uint256 amount)`. `ClaimForBlockedSet(address indexed builder, bool blocked)` records whether a builder refuses claims that others send for it (`claimBuilderFeesFor`). The API's token sheet gives the same rates as `fees`: `buyBps`, `sellBps`, `quoteFeeBps` and `burnBps` for the rates that apply now, `phase` (`curve` or `graduated`), both pairs (`curve` and `afterGraduation`), `shares`, and `surcharge` (with its `currentBps` and `activeUntil`). ## Metadata and token profile **Base metadata.** `LaunchCreated.metadataURI`, also in `factory.tokenInfo(token).metadataURI`. Launches prepared through Otomate's point of upload use `ipfs://` raw CIDs. The factory only bounds a URI's length (128 bytes, `MAX_URI_BYTES`), so handle other schemes as you do for any launchpad. The format, `otomate.token-metadata.v1`: ```json { "schema": "otomate.token-metadata.v1", "name": "Moon Cat", "symbol": "MCAT", "description": "A cat on the moon.", "image": "ipfs://bafkrei…", "banner": "ipfs://bafkrei…", "external_url": "https://mooncat.example", "website": "https://mooncat.example", "twitter": "https://x.com/mooncat", "telegram": "https://t.me/mooncat", "discord": "https://discord.gg/…" } ``` - `name`, `symbol` and `image` are always present; the other fields are optional. Ignore fields you do not know (an `otomate` object may follow). - `name` and `symbol` equal the token's on-chain `name()` and `symbol()`. On chain, a name has 1 to 64 bytes and a symbol 1 to 31 bytes (`MAX_NAME_BYTES`, `MAX_SYMBOL_BYTES`). - Through the point of upload, the logo is square (at most 512 px, PNG) and the banner is 1600 × 600. **Editable profile.** The token's profile authority (the creator at first) may publish a profile: `factory.setProfileURI(token, uri)` emits `TokenProfileUpdated(bytes32 indexed poolId, address indexed token, string uri)`. The format, `otomate.token-profile.v1`: `description`, `banner`, `external_url`, `website`, `twitter`, `telegram`, `discord`, `farcaster`. The latest profile gives the description, banner and links; an empty URI clears it. The authority moves in two steps: `ProfileAuthorityProposed(bytes32 indexed poolId, address indexed token, address indexed proposed, address current)` (a proposed zero address cancels a pending proposal), then `ProfileAuthorityChanged(bytes32 indexed poolId, address indexed token, address indexed next, address previous)`. **Name, symbol and logo never change.** They come from the token and the base metadata. A profile cannot replace them. **Moderation.** Otomate moderates what its own site and API show. The API marks each launch `visible`, `pending` or `hidden`; a hidden launch has `null` name and metadata there and is left out of the token list. It still exists on chain and trades. Providers apply their own policy. ## Supply and burns - **Minted once.** The whole supply is minted at creation, to the hook (`TokenDeployed.supply`, and the token's `Transfer` from the zero address). The token cannot mint again. - **Burns are `Transfer` to the zero address**, and lower `totalSupply()`: - the hook's release of swap burns (`from` = the hook), when anyone calls `distribute` or the hook's `release`; - the Forge, which burns tokens for sealed NFT mints (`from` = the Forge); - rounding dust at creation (`from` = the hook); - any holder calling `burn`. - **Pending burn.** Tokens taken by swaps and not yet burned wait as the hook's claims in the PoolManager: `hook.pendingBurn(poolId)`. Nothing but a burn can take them out. - **Circulating supply** = initial supply − burned − pending burn, that is `totalSupply() − pendingBurn(poolId)`. Otomate's API uses this figure for market cap; FDV uses the initial supply. ## Indexing checklist 1. **Follow the config.** Read `LaunchHookSet` from the config's deployment block. Keep the list of every hook it named. 2. **Subscribe to the hooks' logs only.** One `eth_getLogs` filter on the hook addresses gives every pool (`LaunchRegistered`), trade (`SwapFee`, and `BuilderFee` for builder codes), latch (`MintGateOpened`, `Graduated`), release and builder claim. Add the factory for `LaunchCreated`, `TokenProfileUpdated` and the profile authority events. 3. **Pair nothing with the PoolManager.** `SwapFee` is a complete trade; a `BuilderFee` joins it by `poolId` and `seq`. The PoolManager's `Swap` is only an optional cross-check. 4. **Read the snapshot once per pool**, from `LaunchRegistered` or `hook.snapshot(poolId)`. 5. **Check `seq` per pool.** A gap means a missed log: read that range again. 6. **Keep several swaps of one pool in one transaction** as separate trades. 7. **Order by block, then log index.** 8. **Handle reorgs.** Store block hashes; when a stored hash no longer matches, drop what you hold above the last common block and read again. Otomate's feed sends `reorg` markers for this. 9. **Mark finality.** Keep records above the chain's `finalized` block as provisional. Otomate's feed sends `final` markers. 10. **Take graduation from the protocol** (`Graduated`, `graduated(poolId)`), never from the price. 11. **Attribute traders** with the rules in [Trades from SwapFee](/docs/developers#trades-from-swapfee). Never `tx.from` alone. 12. **Follow `Upgraded`** on the factory, config, distributor and lens proxies. Pin each hook's address and code hash per chain. 13. **Exclude technical holders.** Leave the PoolManager, every hook, the zero address and `0x000000000000000000000000000000000000dEaD` out of holder counts. Otomate also leaves out the factory, the Forge, the distributor and the NFT vault. 14. **Count burns on the launch token only.** On Robinhood Chain, WETH logs an unwrap as a `Transfer` to the zero address: that is WETH, not a launch burn. 15. **Keep amounts as integers**: raw units, 18 decimals for tokens, the pair's own decimals for the pair. No floats. 16. **Find NFT collections only through `LaunchCreated.collection`** or `factory.collectionOf(poolId)`, never through beacon events. ## History: earlier Otomate tokens - **Earlier Otomate tokens are a different family.** They use bonding curve contracts (launchers 035 to 105), a fee witness, and after graduation Uniswap v4 pools on the graduated pool hook [`0x7353B76Ce326A3401162eDB1CD802f88e5EB60Cc`](https://explorer.inkonchain.com/address/0x7353B76Ce326A3401162eDB1CD802f88e5EB60Cc) on Ink, plus relay pools on [`0x92fdd38CdAc2345A689B2e0382047BF458152888`](https://explorer.inkonchain.com/address/0x92fdd38CdAc2345A689B2e0382047BF458152888). Robinhood Chain has its own contracts for that family. Index them with the v1 guide: [Build with Otomate](/docs/developers#build-with-otomate) and [Trade Otomate tokens from a bot](/docs/developers#trade-otomate-tokens-from-a-bot). - **Launches described on this page start at the deployment** of the launch contracts. There is no earlier history to backfill: start at the config's deployment block, published with the addresses. - A token belongs to one family. Tell them apart by the creating contract: `LaunchRegistered` on a hook the config named means this family. ## Otomate's own data You can skip running an indexer, or check yours against Otomate's. Every surface is free to start: no key is needed, and a key signed by a wallet raises the limits ([Free to start](/docs/developers#free-to-start)). | Surface | Where | Notes | | --- | --- | --- | | Public API v1 | `/api/v1/{chain}/…` on each chain's API origin ([Chains and contracts](/docs/developers#chains-and-contracts)) | Launch lists, token sheet, trades, candles, holders, creators, wallets, quotes. [API v1](/docs/developers#api-v1) | | OpenAPI | `/api/v1/openapi.json`, and [openapi.v1.json](/docs/kit/openapi.v1.json) | The machine-readable reference | | Real-time feed | `/api/v1/{chain}/feed` (SSE), `/feed/ws` (WebSocket), `/feed/events` (pages) | Topics `launch`, `trade`, `graduation`, `mint_gate`; resumable by cursor; `reorg` and `final` markers. [Real-time feed](/docs/developers#real-time-feed) | | Token list | `/api/v1/{chain}/tokenlist.json` | tokenlists.org schema. Visible launches only, tagged `bonding` or `graduated`, with `quote`, `hook`, `tickSpacing` and `launchedAt` in `extensions` | | Status | `/api/v1/{chain}/status` | The indexer's latest and finalized blocks, and the feed's health | Amounts are integers as decimal strings (`raw`), often with their exact decimal value. Every record says whether it is `mined` or `final`. # Coming from pump.fun If you built on pump.fun, directly or through PumpPortal, most of what you know maps one to one. The main difference: an Otomate launch never migrates. Its bonding curve is a position in its own Uniswap v4 pool, and graduation happens in that same pool. Otomate runs on **Ink (chain ID 57073)** and **Robinhood Chain (chain ID 4663)**, both EVM chains. Addresses are `0x…`, amounts are integers in each token's smallest unit, and you sign with an EVM wallet. ## pump.fun concepts on Otomate | On pump.fun or PumpPortal | On Otomate | Notes | | --- | --- | --- | | Bonding curve | The launch pool's **bonding position**: hook-owned Uniswap v4 liquidity from the launch price to the graduation price | Trade it with any v4 router; quote it with the chain's V4 Quoter | | Bonding curve progress | `progressBps` in the API and the feed, `bondingProgressBps(poolId)` on the hook | The pair held by the bonding position over the pool's threshold | | Graduation, then migration to a PumpSwap pool (Raydium in earlier years) | **A one-way latch** in the same pool (`Graduated` event) | No migration, no new pool, no LP tokens. The market position continues above the graduation price. | | `subscribeMigration` | Feed topic `graduation` | — | | Creator fees, collected from pump.fun's programs | The **creator share** of each pool's quote fee, paid by the distributor to the creator's payout address | Anyone calls `distribute(poolIds)`: there is nothing to claim. A creator may also choose to burn part of the share in token at each trade. | | Metadata upload to IPFS (pump.fun's former upload endpoint, or your own pinning as PumpPortal now documents) | Otomate's **point of upload**: `POST /api/v1/{chain}/metadata` | JSON body; images as `data:image/…;base64,` or an `https` URL; optional banner. Answers the `metadataURI`. Or pin `otomate.token-metadata.v1` yourself. | | `tokenMetadata` `{name, symbol, uri}` | `name`, `symbol`, `metadataURI` in the factory call | All three appear in the factory's `LaunchCreated` event | | `action: "create"` | The SDK's `createToken`, `POST /api/v1/{chain}/create-transaction`, or the factory directly | Anyone can create, a bot included. The creation fee is paid in ETH. | | Local Transaction API (`POST https://pumpportal.fun/api/trade-local`) | `POST /api/v1/{chain}/swap-transaction`, or the SDK's `buy` and `sell` | Answers the approvals and the transaction, unsigned. You sign. | | Lightning Transaction API (sends for you) | No equivalent | Otomate never holds your keys | | `subscribeNewToken` | Feed topic `launch` | — | | `subscribeTokenTrade` with `keys` | Feed topic `trade` with `tokens=0x…,0x…` | At most 100 tokens per connection | | `subscribeAccountTrade` | No wallet filter on the feed | Filter `trade` events on `trader`, or read `GET /api/v1/{chain}/wallets/{address}` | | `wss://pumpportal.fun/api/data` | `GET /api/v1/{chain}/feed` (SSE) or `/api/v1/{chain}/feed/ws` (WebSocket) | Filters go in the URL. Resumable by cursor, with `reorg` and `final` markers. | | Mint address | **Token address** (ERC-20, 18 decimals) | Known before creation: `predictToken`. The same creator, salt, name, symbol and pair give the same address on both chains when both create with the same hook and give the pair the same token supply. | | Mint keypair | A `salt` (32 bytes) | No extra signer | | SOL | **WETH**, or another pair the config enables (OTO, a stock token: [Chains and contracts](/docs/developers#chains-and-contracts)) | ETH works on WETH pairs: the router wraps and unwraps it | | `denominatedInSol`, `amount` | `amountIn`, always the input | Exact input: the pair amount on a buy, the token amount on a sell | | `amount: "100%"` on a sell | Read `balanceOf` and pass it | — | | `slippage` (percent) | `slippageBps` (basis points) | 100 bps = 1 % | | `priorityFee` | None | Ordinary L2 gas, set by your wallet | | `pool`: `pump`, `pump-amm`, `raydium`, `auto`… | None | One pool per token, the launch pool, before and after graduation | | Dev buy (`amount` with `create`) | `initialBuy`, in the creation transaction | Exempt from the launch surcharge that applies to other buys in the first seconds | | One chain | Two chains, one API origin each | Paths `/api/v1/ink/…` and `/api/v1/robinhood/…` ([Chains and contracts](/docs/developers#chains-and-contracts)) | Rates are numbers in each pool's snapshot: a quote fee in the pair on every buy and sell, and a burn in the token when the creator chose one. Read them per pool ([How a swap is charged](/docs/developers#how-a-swap-is-charged)). ## Create and buy side by side **pump.fun through PumpPortal's Local Transaction API** (Solana, simplified): ```ts const mint = Keypair.generate(); const uri = await pinMetadata({ name, symbol, description, image }); // your own IPFS pinning const res = await fetch('https://pumpportal.fun/api/trade-local', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ publicKey: wallet.publicKey.toBase58(), action: 'create', tokenMetadata: { name, symbol, uri }, mint: mint.publicKey.toBase58(), denominatedInSol: 'true', amount: 0.1, slippage: 10, priorityFee: 0.0005, pool: 'pump' }), }); // Deserialize the answer, sign it with the mint keypair and your wallet, send it. // A later buy: the same endpoint with action 'buy', the mint, an amount, slippage and a priority fee. ``` **Otomate with the SDK** (`@otomate/sdk`, TypeScript and viem): ```ts import { createOtomate } from '@otomate/sdk'; import { readFile } from 'node:fs/promises'; const otomate = createOtomate({ chain: 'ink', walletClient }); // the SDK carries the launch contracts, as deployed const created = await otomate.createToken( { name: 'Moon Cat', symbol: 'MCAT', description: 'A cat on the moon.', image: await readFile('logo.png') }, { quote: otomate.chain.weth, creatorBps: 60, initialBuy: 10n ** 17n }, // dev buy: 0.1 ETH, same transaction ); await otomate.buy(created.token, 5n * 10n ** 16n); // 0.05 ETH on a WETH pair await otomate.sell(created.token, created.tokensOut / 2n); // half of the dev buy ``` `createToken` uploads the metadata, asks the API to build the factory call, checks that it goes to the official factory, signs and waits for the receipt. `buy` and `sell` quote with the V4 Quoter, send the approvals they need (each for exactly the amount) and trade through Otomate's router, then return the hook's `SwapFee` record. `creatorBps` must lie within the config's bounds ([Create tokens](/docs/developers#create-tokens)). **Otomate from any language**, the PumpPortal `trade-local` way: ```bash curl -s -X POST "https://api.otomate.fun/api/v1/ink/swap-transaction" \ -H 'content-type: application/json' \ -d '{"token":"0x…","side":"buy","amountIn":"50000000000000000","account":"0xYourWallet","slippageBps":100,"nativeEth":true}' ``` The answer holds `approvals` (send them first, in order; empty for ETH in), `transaction` (`to`, `data`, `value`) and the `quote` with every fee. Sign and send them from `account`. ## Stream launches and trades like PumpPortal PumpPortal subscribes by message on one WebSocket: ```ts const ws = new WebSocket('wss://pumpportal.fun/api/data'); ws.onopen = () => { ws.send(JSON.stringify({ method: 'subscribeNewToken' })); ws.send(JSON.stringify({ method: 'subscribeTokenTrade', keys: [mint] })); }; ``` Otomate's feed takes its filter in the URL, and resumes where you left off: ```ts const feed = otomate.feed({ topics: ['launch', 'trade', 'graduation'] }) .on('launch', e => console.log('new', e.token, e.data.name, e.data.symbol)) .on('trade', e => console.log(e.data.side, e.token, e.data.quoteRaw, e.data.trader)) .on('graduation', e => console.log('graduated', e.token)) .on('reorg', m => dropAbove(BigInt(m.afterBlock))) // drop what you stored above this block .start(); // Store feed.cursor; pass { cursor } to otomate.feed() to resume after a restart. ``` The same feed without the SDK: `wss://api.otomate.fun/api/v1/ink/feed/ws?topics=launch,trade`. Every event says whether its block is `mined` or `final` ([Real-time feed](/docs/developers#real-time-feed)). ## What works differently - **No migration.** Graduation is a latch in the same pool. There are no LP tokens to burn, and the hook has no function that removes its liquidity. - **Fees in the pair, a burn if the creator chose one.** Each swap pays a quote fee in the pair; when the creator chose to burn part of their share, a burn in the token too, and the burned tokens leave the supply. The V4 Quoter's answer includes everything. - **Launch surcharge.** Buys in the first seconds pay an extra quote fee that decreases quickly; the creator's initial buy pays none. - **Builder codes.** A front end, bot or terminal can earn a fee on the trades it sends ([Builder codes](/docs/developers#builder-codes)). - **Reorgs and finality.** Every API record and feed event is `mined` or `final`; the feed tells you when to drop events after a reorg. - **Moderation.** Otomate's site and API hide launches its moderation refuses. The chain does not: a hidden launch still trades. # AI agents An AI agent can do everything on Otomate a bot can: find launches, read a token, quote, trade, create tokens, earn builder fees, and pay for premium API requests. It can also prepare a trade that its user signs on otomate.fun with their own wallet. Three tools make it easy. | Tool | What it gives the agent | | --- | --- | | The MCP server | Ready-made tools for Claude Code, Cursor and any MCP client | | `llms.txt` and `llms-full.txt` | These documents as plain text, to read in one request | | The prompt template | A starting prompt to build a launchpad front end in one go | ## The MCP server `otomate-mcp` reads and builds; it never signs and never holds a key. The user signs every transaction in their own wallet. | Tool | Does | | --- | --- | | `list_launches` | Lists launches (new, graduating, graduated, trending, gainers), with filters and search | | `get_token`, `get_trades`, `get_candles`, `get_holders` | Reads one launch | | `get_creator`, `get_wallet` | A creator's track record; a wallet's positions and what it can claim | | `follow_launches` | Reads the real-time feed from a cursor (call it again with the cursor it returns) | | `quote` | Quotes a buy or a sell, with every fee | | `build_swap` | Builds a swap ready to sign (Ink or Robinhood Chain), with your builder code if set | | `upload_metadata`, `build_create` | Uploads a token's metadata and builds its creation, ready to sign | | `builder_code` | Encodes a builder code (address, rate, tag) and builds its claims and the claim-for opt-out | | `collection_artwork` | Reads whether a collection's art is final (the creator chooses it on the Otomate site) | | `prepare_trade` | Prepares a trade the user signs on otomate.fun: a signing link and a summary (Ink or Robinhood Chain, v2 launches and OTO) | | `trade_status` | Follows a trade: by its transaction hash, or by wallet and token | Before it returns a swap, `build_swap` checks what the API built. It rebuilds the call locally and compares it byte for byte; its `to` must be Otomate's router (whose code it reads on chain) or the chain's Universal Router. It also compares the API's quote with the chain's own quote of the same trade: the V4 Quoter at the latest block, through `OTOMATE_RPC_URL_` or else the chain's public RPC. - An API quote more than 1 % above the chain's is refused with `QUOTE_ABOVE_ONCHAIN`, which gives both amounts and the gap. Nothing is returned to sign: build the swap again for a fresh quote. - If the chain's quote cannot be read (the node fails, or the Quoter reverts), the swap is refused with `RPC_ERROR`, never built on the API's quote alone. - The minimum out sent is the stricter of the API's and the one from the chain's quote. When it is the chain's (`onchainQuote.minimumOutFrom` is `onchain`), `quote.amountOut` and `quote.executionPrice` are the chain's too: show those with `minimumOut`. An API quote more than 1 % below the chain's also adds a `warning`. Configuration, all optional: `OTOMATE_CHAIN` (`ink` by default), `OTOMATE_API_KEY`, `OTOMATE_BUILDER`, `OTOMATE_BUILDER_FEE_BPS` and `OTOMATE_BUILDER_TAG`, `OTOMATE_RPC_URL_INK`, `OTOMATE_RPC_URL_ROBINHOOD`, and `OTOMATE_SITE_URL` (the site its signing links open; `https://www.otomate.fun` by default). Once the package is published: ```bash claude mcp add otomate -- npx -y @otomate/mcp-server ``` ```json { "mcpServers": { "otomate": { "command": "npx", "args": ["-y", "@otomate/mcp-server"], "env": { "OTOMATE_CHAIN": "ink" } } } } ``` (the second block goes in Cursor's `.cursor/mcp.json`). ## Trade from your assistant With `prepare_trade`, any assistant connected to the MCP server (Claude, ChatGPT, Grok…) prepares a trade that its user signs on otomate.fun, in their own wallet. The assistant never signs and Otomate holds no key. 1. The assistant calls `prepare_trade` with the token, the side, the exact amount in, what pays (`pay`: ETH, the pair token, or for OTO a stablecoin), the slippage and, if it knows it, the user's wallet. 2. It shows the user the `summary` and the `signingUrl`, a link to `https://www.otomate.fun/sign`. 3. The user opens the link, connects their wallet as on the site, reviews the trade and signs the exact approval, if one is needed, then the swap. 4. The page shows the transaction hash while it confirms, then the outcome; the assistant follows the trade with `trade_status`, by that hash, the wallet and the trade it prepared. The link carries the intent only: the chain, the token, the side, the amount, the payment, the slippage and the expected wallet. It never carries a transaction. The page builds the trade itself with the site's own ticket: a fresh quote from the chain, the exact approval, the deadline, the minimum and the checks of the pool on chain. A link changed on its way can only change what the page shows the user before they sign, and the page refuses a link with a field it does not use, a field given twice or a value out of its format. The page also flags what is unusual: a slippage above 1 %, a wallet other than the one the link names (no ticket until the user switches accounts or chooses to trade with the wallet connected; nothing is ever sent from another wallet), and a launch named like $OTO, ETH or a stablecoin, look-alike spellings included (Otomate's own $OTO is marked as such); and it stops on a token Otomate does not list or hides. On OTO's ticket, a price impact above 15 % waits for the user to accept it on the page before anything is signed. The page never opens inside another site's frame. `prepare_trade` gives no link for a token the page stops on, nor for a trade the chain refuses; its estimate is the chain's own quote, and the page quotes again before anything is signed. `prepare_trade` also gives OTO's price impact, measured on its pools. `trade_status` says a trade is done only when the chain proves it (the wallet sent it through a known router, and the token asked about moved against ETH or a known token, on the side and for the amount prepared), never without the wallet and that token, and it says what was traded: a smart account's operation that failed inside its bundle is said to have failed. Signing links cover the v2 launches and OTO on Ink and Robinhood Chain. ## Documents for models - [/llms.txt](/llms.txt): a short index of these documents, with links. - [/llms-full.txt](/llms-full.txt): every developer page in one text file, with the API reference summary, types and errors. - [/docs/kit/openapi.v1.json](/docs/kit/openapi.v1.json): the API's exact schema. ## Build a launchpad in one prompt Give this to your coding assistant, with your values filled in ([launchpad-prompt.md](/docs/kit/launchpad-prompt.md)): ```text Build a token launchpad web app on Otomate with the @otomate/sdk package (docs: https://www.otomate.fun/llms-full.txt). Brand: , colors / , logo . Chains: Ink (and Robinhood Chain). Builder code: , on every trade the app sends. Pages: Explore (new, graduating, graduated, trending, gainers; live new launches from the feed), a token page (price chart from candles, live trades, holders, buy and sell ticket), Create (name, symbol, description, links, logo, banner, pair, my share, first buy; createToken), a creator page. Connect the user's injected wallet; never handle private keys. Show fee rates as numbers from the API. Start from the starter launchpad if you can. ``` The starter launchpad already does all of this: copy it and change one configuration file. ## Agents that pay their own way - **x402.** Where enabled, an API request above the budget answers `402 Payment Required` with its price and terms (USDC on Base). An agent with an x402 client pays and retries automatically, without an account or a key; it pays only for answers that succeed. - **Approvals without a separate transaction.** Agents trade launch tokens through Otomate's router with one approval for exactly the trade, which `permit` (EIP-2612: the holder signs an allowance off chain, anyone sends it) can replace, or through the Universal Router with Permit2. `permit` takes 65-byte signatures only: EOAs, EIP-7702 EOAs, and ERC-1271 wallets whose signature is 65 bytes. Other smart wallets (multi-owner Safes, passkey wallets) use an approval or Permit2. Launch tokens do not implement EIP-3009. The SDK has `signPermit` and `permitCall`. - **Wallets.** An agent should trade from a wallet its user controls and limits, with a spending cap; never paste a private key into a prompt. Without a wallet of its own, an assistant gives its user a signing link (`prepare_trade`): the user signs in their own wallet. ## Good practice for agents - Quote right before trading, and pass a minimum (`slippageBps`). - Wait out the launch protection on new launches (see [Before you trade](/docs/developers#before-you-trade)). - Check that a creation's `to` is the official factory and a swap's `to` Otomate's router or the chain's Universal Router before signing. The MCP server's `build_swap` checks it, with the router's code on chain and the API's quote against the chain's (see [The MCP server](/docs/developers#the-mcp-server)). - Read rates from each pool; never assume them. # Status and security ## Status [otomate.fun/status](/status) shows, for each chain, how far the indexer is (latest and finalized blocks and their age) and whether the real-time feed is serving. Machines read the same from `GET /api/v1/{chain}/status`. ## Report a vulnerability Open a ticket on the [Otomate Discord](https://discord.com/invite/otomate), the same contact as in [/.well-known/security.txt](/.well-known/security.txt). Please include the chain, the contract or endpoint, and the steps to reproduce. Do not post the details in public channels, and do not test on mainnet with other people's funds. Otomate never sends a direct message first, and support never asks for a seed phrase, a private key or a signature. Anyone who does so while claiming to be Otomate support is a scammer. ## Audit The audit report of the launch pool contracts is published here once the audit is complete. ## What the contracts do These are facts of the code, for anyone assessing a launch. The addresses are in [Chains and contracts](/docs/developers#chains-and-contracts). ### What nobody can modify, Otomate included - **The token** cannot be upgraded. It is a plain ERC-20 with 18 decimals, burns and `permit` (EIP-2612). `permit` takes 65-byte signatures only: an ECDSA signature of the owner (an EOA, or an EIP-7702 EOA), or, when the owner is a contract wallet, its ERC-1271 approval of the same 65 bytes. Other smart wallets (multi-owner Safes, passkey wallets) use an approval or Permit2. The token does not implement EIP-3009. It has no owner, no pause, no blocklist and no mint after creation. - **The whole supply** goes into the pool at creation: nothing is held back. - **The hook** cannot be upgraded. It owns the pool's two positions and has no function that removes their liquidity. It has no recovery or admin withdrawal function: fees leave it only through `release` (to the distributor; anyone may call it) and through builder claims. - **Each pool's rates** (the curve's rates, the rates after graduation and the launch surcharge) are set at creation for this pool: the hook stores them in the pool's snapshot and has no function that writes them afterwards, and no other contract or key can reach them. The launch surcharge applies to buys only, for at most 3,600 seconds after creation, and decreases over that time. - **Builder fees** are chosen by the front end or bot that sends a trade, not by Otomate: at most 2 %, paid by the trader on top of the pool's fees. The hook keeps them for that builder; only the builder claims them, or anyone pays them to the builder itself when the builder allows it. See [Builder codes](/docs/developers#builder-codes). - **Creating** is open to anyone. The creation fee is paid in the chain's native currency, the exact amount is written in the call, and the factory sends it to Otomate's treasury in the same transaction. ### What Otomate's Safe sets, within bounds Otomate's Safe (2 of 4 signatures) sets these values directly; each change emits an event that anyone can watch. - **Launch settings, for new pools only** (config): the pairs and their presets (supply, curve, the minimum and maximum initial buy as amounts in the pair), the fee rows a new pool copies into its snapshot, the creation fees (at most 0.05 ETH, 5 HYPE on HyperEVM, or 0.2 BNB on BNB Smart Chain), the hook new pools use, and the roles (guardian, operator, treasury, NFT manager). A pool already created keeps its snapshot. - **Automatic art delay:** 1 to 30 days (`automaticArtworkDelay()` on the config; 3 days by default). - **Never-graduated delays** (NFT vault): 7 to 365 days while no NFT of the collection exists, 90 to 1,825 days once one does (30 and 365 days by default). - **Payout paths** (distributor): the routes, sinks and per-asset caps of OTO burns, royalty burns and global forwarding, never a rate. The Safe can stop those three; it cannot stop `distribute` or the creator share. - **Forge:** a pause of new sealed mints and of openings; trading is not affected. - **No trading pause.** Nothing in Otomate's contracts can stop trading on a pool, before or after graduation: the hook has no pause and no owner. The Safe can only stop new launches (`setCreationEnabled`). A pair's own issuer is another matter: the issuers of the stock pairs ([Chains and contracts](/docs/developers#chains-and-contracts)) can pause their tokens or blocklist addresses, which would stop trading on the pools of that pair. ### What can receive new code, through the timelock - **Upgradeable parts:** the proxies (config, factory, distributor, NFT vault, gate, Forge, lens) and the collection template (one beacon serves every collection). They receive new code only through Otomate's upgrade timelock, which the Safe operates; each upgrade emits events that anyone can watch. A template upgrade applies to every collection at once; a distributor upgrade applies to how the fees released from then on are delivered. No upgrade reaches the hook or the token: an existing pool's rates, liquidity and graduation stay as they are. - **The timelock's delay** is 0 at launch; Otomate plans to raise it to 24 to 48 hours once the system is stable. Only the timelock itself can raise it (a scheduled operation, visible on chain). ### Collections - **Rare NFT draws.** When NFTs are revealed, Otomate's operator provides the draw, with a commitment published before it. The commitment proves the attribution was not altered afterwards; it is not independent randomness. - **A collection's art.** The creator chooses it after the launch: three designs, then one confirmation from the creator payout wallet. The NFT mint opens once it is chosen. Automatic art is not available for now. - **A launch that never graduates.** Its collection's waiting NFT share can be sent to Otomate's treasury by anyone, once the pool has not graduated `neverGraduatedDelay()` after its creation while no NFT of the collection exists, or `neverGraduatedMintedDelay()` once one does. Read both, and `neverGraduatedSweep(collection)`, on the NFT vault. Graduation closes this for good. - **Creator earnings** are delivered by the distributor; the amounts owed are public in the hook. ## Stability of the API See [Versioning and stability](/docs/developers#versioning-and-stability). # API operations (OpenAPI summary) - GET /api/v1/{chain}/tokens — Launches, sorted and filtered (listTokens) - GET /api/v1/{chain}/tokens/{token} — One launch in one read (getToken) - GET /api/v1/{chain}/tokens/{token}/trades — Trades, newest first (getTrades) - GET /api/v1/{chain}/tokens/{token}/candles — OHLCV candles in the quote or in USD (getCandles) - GET /api/v1/{chain}/tokens/{token}/holders — The 50 largest holders and the count (getHolders) - GET /api/v1/{chain}/creators/{address} — A creator's launches, their outcome and earnings (getCreator) - GET /api/v1/{chain}/wallets/{address} — A wallet's positions, value and what it can expect or claim (getWallet) - GET /api/v1/{chain}/quote — A quote from the chain's V4 Quoter, with every fee (quote) - POST /api/v1/{chain}/swap-transaction — A swap ready to sign, with its approvals (swapTransaction) - GET /api/v1/{chain}/tokenlist.json — The chain's launches as a token list (tokenlists.org) (tokenList) - GET /api/v1/{chain}/feed — Real-time feed (server-sent events) (feed) - GET /api/v1/{chain}/feed/events — One page of the feed (polling, catching up) (feedPage) - GET /api/v1/{chain}/status — Indexer marks and the feed's health (status) - GET /api/v1/{chain}/keys/challenge — The message a wallet signs to get an API key (EIP-4361) (keyChallenge) - POST /api/v1/{chain}/keys — An API key bound to the signing wallet (shown once; nothing is stored; valid on both chains) (createKey) - GET /api/v1/{chain}/keys/me — The tier and limits of the request's key (keyInfo) - GET /api/v1/{chain}/images/{token}/{cid} — A launch's moderated logo or banner (getImage) - POST /api/v1/{chain}/metadata — Point of upload: moderated, drawn and pinned token or profile metadata (uploadMetadata) - POST /api/v1/{chain}/create-transaction — The factory call of a new launch, ready to sign (createTransaction) # Launchpad prompt # Prompt: build a launchpad on Otomate Copy this into your coding assistant (Claude, Cursor, …), fill in the angle brackets, and attach your logo. ```text Build a token launchpad web app on Otomate with the @otomate/sdk package. Read the documentation first: https://www.otomate.fun/llms-full.txt (API: https://www.otomate.fun/docs/kit/openapi.v1.json). Brand: . Colors: primary <#hex>, background <#hex>, text <#hex>. Logo: . Chains: Ink<, and Robinhood Chain>. Builder code: , , on every trade the app sends. Optional API key from the environment variable OTOMATE_API_KEY (never hard-coded). Pages: 1. Explore: tabs new, graduating, graduated, trending, gainers; search by name, symbol or address; new launches appear live from the real-time feed (SDK createOtomateFeed, topic launch). 2. Token page: name, logo, price, market cap, liquidity, 24 h volume and change, progress to graduation, a price chart from the API candles, trades updating live from the feed (topic trade, filtered on the token), holders, and a buy and sell ticket (SDK swap with my builder code; ETH on WETH pairs; show the quote's fees and price impact before signing). 3. Create: name, symbol, description, links, logo and optional banner, pair (SDK otomatePairs: WETH paid in ETH, OTO, the stock tokens), my creator share and its burned part, optional first buy; SDK createToken; show each step; explain that the creation fee is paid in ETH. 4. Creator page: their launches and how many graduated. Rules: connect the user's injected wallet (EIP-1193); never ask for or handle private keys; check that a creation goes to the official factory and a swap to Otomate's router or the chain's Universal Router; show fee rates as numbers read from the API; mobile first; no horizontal scroll. If you can, start from Otomate's starter launchpad and only change its launchpad.config.ts. ```