@frontierfun/sdk
v1.1.0
Published
TypeScript SDK for the Frontier launchpad — deploy, trade, graduate, stake, and manage coins programmatically (bonding curve → Uniswap V4 pool)
Readme
@frontierfun/sdk
TypeScript SDK for Frontier, the coin launchpad on Robinhood Chain. Launch coins, trade them on their bonding curve and on their Uniswap V4 pool, graduate them, provide liquidity, stake, manage fee recipients, referrals, highlights and vesting, from a bot, a backend or an AI agent framework.
A coin on Frontier either sells along a bonding curve and graduates into its own locked Uniswap V4 pool, or launches instantly straight into that pool. Both modes register the pool in the deploy transaction; isLPd says where the coin trades. Creators pick the pool fee model, optional hook extensions and a staking vault at creation; the rules are frozen afterwards and the liquidity is locked forever.
This package is the single source of truth: the MCP server and the OpenClaw plugin are thin adapters over its ToolExecutor (84 tools: 50 read, 30 write, 4 config; see the API reference).
Requirements
- Node.js >= 24
- ethers v6 (dependency), zod v4 (dependency)
Installation
npm install @frontierfun/sdk
# or
pnpm add @frontierfun/sdkQuick Start
import { LaunchpadAgent } from '@frontierfun/sdk'
// Robinhood Chain (4663) by default; every contract, the API and the explorer come from the chain registry
const agent = new LaunchpadAgent({
rpcUrl: 'https://rpc.mainnet.chain.robinhood.com',
privateKey: process.env.FRONTIER_PRIVATE_KEY, // omit for read-only mode
})
// Or from the live registry (GET /chains + the extension catalog); static fallback with a warning
const testnet = await LaunchpadAgent.fromRegistry({ chainId: 421614, privateKey: process.env.FRONTIER_PRIVATE_KEY })
const state = await agent.getTokenState('0xCOIN...')
console.log(state.launchMode, state.isLPd, state.poolId)
if (!state.isLPd) {
const quote = await agent.quoteBuy('0xCOIN...', '0.5') // fee-inclusive: pass what you will send
const result = await agent.buy('0xCOIN...', '0.5') // path 'bonding-curve' (auto-routes to the pool once isLPd)
} else {
const quote = await agent.quotePoolSwap({ tokenAddr: '0xCOIN...', side: 'buy', amountIn: '0.5' })
const result = await agent.poolSwap({ tokenAddr: '0xCOIN...', side: 'buy', amountIn: '0.5', slippageBps: 100 })
}Amounts in the agent API are decimal strings ('0.5' = 0.5 ETH, '1000' = 1000 coins) and results are bigint wei unless documented otherwise. The pool module (pool.ts) works in wei; the agent converts.
Configuration
ConnectionConfig
new LaunchpadAgent(conn: ConnectionConfig, config?: DeepPartial<FullConfig>). Everything except rpcUrl has a default derived from the chain registry (chains.ts, a verified snapshot of the Frontier API's GET /chains). resolveConnectionConfig(conn) is exported and pure: it applies the defaults, checksums every address and throws on an unknown chain, a malformed URL or a bad address (a typo must fail loudly rather than fall back to production addresses on the wrong network).
| Field | Default | Notes | MCP server env |
|-------|---------|-------|----------------|
| rpcUrl | required | http(s):// or ws(s)://. Strategies and live event feeds need wss:// | FRONTIER_RPC_URL |
| privateKey | none | Omit for read-only mode (every write throws) | FRONTIER_PRIVATE_KEY / FRONTIER_KEYSTORE_PASSWORD |
| chainId | 4663 | 4663 Robinhood Chain, 421614 Arbitrum Sepolia | FRONTIER_CHAIN_ID |
| chain | registry entry for chainId | A full FrontierChainConfig (from fetchChainRegistry) to use instead of the static one; set by fromRegistry | |
| factoryAddress | chain contracts.tokenFactory | BCTokenFactory | FRONTIER_FACTORY_ADDRESS |
| bondingCurveAddress | contracts.bondingCurve | | FRONTIER_BONDING_CURVE_ADDRESS |
| curveMathAddress | contracts.curveMath | | FRONTIER_CURVE_MATH_ADDRESS |
| highlightsManagerAddress | contracts.highlightsManager | | FRONTIER_HIGHLIGHTS_ADDRESS |
| referralManagerAddress | contracts.referralManager | | FRONTIER_REFERRAL_ADDRESS |
| vestingAddress | contracts.tokenVesting | | FRONTIER_VESTING_ADDRESS |
| hookAddress | contracts.univ4.hook | FactoryHook, the coin's Uniswap V4 hook | FRONTIER_HOOK_ADDRESS |
| liquidityManagerAddress | contracts.liquidityManager | Optional; on-chain pool key lookup (getPoolKeyOnChain) | FRONTIER_LIQUIDITY_MANAGER_ADDRESS |
| poolManagerAddress | contracts.univ4.poolManager | | FRONTIER_POOL_MANAGER_ADDRESS |
| universalRouterAddress | contracts.univ4.universalRouter | | FRONTIER_UNIVERSAL_ROUTER_ADDRESS |
| quoterAddress | contracts.univ4.quoter | V4Quoter | FRONTIER_QUOTER_ADDRESS |
| positionManagerAddress | contracts.univ4.positionManager | | FRONTIER_POSITION_MANAGER_ADDRESS |
| stateViewAddress | contracts.univ4.stateView | Optional lens; PoolManager.extsload is the primary read path | FRONTIER_STATE_VIEW_ADDRESS |
| permit2Address | contracts.permit2 | Same address on every chain | FRONTIER_PERMIT2_ADDRESS |
| stakingVaultFactoryAddress | contracts.stakingVaultFactory | | FRONTIER_STAKING_VAULT_FACTORY_ADDRESS |
| harvesterAddress | contracts.harvester | | FRONTIER_HARVESTER_ADDRESS |
| polDistributorAddress | contracts.polDistributor | | FRONTIER_POL_DISTRIBUTOR_ADDRESS |
| cloneFactoryAddress | contracts.cloneFactory | | FRONTIER_CLONE_FACTORY_ADDRESS |
| bcTokenDeployerAddress | contracts.bcTokenDeployer | Optional; predicts a coin's address before the deploy | |
| wethAddress | native.wrapped | Reward asset of vaults and referrals (never a pool currency) | FRONTIER_WETH_ADDRESS |
| poolFee | DYNAMIC_FEE_FLAG (0x800000) | Fee field of every Frontier pool key | |
| poolTickSpacing | 60 | | |
| extensionAddresses | chain extensions | { dynamicFee, sniperTax, maturityCurve } overrides | |
| routerVariant | detected | 'legacy' or 'minHopPrice'; config override > chain default > bytecode detection | |
| apiBaseUrl | chain apiBaseUrl | https://api.frontier.fun / https://dev-sepolia.api.frontier.fun | FRONTIER_API_BASE_URL |
| rpcTimeoutMs | 15000 | HTTP only | |
| orderSplitter | off | Opt-in liquidity-aware chunking of large curve buys | |
| stateBackend / statePersistDir | none | State persistence (see Advanced) | |
| explorerBaseUrl | chain explorerUrls[0] | Origin; a bare host is assumed https | FRONTIER_EXPLORER_URL |
| referralBaseUrl | chain appBaseUrl | Referral links are ${referralBaseUrl}?ref=${address} | FRONTIER_REFERRAL_BASE_URL |
| referrerAddress | agent address | | FRONTIER_REFERRER_ADDRESS |
| expectedChainId | chainId | Verified against provider.getNetwork() before the first write; null disables (local forks only) | |
The SDK itself reads only the wallet variables (through WalletManager); the FRONTIER_* column is what the MCP server maps onto these fields. connectionConfigForChain(chainId) (from ./constants) returns the same defaults as a plain object you can spread into a ConnectionConfig.
import { LaunchpadAgent, connectionConfigForChain, resolveConnectionConfig } from '@frontierfun/sdk'
const testnetAgent = new LaunchpadAgent({ ...connectionConfigForChain(421614), privateKey })
const resolved = resolveConnectionConfig({ rpcUrl: 'https://rpc.mainnet.chain.robinhood.com' })
resolved.chain.contracts.univ4.hook // '0xb31780AAd49D3Cc7Dd6E03E9e462606F0A5A30Cc'Providers are created with { staticNetwork: true, cacheTimeout: -1 }: ethers otherwise dedupes identical reads for 250 ms and returns a stale balance right after a transaction on a ~250 ms-block chain. Do the same for any provider you build yourself.
LaunchpadAgent.fromRegistry(opts, config?)
Static async factory: fetches GET /chains (and the extension catalog for the chain) from opts.apiBaseUrl (default: the chain's) and builds the agent on the live registry entry. rpcUrl defaults to the chain's first HTTP RPC. When the API is unreachable it logs a warning and falls back to the static registry, so a bot still starts.
FullConfig (behaviour)
The second constructor argument, also settable at runtime with updateConfig(partial) / setTrading / setRisk / setCopy / setStrategies / setOps; getConfig() returns the effective config. createDefaultConfig(), mergeConfig(), validateConfigBounds() and ABSOLUTE_MINIMUM_FLOORS are exported (@frontierfun/sdk/config).
| Section | Fields (defaults) |
|---------|-------------------|
| trading | defaultSlippageBps (200), defaultAffiliate (zero address), maxTradeETH ('1.0'), gasReserve ('0.05'), gasPriceMultiplier (1.0), gasLimitOverride (0 = estimate), autoApprove (true), maxApprovalAmount |
| risk | maxTotalExposure, stopLossPct, takeProfitPct, tradeCooldownMs, maxOpenPositions, minHoldTimeMs |
| copy | targets, scaleFactor, delayMs, maxCopyETH, minCopyETH, copyBuys, copySells, slippageBpsOverride, selfAffiliate, tokenWhitelist, tokenBlacklist, copyPoolSwaps (false) |
| strategies | sniper (ethPerSnipe, maxReserveToBuy, delayAfterDeployMs, slippageBps?, includeInstant false), momentum, graduation, exitMonitor |
| ops | logging, logLevel, dryRun (false), webhookUrl |
maxTradeETH caps every buy, triggerGraduation and poolSwap (which throws rather than silently capping); gasReserve is kept back on every write; dryRun returns a typed DryRunResult (isDryRunResult()) instead of sending.
Launching a coin
uploadImage(pathOrDataUrl, name?)
Uploads a PNG/JPEG/GIF/WebP (2 MiB decoded max) to the Frontier API (POST /upload) and returns ipfs://<cid>. File paths must sit under ./uploads (agent.uploadBaseDir, relative to the working directory); data URLs are accepted.
deploy(cfg: DeployConfig) and buildDeploy(cfg)
buildDeploy builds and validates everything the deploy sends without sending it (BuildDeployResult { args, valueWei, creationFee, devBuyWei, launchMode, curve?, seedTick?, predictedTokenAddress?, predictedPoolId? }); deploy sends it (gas limit = estimate × 1.25) and returns DeployResult { txHash, tokenAddress, poolId, launchMode, hookConfig, launchConfig, valueWei, devBuy?: { ethAmount, tokenAmount }, stakingVault?, predictedTokenAddress? }. msg.value = creationFee + devBuyWei.
| DeployConfig field | Meaning |
|----------------------|---------|
| name, symbol, description | Metadata; the factory limits are 32 / 16 / 256 characters (METADATA_LIMITS) |
| image | ipfs://<cid>, a bare CID or an https URL (required; use uploadImage) |
| twitter?, telegram?, website? | Packed into the on-chain description like the app (packDescription) |
| communityFeeRatio? | 0 without a vault, 1..100 with one (default 50 when stakingConfig.deployStaking) |
| salt? | bytes32 hex or a memorable string (keccak'd); random when omitted |
| stakingConfig? | { deployStaking, alternativeFeeRecipient? } |
| launch? | { mode: 'curve', virtualReserves?, initialSupply?, startingFdvEth?, raiseTargetEth? } or { mode: 'instant', seedTick?, startingFdvEth? }. Default: bonding curve with the factory's initialParams() |
| devBuyEth? / devBuyPercent? | Creator buy sent on top of the creation fee (a curve buy or a pool swap); percent is capped by maxCreatorBuyBps |
| hookConfig? | 'default' (the app default: 1% base fee, 70% LP share, dynamic-fee extension, no sniper window), 'none' (0x: flat 0.30%, no extensions), raw hex, or a HookConfigV2Input. Default 'default' |
| fixedFee? | Base fee in pips (default 10000 = 1%; ceiling MAX_HOOK_FEE = 10%) |
| lpShareBps? | LP share of the swap fee (default 7000) |
| feeMode? | 'dynamic' binds the dynamic-fee calculator (default), 'fixed' uses the base fee alone |
| dynamicFee? | { floorFee, midFee, capFee } pips, strictly ascending |
| extensions? | [{ id \| address, params?, calls?, kind? }], resolved through the extension catalog |
Extension ids (from GET /extensions, staticCatalog(chain) as fallback): dynamic-fee (floorFee, midFee, capFee), maturity-curve (ages, fees or steps), sniper-tax (peakFee, window). dynamic-fee and maturity-curve are mutually exclusive base-fee providers; a sniper tax whose peak exceeds MAX_HOOK_FEE needs a declared sniper window (derived automatically).
const image = await agent.uploadImage('./coin.png')
// Bonding curve with the creator knobs
const curveCoin = await agent.deploy({
name: 'Mittens', symbol: 'MITTENS', description: 'Cat coin', image,
launch: { mode: 'curve', startingFdvEth: '2', raiseTargetEth: '5' },
devBuyEth: '0.1',
stakingConfig: { deployStaking: true }, communityFeeRatio: 50,
})
// Instant launch straight into the pool, fixed fee, sniper tax for the first 10 minutes
const instantCoin = await agent.deploy({
name: 'Rocket', symbol: 'RKT', description: 'Instant', image,
launch: { mode: 'instant', startingFdvEth: '10' },
fixedFee: 5000, feeMode: 'fixed',
extensions: [{ id: 'sniper-tax', params: { peakFee: 200000, window: 600 } }],
})Factory reads
getFactoryParams(): FactoryParams { initialParams: CurveParams, curveBounds: CurveBounds, seedFdvBounds: { minFdv, maxFdv }, feeConfig: { protocolFee, creatorFee, refundFee, creationFee }, maxCreatorBuyBps, creatorShareBps, txFee, treasury? }(30 s cache). Governance values: read them, never hardcode them.getCreationFee(): bigint,isProtocolPaused(): boolean.getExtensionCatalog(): ExtensionCatalogEntry[](GET /extensions?chainId, 5 min cache, static fallback),primeExtensionCatalog(catalog).
Bonding curve trading
Curve quotes are fee-inclusive: pass the full ETH you will send to quoteBuy and it returns the coins delivered; quoteSell returns the net ETH you receive. The curve fee is txFee bps of the ETH side (fee-on-total); CurveFeeDistributed splits it referral / creator (creatorShareBps of the rest) / protocol.
buy(tokenAddr, ethAmount, overrides?),sell(tokenAddr, tokenAmount, overrides?)->TradeResult { txHash, amount, spent, priceAfter, graduated, grossAmount, curveFee, protocolFee (alias of curveFee), netAmount, feeBps, path: 'bonding-curve' | 'v4-pool', feeSplit?, poolId?, feePips? }. Both auto-route to the Uniswap V4 pool once the coin isisLPd(the affiliate is dropped with an audit warning; on the pool pathcurveFeeis the hook fee andfeeBps = feePips / 100).TradeOverrides { slippageBps?, affiliate?, gasLimit?, gasPriceMultiplier? }.quoteBuy(tokenAddr, ethAmount)->QuoteBuyResult { tokensOut, costAfterFee, grossAmount, curveFee, netAmount, feeBps, wouldGraduate, supplyRemaining, ethToGraduation, feeSplit?, graduationFees? }(throws once the coin isisLPd: usequotePoolSwap).quoteSell(tokenAddr, tokenAmount)->QuoteSellResult { ethOut, fee, grossAmount, curveFee, netAmount, feeBps, feeSplit? }.calculateCost(tokenAddr, tokenAmount)->CalculateCostResult { grossCost, curveFee, netCostWithFee, feeBps, tokenAmount }(ETH to buy exactly that many coins, fee included).estimateGraduation(tokenAddr)->GraduationEstimate { supplyRemaining, ethNeeded, callerFeeETH, creatorFeeETH, protocolFeeETH }: graduation paysREFUND_FEEbps of the raise to whoever fills the curve,CREATOR_FEEbps to the fee recipient andPROTOCOL_FEEbps to the treasury.triggerGraduation(tokenAddr, overrides?): a buy ofethNeeded × 1.01(default slippage 500 bps), capped bymaxTradeETH.canOTCTransfer(tokenAddr, from, to)->{ allowed, reason }(isLPd, orfrom/tois the factory or the vesting contract) andotcTransfer(tokenAddr, to, amount): curve coins are locked until graduation.
Uniswap V4 pool
Every coin has one hooked pool: PoolKey { currency0: native ETH (address(0)), currency1: coin, fee: DYNAMIC_FEE_FLAG, tickSpacing: 60, hooks: FactoryHook }, poolId = keccak256(abi.encode(poolKey)). Buys are zeroForOne. There is no WETH on the pool path; sells need Permit2 approvals, set up on demand.
getPoolKey(tokenAddr): PoolKey,getPoolId(tokenAddr): string(local, no RPC),getPoolKeyOnChain(tokenAddr)(LiquidityManager, when configured).getPoolState(tokenAddr): PoolState { poolId, poolKey, initialized, tradable (= isLPd), launchMode, sqrtPriceX96, tick, liquidity, priceEthPerCoin, coinPerEth, fdvEth, hook: HookPoolState { registered, coin, communityFeeRatio, stakingVault, fixedFee, lastAppliedFee, lpShareBps, sniperWindow, feeCalculators, observers, referenceTick, lastSwapTimestamp, volatilityAccumulator, ... }, fee: PoolFeePreview & { source, previewAmountWei }, protocolFeeRatio, lpdAt }. Slot0/liquidity are read throughPoolManager.extsload(StateView is the fallback); a curve coin's pool is initialized but dormant until graduation, sotradableis the truth, neverinitialized.previewPoolFee(tokenAddr, { side, amountIn? | amountOut? })->PoolFeePreview { totalFeePips, lpFeePips, nonLpFeePips, totalFeeBps }(FactoryHook.previewFee). Pool fees are dynamic: quote them for the actual size, never assume them.quotePoolSwap({ tokenAddr, side, amountIn? | amountOut?, hookData? })->PoolQuote { poolId, side, mode: 'exact-in' | 'exact-out', amountIn, amountOut, gasEstimate, feePips, fee, priceEthPerCoin, executionPriceEthPerCoin }(V4Quoter, runs the hook).poolSwap({ tokenAddr, side, amountIn? | amountOut?, slippageBps?, deadlineSeconds? (1200), recipient?, hookData? })->PoolSwapResult { txHash, side, mode, amountIn, amountOut, poolId, path: 'v4-pool', recipient, feePips, gasUsed, amountsFromLog, sqrtPriceX96After?, swapLog? }through the Universal Router (V4_SWAP+SETTLE_ALL/TAKE_ALL; exact-out buys add aSWEEPthat refunds the unused ETH).amountInis what the swapper paid, hook fee included;swapLogcarries the raw PoolManagerSwapdeltas (swapper-side sign convention: negative = paid in, positive = received; the log's input leg is net of the hook's non-LP fee). Gated bymaxTradeETH, the ETH balance and the coin balance.ensurePoolSellApproval(tokenAddr, amount?)->{ txHashes }: ERC-20 approve to Permit2 (max) andPermit2.approve(coin, universalRouter, MAX_UINT160, MAX_UINT48), skipped when already sufficient.poolSwapcalls it before a sell.hookData: optional bytes (<= 256) forwarded to the hook;encodeBeneficiaryHookData(address)is the observer-attribution convention (abi.encode(address)).
The Universal Router on Robinhood Chain decodes the newer 6-field exact-input params (minHopPriceX36), Arbitrum Sepolia the legacy 5-field ones; the agent resolves routerVariant once (config, then chain default, then bytecode detection). With empty hookData both encodings work by accident; with non-empty hookData only the right one does.
Liquidity (Uniswap V4 PositionManager)
Positions target the coin's one hooked pool (there is no "default pool"). Native ETH is sent as msg.value (the surplus is swept back), the coin is pulled through Permit2 (ensurePoolLpApproval), everything goes through PositionManager.modifyLiquidities.
addLiquidity({ tokenAddr, amountEthMax, amountTokenMax, positionTokenId?, tickLower?, tickUpper?, tickPreset? ('full' | 'narrow' ~±5% | 'wide' ~±25%), slippageBps?, deadlineSeconds?, recipient? })->AddLiquidityResult { txHash, positionTokenId, liquidity, positionLiquidity, amount0, amount1, tickLower, tickUpper, poolId, valueWei, minted }. Mints, or increases whenpositionTokenIdis given.removeLiquidity({ positionTokenId, liquidityPct? (100) | liquidity?, amount0Min?, amount1Min?, slippageBps?, deadlineSeconds?, recipient?, burn? })->RemoveLiquidityResult { txHash, positionTokenId, amount0, amount1, liquidityRemoved, remainingLiquidity, burned, poolId }.collectFees({ positionTokenId, recipient? })->CollectFeesResult { txHash, positionTokenId, amount0, amount1, poolId }.getLpPositions(owner?, tokenAddr?, opts?: PositionScanOptions { fromBlock?, toBlock?, chunkSize? })->LpPosition[] { tokenId, poolId, poolKey, tokenAddress, tickLower, tickUpper, liquidity, amount0, amount1, inRange }. The PositionManager has no enumerable extension: positions are found from itsTransferlogs (fromKNOWN_POSITION_MANAGER_START_BLOCKfor the chain), which needs an RPC that serveseth_getLogs.
Protocol-owned liquidity, fee recipients, extensions
collectPoolFees(tokenAddr, opts?: { tokenIds? } & PositionScanOptions)->CollectPoolFeesResult { txHash, token, tokenIds, collected[], distributed[], deferred[], totalWeth, totalCoin }: permissionlessPolDistributor.multiCollectover the coin's locked positions (found from the LiquidityManager mints whentokenIdsis not given). The creator's share of the pool fees reaches the fee recipient (and the vault) this way.getFeeRecipient(tokenAddr)(BCToken.getFeeRecipient(): the alternative recipient or the creator),setFeeRecipient(tokenAddr, recipient)->{ txHash, recipient }(only the current recipient may change it, per the contract).createFeeRecipient(params)->CreateFeeRecipientResult { txHash, clone, kind, implId, initData, deterministic }withparamsone of{ kind: 'lottery', coin?, salt?, implId? },{ kind: 'buyback-burn', coin, shareRecipient?, shareBps? (<= 5000), salt?, implId? },{ kind: 'fee-splitter', coin, payees, shares (bps summing to 10000), salt?, implId? }. Clones come from the chain'sCloneFactory(RECIPIENT_IMPL_IDS); the SDK checksimplementationOf(implId)first and throws "No '' fee-recipient implementation is registered on this chain" when the chain has none (production today; Arbitrum Sepolia has the lottery implementation).predictFeeRecipientAddressandencodeFeeRecipientInitDataare exported for salted deployments.getCoinExtensions(tokenAddr)->CoinExtensionsState & { source: 'api' | 'chain' }: the API's/coins/{addr}/extensionspayload (pool id, hook config, bound fee calculators and observers with catalog joins), rebuilt fromgetPoolStatewhen the API is unavailable.
Staking (vaults with a WETH stream)
Vault shares have 21 decimals on Frontier vaults: the SDK reads vault.decimals() (getVaultDecimals, cached) and every share amount goes through it. requestUnstake burns shares immediately, escrows the assets in the cooldown holder and restarts the cooldown for the whole escrow.
getStakingVaultAddress(tokenAddr): string | null(FactoryHook.getPoolState(poolId).stakingVault, then the API's/vaultByToken).previewStake(vault, amount): bigint(shares),previewUnstake(vault, amount): bigint(assets),formatShares(vault, shares): string.stake(vault, tokenAddr, amount)->{ txHash, shares },requestUnstake(vault, amount),requestUnstakeByShares(vault, shares)(decimal, vault decimals),completeUnstake(vault, receiver?),getCooldownStatus(vault)->{ amount, cooldownEnd, isReady }.getStakingPosition(vault)->StakingPosition { shares, sharesDecimals, sharesFormatted, assets, maxWithdraw, maxRedeem, cooldownDuration, earnedWeth, pendingWeth }.getStakingRewards(vault)->{ earnedWeth, pendingWeth },claimStakingRewards(vault)->{ txHash, wethAmount }(StakingVault.claimWeth()).
Referral, highlights, vesting
- Referral:
referralLink(referrer?)->${referralBaseUrl}?ref=${address}(https://frontier.fun?ref=0x...),getReferralFees()->{ directBps, indirectBps, totalBps },getReferralBpsFor(user),getReferralChain(user)->{ direct, indirect },getPendingReferralReward(rewardToken = WETH),claimReferralReward(rewardToken = WETH). - Highlights:
getHighlightConfig()->{ minDuration, hardCap, baseFeePerSecond, cooldownPeriod, expThreshold },getHighlightStatus(tokenAddr, duration)->{ bookedUntil, tokenCooldownUntil, isSlotFree, isTokenEligible, quotedFee },highlightToken(tokenAddr, durationSeconds)->{ txHash, fee }(MAX_HIGHLIGHT_DURATION_SECONDSbounds it). - Vesting:
createVestingSchedule({ token, beneficiaries, amounts, startTimestamp, cliffDurationSeconds, totalDurationSeconds, slicePeriodSeconds })->{ txHash, scheduleIds },releaseVestedTokens(token, scheduleId)->{ txHash, amount },getVestingInfo(token, index?),getAllVestingSchedules(token).
Reads
getTokenState(tokenAddr): TokenState { address, name, symbol, description, imageURI, totalSupply, maxSupply, initialSupply, virtualBalance, targetETH, reserveBalance, tvl, isLPd, lpAddress, creator, pricePerToken, protocolFee, creatorFee, refundFee, communityFeeRatio, launchMode, directSeed, lpdAt, poolId, feeRecipient, alternativeFeeRecipient, curve: { virtualReserves, initialSupply, maxSupply, targetETH }, graduationFees: { toCreator, toProtocol, toGraduationCaller }, pool?: PoolState }.lpAddressisBCToken.lp(), which on Frontier is the hook address, not a pool: usepoolId.poolis filled whenisLPd(best effort).hasGraduated(tokenAddr),getTokenBalance(tokenAddr),getETHBalance(),getWETHBalance(),isRPCHealthy(timeoutMs?).- Getters:
address,chainId,chain(FrontierChainConfig),connection(ResolvedConnectionConfig),apiBaseUrl,explorerBaseUrl,referralBaseUrl,referrerAddress,wethAddress,uploadBaseDir,readOnly,hasWebSocket,txQueue; contract accessorsfactory,bondingCurve,curveMath,highlights,referral,vesting,hook,poolManager,universalRouter,quoter,positionManager.
Events
Typed subscriptions through the EventBus (fields read by name, never positionally); each returns an unsubscribe function. A wss:// RPC gives push delivery; HTTP falls back to ethers polling, which needs an RPC that serves eth_getLogs.
onNewCoin(cb: NewCoinEvent { creator, token, name, symbol, description, image, lp (hook), initialSupply, maxSupply, initialETHReserves, initialPrice, initialMarketCap, targetETH, directSeed, launchMode })onBuy(cb: BuyEvent),onSell(cb: SellEvent)(amount,amountOut,totalSupply,price,marketCap,reserveBalance),onGraduation(cb: GraduationEvent { token, pool (hook) })onCurveFeeDistributed(cb, tokenAddr?)(totalFee,referralAmount,creatorAmount,protocolAmount,feeRecipient)onPoolSwap(tokenAddr | undefined, cb: PoolSwapEvent { poolId, sender (the router, not the trader), amount0, amount1, sqrtPriceX96, liquidity, tick, fee (LP fee pips), ethAmount, coinAmount, side }): PoolManagerSwapfiltered by pool id (all Frontier pools whentokenAddris undefined)onSwapFeeDistributed(tokenAddr | undefined, cb)(the hook's non-LP fee leg:protocolAmount,vaultAmount,recipientAmount, ...)removeAllListeners(),waitForEventSubscriptions(),destroy()
Strategies and copy trading
StrategyRunner runs the built-in strategies (STRATEGY_CLASSES: LaunchSniper, Momentum, GraduationSniper, ExitMonitor, CopyTrader); they need a wss:// RPC. CopyTrader / CopyTradeMonitor mirror curve trades of target wallets (pool swaps opt-in with copy.copyPoolSwaps, keyed on coins the target already traded on the curve because the PoolManager Swap event names the router, not the trader). See the skill's strategy notes.
import { StrategyRunner } from '@frontierfun/sdk/strategies'
const runner = new StrategyRunner(agent, gate) // TxGate optional
agent.setStrategyRunner(runner)
await runner.startByName('GraduationSniper')AI agent integration
ToolExecutor and TOOL_DEFINITIONS
TOOL_DEFINITIONS (84 tools) is the framework-agnostic catalog: name, category (read | write | config), description, JSON-schema parameters, return description. ToolExecutor validates the parameters (TOOL_SCHEMAS, zod), scans freeform fields for prompt injection, resolves token_identifier (address, symbol or name through the TokenRegistry), applies the TxGate to writes, runs pre-flight checks and executes against the agent.
import { LaunchpadAgent, ToolExecutor, TxGate, TokenRegistry, TxHistory, TOOL_DEFINITIONS } from '@frontierfun/sdk'
const gate = new TxGate({ maxPerTxETH: '0.5', dailyBudgetETH: '3', cooldownSeconds: 2 })
const registry = new TokenRegistry(agent.apiBaseUrl, agent, { pollIntervalMs: 60_000 })
registry.start()
const tools = new ToolExecutor(agent, { gate, tokenRegistry: registry, txHistory: new TxHistory(100), toolPrefix: 'frontier_' })
const state = await tools.execute('get_token_state', { token_identifier: 'MITTENS' })
const swap = await tools.execute('pool_swap', { token_identifier: 'MITTENS', side: 'buy', amount_in: '0.1' })ToolExecutorOptions { gate?, toolPrefix?, signal?, strategyRunner?, tokenRegistry?, txHistory? }. Results are ToolResult { success, data?, error?, simulatedRevert?, revertReason?, revertCode? } with amounts formatted as decimal strings. buildDeployConfig maps deploy_token parameters onto a DeployConfig.
TxGate
new TxGate({ maxPerTxETH, dailyBudgetETH, cooldownSeconds, tokenBlacklist, confirmAboveETH, enabled }, ceilings?): per-transaction limit, rolling 24h budget, cooldown, blacklist, optional confirmation callback (ConfirmFn); reservations are rolled back on failure; getStats() feeds spending_stats. Operator ceilings bound what update_tx_gate may set; ABSOLUTE_MINIMUM_FLOORS cannot be crossed.
TokenRegistry
new TokenRegistry(apiBaseUrl, agent /* onNewCoin source */, options?, signal?, log?): coin cache bootstrapped from /coinsPage (newest first, up to 20 pages of 100), refreshed every 60 s and on onNewCoin; resolve(query) (async; server-side search fallback), resolveLocal(query), getTokenList(options), getRecentDeployments(limit), getInitStatus(). Ambiguous symbols resolve to the highest market cap by default and report the alternatives.
Wallet management
import { WalletManager } from '@frontierfun/sdk/wallet'
const { wallet, source } = await new WalletManager().resolve({ keystorePassword: process.env.FRONTIER_KEYSTORE_PASSWORD })Priority: FRONTIER_PRIVATE_KEY > explicit config > unprefixed PRIVATE_KEY; with a password and no keystore the key is migrated into the encrypted keystore ~/.frontier/agent-wallet.enc (AES-256-GCM, scrypt) and the env var must then be removed; with neither, a wallet is generated. The audit log lives in ~/.frontier/audit (DEFAULT_AUDIT_DIR, FRONTIER_AUDIT_DIR in the adapters), agent state in ~/.frontier/state.
Frontier data API client
Every function takes (baseUrl, ..., signal?) and validates the response with zod; amounts arrive as decimal ETH strings, addresses checksummed. Base URLs: https://api.frontier.fun (4663), https://dev-sepolia.api.frontier.fun (421614); the API is server-side only (no CORS for third parties) and has no third-party WebSocket feed.
| Function | Endpoint |
|----------|----------|
| fetchChainRegistry | GET /chains (ServedChain[], mergeRegistryChain merges into the static entry) |
| fetchExtensionCatalog | GET /extensions?chainId (extensionAddressesFromCatalog) |
| fetchCoinsPage, fetchAllCoins, fetchTokenList | GET /coinsPage (sort: activity, newest, oldest, marketcap, volume24h, trending; filter: all, new, bonded, trending, instant; search; max 100 per page, offset <= 10000) |
| fetchCoinsHydrate | GET /coinsHydrate?addresses&full=1 (<= 100 addresses) |
| fetchCoinsStats, fetchCoinsTop | GET /coinsStats, GET /coinsTop (kind: 'closest' = nearest graduation) |
| fetchCoinEvents, fetchCoinCandles | GET /coinEvents?tokenAddress&limit, GET /coinCandles (COIN_CANDLE_INTERVALS) |
| fetchCoinExtensions | GET /coins/{addr}/extensions?chainId |
| fetchRecentActivity, fetchBondedActivity | GET /recentActivity, GET /bondedActivity |
| fetchPoolTxHistory | GET /pools-v4/{poolId}/history/txs |
| fetchTokensVolume | GET /tokens |
| fetchVaults, fetchVaultByToken, fetchVaultEventsByToken, fetchVaultPosition | GET /vaults, /vaultByToken, /vaultEventsByToken, /vaultPositionByUserVault |
| fetchReferralRewards, fetchReferralPayouts | GET /referralRewards, /referralPayouts |
| fetchHighlights, fetchCurrentHighlight | GET /highlights, /currentHighlight |
| fetchDistribution, fetchHoldings, fetchCreations | app surfaces (/distribution, /holdings, /creations; unstable) |
| uploadImage, validateUploadPath, ipfsGatewayUrl | POST /upload (MAX_UPLOAD_FILE_SIZE = 2 MiB), GET /ipfs/{cid} |
Errors are classified by classifyAPIError (APIError, APIErrorCategory; RATE_LIMITED carries retryAfterSeconds, no automatic retry).
Chain registry, Uniswap V4 and launch helpers
@frontierfun/sdk/chains:CHAINS,chainConfig(chainId),isSupportedChain,explorerBaseUrl,ROBINHOOD_CHAIN(4663),ARBITRUM_SEPOLIA_CHAIN(421614),DEFAULT_CHAIN_ID,SUPPORTED_CHAIN_IDS,DYNAMIC_FEE_FLAG,FRONTIER_POOL_TICK_SPACING,NATIVE_CURRENCY,PERMIT2_ADDRESS,MULTICALL3_ADDRESS, typesFrontierChainConfig,FrontierContractAddresses,FrontierUniV4Addresses,FrontierExtensionAddresses,FrontierCurveConfig.@frontierfun/sdk/v4:buildPoolKey,computePoolId,normalizePoolId,isFrontierPoolKey, TickMath (getSqrtPriceAtTick,getTickAtSqrtPrice,alignTickDown/Up,fullRangeTicks,MIN_TICK/MAX_TICK,MODULATED_MIN_TICK), price helpers (ethPerCoinFromSqrtPrice,fdvWeiAtSqrtPrice,fdvWeiAtTick,sqrtPriceFromEthPerCoinWad), StateLibrary slot reads (poolStateSlot,decodeSlot0,readPoolSlotState,readPoolSlotStateViaStateView), Universal Router encoders (encodeV4SwapExactInSingle,encodeV4SwapExactOutSingle,encodeUniversalRouterCommands,encodeUniversalRouterSweep,Actions,UR_COMMAND_V4_SWAP), PositionManager encoders (encodeMintPosition,encodeIncreaseLiquidity,encodeDecreaseLiquidity,encodeBurnPosition,encodeSettlePair,encodeTakePair,encodeCloseCurrency,encodeSweep,encodeModifyLiquidities,decodePositionInfo,positionMatchesPool), LiquidityAmounts (getLiquidityForAmounts,getAmountsForLiquidity,getAmount0Delta,getAmount1Delta),detectUniversalRouterVariant,instantDevBuyValueWei.@frontierfun/sdk/launch:encodeHookConfig/decodeHookConfig,defaultHookConfigInput,buildHookConfigInput,encodeDynamicFeeConfig,encodeSniperTaxConfig,encodeMaturityCurveConfig,encodeExtensionConfig,staticCatalog,curveParamsFromKnobs,describeCurve,validateCurveParams,curveBoundsFromConfig,curveDefaultsFromConfig,seedTickFromFdv,validateSeedTick,devBuyTokens,instantDevBuyQuote,packDescription/unpackDescription,validateMetadata,toIpfsUri,normalizeSalt,deployArgsToArray,curveLaunchConfig,instantLaunchConfig, constants (METADATA_LIMITS,MAX_HOOK_FEE,SNIPER_MAX_FEE,DEFAULT_LP_SHARE_BPS,PROTOCOL_FLOOR_PIPS,DYNAMIC_FEE_DEFAULTS,APP_DEFAULT_FIXED_FEE, ...).pool.ts(root export): thePoolContext-based operations the agent delegates to (getPoolState,previewPoolFee,quotePoolSwap,poolSwap,ensurePermit2Approval,addLiquidity,removeLiquidity,collectFees,getLpPositions,findLockedPositionIds,collectPoolFees,createFeeRecipient, ...) plus their types and constants (DEFAULT_POOL_SLIPPAGE_BPS,KNOWN_POSITION_MANAGER_START_BLOCK,RECIPIENT_IMPL_IDS).
Error decoding
decodeRevertData(data), extractRevertData(error), innermostRevert, humanizeRevert(decoded) and KNOWN_REVERT_NAMES cover the Frontier contracts' custom errors and the Uniswap V4 ones (getRevertDecoderInterface()); the agent applies them to every failed write, and runPreflightChecks simulates writes before sending. RPC failures are classified by classifyRPCError.
Advanced modules
- State persistence:
stateBackend/statePersistDir(JSONFileBackend),saveState(),loadState(),enableAutoSave(ms),disableAutoSave(),validatePersistedState,CURRENT_STATE_VERSION. - Order splitting:
orderSplitter(OrderSplitter,DEFAULT_ORDER_SPLITTER_CONFIG) chunks large curve buys by curve liquidity. - Transaction queue:
agent.txQueue(TransactionQueue,executeWithRetry, dead-letter queue helpers) serializes every write. - Dry-run:
ops.dryRunreturnsDryRunResult(isDryRunResult,createDryRunResult). - Multicall3:
multicallBatch; bigint-safe JSON:safeJsonStringify,jsonReplacer; provider abstraction:EthersProviderAdapter,EthersSignerAdapter; audit:auditLog,initAuditSession,generateCorrelationId.
Sub-path exports
@frontierfun/sdk (everything), /abis (generated ABIs), /config, /chains, /v4, /launch, /tools, /strategies, /copytrader, /wallet, /tx-gate.
Environment variables
The SDK reads FRONTIER_PRIVATE_KEY and FRONTIER_KEYSTORE_PASSWORD (fallbacks PRIVATE_KEY, KEYSTORE_PASSWORD) through WalletManager. Everything else (FRONTIER_CHAIN_ID, FRONTIER_RPC_URL, FRONTIER_API_BASE_URL, FRONTIER_EXPLORER_URL, FRONTIER_REFERRAL_BASE_URL, FRONTIER_REFERRER_ADDRESS, the FRONTIER_*_ADDRESS overrides, the TxGate knobs) is read by the MCP server and the OpenClaw plugin and passed into ConnectionConfig; see .env.example in this package for the list.
Networks
| Chain | Id | Default RPC | WebSocket | Explorer | API |
|-------|----|-------------|-----------|----------|-----|
| Robinhood Chain (production, default) | 4663 | https://rpc.mainnet.chain.robinhood.com | wss://robinhood-rpc.publicnode.com | https://robinscan.io | https://api.frontier.fun |
| Arbitrum Sepolia (testnet) | 421614 | https://sepolia-rollup.arbitrum.io/rpc | wss://arbitrum-sepolia-rpc.publicnode.com | https://sepolia.arbiscan.io | https://dev-sepolia.api.frontier.fun |
https://robinhood-rpc.publicnode.com refuses eth_getLogs, which breaks event polling and position enumeration; the SDK defaults to the official RPC over HTTP. Contract addresses live in chains.ts (verified on-chain on 2026-08-17).
Changes
See CHANGELOG.md. Latest: v1.1.0, the official extension set made operable (recipient state, the permissionless calls that drive a lottery / buyback / splitter, and the pool fee schedules). 84 tools.
Development
# From the monorepo root
pnpm install
pnpm build:sdk
pnpm typecheck
npx vitest run --project sdk # unit tests
pnpm test:integration # Anvil fork of Robinhood Chain (needs Foundry)License
MIT
