Skip to content

Documentation · Updated September 2026

Countdown Family docs

Guides for traders and creators, the smart-contract reference, the HTTP API, and everything an operator needs to run the indexer and deploy the factory.

Contents

Overview

Countdown Family launches fixed-supply ERC-20 tokens directly into permanently locked Uniswap V3 positions on Robinhood Chain (chain 4663). There is no bonding curve and no migration: the token trades on Uniswap from the block it is created, and the liquidity NFT sits in a locker that has no withdrawal function.

For traders

Connect a wallet, pick a market, buy with ETH or sell for ETH. Every trade is a Uniswap V3 swap you sign yourself. See Trading.

For creators

One transaction: name, symbol, logo, description, socials, optional opening buy. Then pick your trading fee (0–5%), paid to you in ETH on every trade through the app — on top of 70% of the pool’s 1% swap fee. See Launching and Trading fees.

For integrators

Read markets and price history from GET /api/data. Every row carries block and transaction provenance. See HTTP API.

For operators

Run the indexer under PM2, point the app at a hosted database, deploy the factory. See Indexer and Deploying.

Getting started

Wallet

Any injected EVM wallet works: MetaMask, Rabby, Coinbase Wallet. The app will prompt you to add and switch to Robinhood Chain mainnet. If you add it manually:

FieldValue
Network nameRobinhood Chain
Chain ID4663
RPC URLhttps://rpc.mainnet.chain.robinhood.com
CurrencyETH
Explorerhttps://robinhoodchain.blockscout.com

Gas

Robinhood Chain uses ETH for gas, bridged from Ethereum. You need ETH on chain 4663 to do anything. There is no faucet for mainnet; the testnet faucet is irrelevant because Uniswap V3 — and therefore this protocol — does not exist on testnet.

The quickest routes in are Relay and Across (intent-based, seconds to minutes, from Base and other L2s) or the canonical Arbitrum bridge from Ethereum L1. Bridging back to Ethereum L1 through the canonical bridge takes 7 days. An empty wallet is offered the same routes in the trade panel.

Reading a market page

  • Price is the pool’s spot price from slot0, in WETH, converted to USD with a cached multi-source ETH/USD rate.
  • Market cap is price × total supply (fully diluted; supply is fixed, so this equals FDV).
  • Liquidity values both sides of the pool separately at the current price.
  • Volume (24h) is a count of trades, not a dollar amount.
  • Progress is WETH principal inside the locked position divided by the graduation threshold, clamped to 100%.
  • Stale appears when the snapshot is older than five minutes. The number shown is still real, just old.
  • The chart shows two kinds of points: trades (from Swap logs, each with a transaction hash) and samples (periodic slot0 reads). A flat line means nobody traded.

Launching a token

What you provide

FieldRequiredStoredNotes
NameYesOn chain (ERC-20 name)Non-empty. Also determines the CREATE2 address with the salt.
SymbolYesOn chain (ERC-20 symbol)Non-empty.
LogoNoOn chain (string)A URL. The upload endpoint returns one for an image you pick.
DescriptionNoOn chain (string)Plain text.
SocialsNoOn chain (5 strings)twitter, telegram, discord, website, farcaster.
Fee walletNoLocker feeRedirectsReceives the creator fee share and the opening buy. Defaults to your wallet.
Opening buy (ETH)NoSpent in the same txAnything you send above the launch fee.
Creator trading fee (%)NoTrade router, second tx0–5%. Set once, never raised. Default 2%. See Trading fees & rules.

What it costs

msg.value = launchFee + openingBuy. The launch fee is read from the factory at the time of launch (default 0.0005 ETH) and goes to the protocol treasury. The opening buy is swapped through the router at the pool price; you receive tokens, and the 1% swap fee on that buy accrues to your own locked position like any other trade.

What you get

  1. A token contract with 1,000,000,000 supply, 18 decimals, no mint function, no owner.
  2. A Uniswap V3 pool token/WETH at the 1% tier, initialised at the opening price (≈ 1.36 × 10⁻⁹ WETH).
  3. The entire supply deposited as a one-sided position, and that position’s NFT locked forever.
  4. A launch record on the factory and a TokenLaunched event the indexer picks up.
  5. Your opening-buy tokens, delivered to you (or your fee wallet) in the same transaction.

Before the wallet opens

The app dry-runs the exact launchToken call with eth_call. Anything that would revert — public launching closed and you are not allowlisted, a name/salt collision with an existing pool, a disabled config — is surfaced with the contract’s own revert reason before you pay anything.

The restriction window

For the launch block plus the next two, buys from the pool are capped at 5% of supply per wallet balance and 5.5% cumulative per recipient. Nobody but you can buy in the launch block at all. Your opening buy is exempt from the caps. After the window the token is a plain ERC-20 with no rules.

After launch

  • The indexer sees the event within one poll (default 20 s) and the market appears in the list.
  • You can verify everything on Blockscout: the token, the pool, the position NFT’s owner (the locker).
  • Fees start accruing on the first trade. See Creator fees.

Trading

Buying

  1. Enter an ETH amount. The app calls Quoter V2’s quoteExactInputSingle (simulated, since it is non-view) to get the token output for that exact size.
  2. Your slippage tolerance (default in the panel; 0–50%) is applied to the quote to derive amountOutMinimum.
  3. You sign exactInputSingle on SwapRouter02 with the ETH as msg.value. The router wraps it to WETH.
  4. Tokens arrive in your wallet. The pool emits Swap; the indexer records the trade.

Selling

  1. First sale of a token: approve the router for the token (unlimited allowance, one transaction).
  2. Enter a token amount; the quoter returns the WETH output.
  3. You sign one multicall: exactInputSingle with recipient address(2) (leave output in the router), then unwrapWETH9(minOut, you). The slippage floor is on the unwrap, so you receive ETH or the whole thing reverts.

Slippage

Set it against the quote, not against the chart. The chart shows spot price; the quote already includes the impact of your trade’s size. Reverts with a router error almost always mean the pool moved between the quote and inclusion and your tolerance was too tight — re-quote and try again.

What you pay

Every trade through the app shows one fee line before you sign: the creator’s fee (0–5%, chosen by the token’s deployer) plus Countdown Family’s 1%, both taken in ETH by the trade router. A token with a 2% creator fee shows 3%. Underneath, Uniswap’s own 1% pool fee applies to every swap on the chain regardless of route. The full rules are in Trading fees & rules.

What is not there

  • No limit orders, no order book. It is an AMM.
  • No server-side settlement. The app never holds your funds and never signs on your behalf.
  • No buying in the launch block for anyone but the creator, and caps for the 300 L2 blocks after it (about 30 seconds). If you get LaunchBlockBuyBlocked, MaxWalletExceeded, MaxTxExceeded, TF or ST, wait a moment or buy less.

Creator fees

You earn from two places.

StreamRatePaid inWhen
Your trading fee0–5%, chosen at launchETHInstantly, on every trade routed through countdown.family
Pool fee share70% of Uniswap’s 1%WETH + your tokenWhen you claim from the locked position

The pool fee share: every swap pays Uniswap’s 1% fee into the locked position. On claim, the locker splits it 70% to you (or your fee wallet), 30% to the protocol. The split was snapshotted when your token launched and cannot be changed for it afterwards. The trading fee is described in full under Trading fees & rules.

Seeing what you have earned

Your profile page lists every token you deployed or were assigned as fee recipient, across every known factory (Countdown Family’s and both of pons’s). For each, the app simulates collectFees and shows the result. The amounts are in WETH and in the token — fees are paid in whichever asset the trade brought in.

Claiming

Click claim; you sign collectFees(token) on the locker. Both assets are transferred in the same transaction. If nothing has accrued the call reverts NoFeesToCollect, which the app shows as zero.

Redirecting

Call setFeeRedirect(token, wallet) on the locker from the deployer wallet to send your share somewhere else — a multisig, a team wallet. Pass the zero address to clear it. The new recipient can also call collectFees.

Trading fees & rules

A trade on countdown.family pays three fees. Two are charged by CountdownTradeRouter in ETH; one is Uniswap’s and lives in the pool. The app shows the first two as a single line — a 2% creator fee displays as 3% — and the pool fee separately.

FeeRateSet byGoes toCharged by
Creator fee0–5% (default 2%)Token deployer, onceCreator wallet, in ETH, instantlyTrade router
Countdown Family fee1% (cap 2%)Protocol ownerTreasury, in ETH, instantlyTrade router
Pool fee1% (fixed)Uniswap V3 tierLocked position → 70/30 on claimUniswap pool

The rules

  1. The creator fee is set once and can never be raised. setTokenFee reverts FeeAlreadySet on a second call. A trader reads the fee on the token page and that is the fee for the life of the token. Only the recipient wallet can be changed.
  2. Only the token’s deployer can set it. The router reads deployer() from the token contract; anyone else gets NotTokenDeployer.
  3. It is capped at 5%. MAX_CREATOR_FEE_BPS = 500 is a constant; the owner cannot raise it. Above that, a fee stops being revenue and starts being a soft honeypot — a buyer who pays 10% in and 10% out is down 20% before the price moves. 5% is high enough to fund a serious team and low enough that a round trip still costs less than a bad day.
  4. Countdown Family’s fee is 1% and capped at 2%. MAX_PROTOCOL_FEE_BPS = 200. The owner can move it between 0 and 2%; a change applies to all routed trades from that block on.
  5. Fees are taken in ETH, not tokens. Off the input on a buy, off the output on a sell. The creator is paid on every trade instead of accumulating tokens they then have to dump on their own holders to realise.
  6. The slippage floor is after fees. amountOutMinimum on a sell is what the seller receives net; the app quotes the net figure and shows it.
  7. A fee recipient can never break a trade. Fees are pushed under a 30k gas stipend. A wallet that rejects ETH has the amount booked to pending and claims it later with claimPending(); the trade goes through either way.
  8. An unregistered token pays only Countdown Family’s 1%. If a deployer never calls setTokenFee, the creator leg is zero.

Why a router and not a tax in the token

The obvious way to charge a creator fee is a fee-on-transfer hook in the token. It was rejected for three reasons. Uniswap V3 pools check the exact amount they receive, so a token that taxes transfers into the pool makes every sell revert. Wallets and scanners flag fee-on-transfer tokens as tax tokens and warn users away. And the fee would be paid in tokens the creator then has to sell. A router charges ETH, keeps the token a plain ERC-20, and leaves Uniswap untouched.

The honest limitation
The router is a front door, not a wall. A trade sent directly to Uniswap or through an aggregator pays only the pool’s 1% and none of the router fees. The token cannot enforce them without becoming a tax token. In practice the app is where the liquidity is discovered, and the pool fee share still reaches the creator on every trade anywhere.

Worked example

A token with a 2% creator fee. A trader buys with 1 ETH: 0.02 ETH to the creator, 0.01 ETH to Countdown Family, 0.97 ETH swapped — of which Uniswap keeps 0.0097 in the locked position (0.00679 creator / 0.00291 protocol on claim). The trader receives tokens worth ≈ 0.9603 ETH at the pre-trade price, before price impact. A sell of tokens that fetch 1 ETH from the pool: Uniswap keeps 0.01 inside the pool first, 0.99 ETH comes out, 0.0198 to the creator, 0.0099 to Countdown Family, 0.9603 ETH to the seller.

Contract addresses

All addresses are on Robinhood Chain mainnet (4663). Click to open in Blockscout.

Countdown Family

ContractAddress
CountdownLaunchFactory0x1b79E530C86a3b3931f7240182359cac0E7B7af1
CountdownLaunchLockerRead locker() on the factory
CountdownTokenDeployerRead tokenDeployer() on the factory
CountdownTradeRouter0xe6A96d79BB6B7F22460970AFe42636e275ced419

Shared infrastructure (Uniswap V3 on Robinhood Chain)

pons factories (indexed, not operated by us)

Addresses are constants in lib/pons-contracts.ts, not environment variables: new versions ship as new addresses, so a bump is a reviewed code change rather than a deploy-time flag.

Reference: CountdownLaunchFactory

contracts/launch/CountdownLaunchFactory.sol · GPL-2.0-or-later · Ownable2Step, ReentrancyGuard

Write

FunctionAccessDescription
launchToken(TokenParams, uint256 launchConfigId, uint256 dexId, bytes32 salt) payable → addressAnyone (when launchEnabled) or allowlistedChecks the tick against the pool spacing, pays the fee, deploys through the token deployer, pools, locks, records, hands rounding dust to the creator, and buys. Returns the token address.
addDexConfig(DexConfig) → uint256OwnerRegister a V3 factory/positionManager/router/fee/tickSpacing set.
setDexStatus(uint256, bool)OwnerEnable or disable a DEX config.
addLaunchConfig(LaunchConfig) → uint256OwnerRegister a parameter set. Validates maxTxBps = 110% of maxWalletBps, supply ≥ 1e18, tick non-zero and in range.
updateLaunchConfig(uint256, LaunchConfig)OwnerReplace a parameter set. Tokens already launched keep their immutables.
setLaunchFee(uint256)OwnerChange the ETH launch fee.
setLaunchEnabled(bool)OwnerOpen/close public launching.
setWhitelistedLauncher(address, bool)OwnerAllowlist while closed.

Read

FunctionReturns
getLaunchedToken(address) → LaunchedTokentoken, deployer, pairedToken, positionManager, positionId, dexId, launchConfigId, restrictionsEndBlock, supply, isToken0, poolFee, exists, initialBuyAmount
graduationStatus(address) → (pairedPrincipal, threshold, graduated)WETH principal in the locked position vs the per-token threshold.
predictTokenAddress(TokenParams, configId, dexId, salt, deployer) → addressCREATE2 address for the inputs, via tokenDeployer.predict with the factory as the salt namespace.
getDexConfig(uint256) / dexConfigCount()DEX configs.
getLaunchConfig(uint256) / launchConfigCount()Launch configs.
launchFee() / launchEnabled() / whitelistedLaunchers(address) / locker() / tokenDeployer()Policy and wiring.

Structs

Solidity
struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }

struct TokenParams {
    string name; string symbol; string logo; string description;
    Socials socials;
    address feeWallet;          // 0x0 = deployer receives fees and the opening buy
}

struct DexConfig {
    string name; address factory; address positionManager; address swapRouter;
    uint24 poolFee; int24 tickSpacing; bool enabled;
}

struct LaunchConfig {
    address pairToken; uint256 graduationThreshold; int24 initialTick; uint256 supply;
    uint16 maxWalletBps; uint16 maxTxBps; uint32 restrictionBlocks; uint24 reservedFee;
    bool enabled; bool routerRequiresDeadline;
}

Events

Solidity
event TokenDeployed(address indexed token, address indexed deployer, address indexed dexFactory,
                    address pairToken, uint256 dexId, uint256 launchConfigId);

event TokenLaunched(address indexed token, address indexed deployer, address indexed dexFactory,
                    address pairToken, address pool, uint256 dexId, uint256 launchConfigId,
                    uint256 positionId, uint256 restrictionsEndBlock, uint256 initialBuyAmount);
// topic0 is byte-identical to pons's TokenLaunched. A fork test asserts this.

event DexConfigAdded(...); event DexStatusUpdated(uint256 indexed id, bool enabled);
event LaunchConfigAdded(...); event LaunchConfigUpdated(...);
event LaunchFeeUpdated(uint256); event LaunchEnabledUpdated(bool);
event WhitelistedLauncherUpdated(address indexed launcher, bool enabled);

Errors

ErrorWhen
NotWhitelistedlaunchEnabled is false and caller is not allowlisted.
LaunchFeeNotPaidmsg.value < launchFee.
InvalidDexId / DexDisableddexId out of range or disabled.
InvalidLaunchConfigId / LaunchConfigDisabledlaunchConfigId out of range, disabled, or an invalid tick on add.
InvalidTokenParamsEmpty name or symbol.
PoolAlreadyExistsA V3 pool already exists for the predicted token address at this fee tier. Change the salt.
InvalidInitialTickThe launch config’s initialTick is not a multiple of the DEX config’s tickSpacing. Checked before the fee is paid.
FeeTransferFailedThe protocol fee recipient rejected the launch fee.
TokenDeploymentFailedThe token deployer returned zero or an address other than the prediction (salt reuse with identical params).
RouterNotSetDeclared for ABI parity; unreachable since a DEX config without a router is rejected on add.
InvalidBasisPoints / InvalidMaxTxBasisPoints / SupplyTooLow / InvalidDexConfig / ZeroAddressConfig validation. InvalidDexConfig includes a zero swapRouter.
TokenNotFoundgraduationStatus for a token this factory did not launch.

Reference: CountdownLaunchLocker

contracts/launch/CountdownLaunchLocker.sol · MIT · Ownable2Step, ReentrancyGuard, IERC721Receiver · MAX_PROTOCOL_FEE_SHARE = 50

FunctionAccessDescription
initialize(address factory)Owner, onceBinds the locker to its factory.
onERC721Received(operator, from, id, data)Position manager callbackAccepts only when operator and from are both the factory.
lockPosition(address token)FactoryVerifies ownerOf(positionId) == locker, marks locked, snapshots protocolFeeShare, indexes deployer.
collectFees(address token) → (amount0, amount1)Owner, deployer, redirect recipient, or feeCollectorsCollects from the position, splits, transfers. Reverts NoFeesToCollect on zero.
setFeeRedirect(address token, address wallet)Deployer or factorySets the creator-share recipient; maintains the reverse index.
setFeeCollector(address, bool)OwnerAllow an operator to trigger collection.
setProtocolFeeRecipient(address)OwnerWhere the protocol share (and launch fees) go.
setProtocolFeeShare(uint256 ≤ 50)OwnerShare copied by future launches.
deployerTokens / deployerTokenCount / feeRecipientTokens / feeRecipientTokenCountViewProfile indexes.
tokenProtocolFeeShares(address) / feeRedirects(address) / getLaunchedToken(address)ViewPer-token state.
Events
event PositionLocked(address indexed token, address indexed deployer, uint256 indexed dexId,
                     address pairToken, uint256 positionId, address positionManager);
event FeesClaimed(address indexed token, address indexed caller, address token0, address token1,
                  uint256 recipientAmount0, uint256 recipientAmount1,
                  uint256 protocolAmount0, uint256 protocolAmount1);
event FeeRedirectUpdated(address indexed token, address indexed newFeeWallet);
event FeeCollectorUpdated(address indexed collector, bool enabled);
event ProtocolFeeRecipientUpdated(address recipient);
event ProtocolFeeUpdated(uint256 share);
event FactoryUpdated(address indexed factory);

Reference: CountdownLauncherToken

contracts/launch/CountdownLauncherToken.sol · MIT · OpenZeppelin ERC20 · no owner, no mint, no pause.

FunctionDescription
liquidityPool() → addressThe canonical pool: getPool(this, pairToken, poolFee) on the V3 factory.
socials() → (twitter, telegram, discord, website, farcaster)On-chain socials.
getTokenInfo() → (deployer, logo, description, Socials)Launcher-compatible metadata tuple.
maxWalletLimit() / maxWalletAmount()totalSupply × maxWalletBps / 10000.
maxTxLimit() / maxTxAmount()totalSupply × maxTxBps / 10000 — cumulative per recipient during the window.
setInitialBuyRecipient(address)Factory only. Opens/closes the launch-block exemption.
currentBlock() → uint256The height the window is measured in: ArbSys.arbBlockNumber() on Robinhood Chain, block.number where the precompile is absent.
restrictionsActive() → boolTrue while pool and router buys are still capped.
deployer, launchFactory, dexFactory, positionManager, pairToken, swapRouter, poolFee, launchBlock, restrictionBlocks, restrictionEndBlock, maxWalletBps, maxTxBpsImmutables. launchBlock and restrictionEndBlock are L2 block heights.
logo, descriptionPublic strings.
Transfer hook (simplified)
if (from != 0 && to != 0 && !_windowClosed) {
    uint256 now_ = _currentBlock();                     // ArbSys.arbBlockNumber(), else block.number
    if (now_ > restrictionEndBlock) { _windowClosed = true; return super._update(...); }

    bool fromPool = isPairPool(from), fromRouter = from == swapRouter;
    if (!fromPool && !fromRouter) return super._update(...);   // wallet-to-wallet: plain ERC-20

    bool atomic = fromPool && now_ == launchBlock && to == _initialBuyRecipient && _initialBuyRecipient != 0;
    if (!atomic && now_ == launchBlock) revert LaunchBlockBuyBlocked(to);
    if (!atomic && to != swapRouter) {                  // tokens parked in the router are nobody's yet
        require(balanceOf(to) + value <= maxWalletLimit(), MaxWalletExceeded);
        require(_restrictedPoolBuys[to] + value <= maxTxLimit(), MaxTxExceeded);
        _restrictedPoolBuys[to] += value;
    }
}
super._update(from, to, value);

isPairPool matches the canonical pool and also any pool the V3 factory returns for (this, pairToken, candidate.fee()), so a pool created at a different fee tier cannot bypass the window. Transfers from the swap router are capped like pool buys and keyed by the receiving wallet, which closes the recipient = address(2) + sweepToken route around the wallet cap. Once a transfer sees the window closed the token records it and never reads the block again.

Why L2 blocks, and why 300
Robinhood Chain is an Arbitrum Orbit L2. Inside the EVM, block.number is the Ethereum block height — measured on 20 September 2026 at 26.0 million against an L2 height of 67.5 million — and it moves once every ~12 seconds. The token therefore reads ArbSys(0x64).arbBlockNumber(), the height the explorer and the indexer count, through a gas-capped call with a block.number fallback. L2 blocks arrive about every 0.1 s, so restrictionBlocks = 300 is roughly 30 seconds of caps after the launch block. It is about ordering, not duration: the launch block is the creator’s alone.

Reference: CountdownTradeRouter

contracts/launch/CountdownTradeRouter.sol · MIT · Ownable2Step, ReentrancyGuard · MAX_CREATOR_FEE_BPS = 500, MAX_PROTOCOL_FEE_BPS = 200

FunctionAccessDescription
buy(address token, uint256 amountOutMinimum) payable → amountOutAnyoneTakes fees off msg.value, swaps the rest via SwapRouter02, tokens go to the caller.
sell(address token, uint256 amountIn, uint256 amountOutMinimum) → ethOutAnyone (approve router first)Swaps to WETH, unwraps, takes fees, pays the caller. Floor is on ethOut.
setTokenFee(address token, uint16 bps, address wallet)Token deployer, once0–500 bps. Zero wallet = caller.
setCreatorWallet(address token, address wallet)Current wallet or deployerMoves the recipient; the bps never change.
setProtocolFeeBps(uint16) / setProtocolFeeRecipient(address)Owner0–200 bps; where Countdown Family’s leg goes.
claimPending()Anyone with a balanceWithdraws fees that could not be pushed.
rescueERC20(address token, address to)OwnerReturns tokens sent to the router by mistake. The router never holds a trader’s tokens between transactions, so nothing in flight can be touched. ETH is not covered.
feeFor(address) → (creatorBps, protocolBps, totalBps, creatorWallet)ViewWhat the UI displays.
tokenFees(address) / pending(address) / protocolFeeBps() / protocolFeeRecipient()ViewState.
Events
event Trade(address indexed token, address indexed trader, bool isBuy,
            uint256 ethAmount, uint256 tokenAmount, uint256 creatorFee, uint256 protocolFee);
event TokenFeeSet(address indexed token, address indexed creatorWallet, uint16 creatorFeeBps);
event CreatorWalletUpdated(address indexed token, address indexed creatorWallet);
event ProtocolFeeUpdated(uint16 protocolFeeBps);
event ProtocolFeeRecipientUpdated(address recipient);
event FeeDeferred(address indexed recipient, uint256 amount);
event PendingClaimed(address indexed recipient, uint256 amount);
event TokenRescued(address indexed token, address indexed to, uint256 amount);
ErrorWhen
UnexpectedEtherPlain ETH sent to the router by anything other than WETH9.withdraw. Refused so nothing can be stranded.
NotTokenDeployersetTokenFee from anyone but token.deployer().
FeeAlreadySetsetTokenFee called a second time.
FeeTooHighCreator bps > 500 or protocol bps > 200.
NotCreatorWalletsetCreatorWallet from anyone but the current wallet or deployer.
InsufficientOutputSell output after fees below amountOutMinimum, or ETH transfer to seller failed.
ZeroAmount / ZeroAddress / NothingPendingInput validation.

Reference: CountdownTokenDeployer

contracts/launch/CountdownTokenDeployer.sol · MIT · no owner, no settings, no state. It holds the token’s creation code and performs the CREATE2 on the factory’s behalf.

Why it exists: EIP-170 caps a contract at 24,576 bytes of runtime code. Embedding the token’s ~5.8 KB in the factory left it 945 bytes over the cap once the launch-window fixes landed. With the bytecode here the factory is 15,699 bytes.

FunctionAccessDescription
deploy(bytes32 salt, bytes constructorArgs) → addressAnyoneCREATE2 with the salt namespaced as keccak256(msg.sender, salt). Reverts TokenDeploymentFailed on a zero address.
predict(address caller, bytes32 salt, bytes constructorArgs) → addressViewThe address deploy would return for that caller.
creationCodeHash() → bytes32Viewkeccak256 of the token creation code, for off-chain derivation.

HTTP API

Two routes serve the frontend and are public. Both return JSON, both are force-dynamic. There is no authentication and no write endpoint for trading — writes are wallet transactions.

GET /api/config

Response
{
  "chainId": 4663,
  "chainName": "Robinhood Chain",
  "explorer": "https://robinhoodchain.blockscout.com",
  "rpc": "https://rpc.mainnet.chain.robinhood.com",
  "pons": { "factories": ["0x0c37…", "0xa5aa…"], "pairToken": "0x0bd7…", "pairSymbol": "WETH", "site": "https://pons.family" },
  "launchpad": { "contractAddress": null, "onchain": false },
  "featureFlags": { "launching": false, "trading": false, "ponsMarkets": true },
  "simulated": false,
  "dataSource": "indexed-onchain",
  "notice": "Launching and trading are disabled until … Market data below is indexed from pons on Robinhood Chain and is real."
}

GET /api/data?kind=updates

The polling endpoint. Six monotonic counters and nothing else. Send If-None-Match with the previous ETag and receive 304 when nothing moved. positions moves when the indexer folds trades into PnL positions, alerts when it writes alert rows.

Response
{ "launches": "118:12", "prices": 902, "markets": 340, "trades": 1204, "positions": 88, "alerts": 5 }
// launches = "<row count>:<version>"

GET /api/data?kind=markets&offset=0&sort=created|marketCap|oldest|…

A page of 50 markets, each with up to 60 trimmed price points for sparklines. This is also the screener: every filter the Discover page offers is a query parameter (status, factory, maxAgeH, minLiquidity, minHolders, maxTop10Pct, maxCreatorPct, minVolLiq, minProgress, antiSnipePassed, noRugCreator, watchlist, q, spark=1h|24h), each bounded server-side. kind=markets-lean is the same page without points.

Response
{
  "markets": [ { …Market } ],
  "hasMore": true,
  "nextOffset": 50
}

GET /api/data?kind=market&token=0x…

One market with up to 1,000 price points. 400 on a malformed address, 404 if unknown.

Market shape
{
  "token": "0x…",            // lowercased token address (identity)
  "curve": "0x…",            // the Uniswap V3 pool
  "helper": "0x…",           // position manager holding the NFT
  "creator": "0x…",
  "pairToken": "0x…", "pairSymbol": "WETH", "pairDecimals": 18,
  "name": "…", "symbol": "…", "logo": "…", "description": "…",
  "socials": { "twitter": "…", "telegram": "…", "discord": "…", "website": "…", "farcaster": "…" },
  "supply": "1000000000000000000000000000",   // uint256 as string
  "decimals": 18, "configId": 0, "leverage": 1,
  "block": 8601234, "blockHash": "0x…", "hash": "0x…", "createdAt": 1757000000000,
  "live": {
    "price": 1.36e-9,        // WETH per token
    "usdPrice": 4.07e-6,
    "marketCap": 4067,       // USD, fully diluted
    "liquidity": 4100,       // USD, both sides valued
    "pairUsd": 3000,         // ETH/USD used
    "volume": 12,            // COUNT of trades in 24h
    "graduated": false,
    "progress": 0.31,        // 0..1
    "stale": false,
    "syncedAt": 1757000123000, "syncedBlock": 8602000,
    "points": [ { "time": 1757000000000, "price": 1.36e-9, "usdPrice": 4.07e-6, "source": "swap", "txHash": "0x…" } ]
  }
}

Other read kinds

Every number below comes from tables the indexer writes; nothing is computed on the request path beyond a bounded query, and a value the indexer has not produced yet is served as null, 0 or -1 (the UI prints “—”).

  • kind=position&wallet&token — the wallet’s average-cost position on one token (WETH wei strings, pending while its last trade is not folded yet). kind=wallet-pnl&wallet — every position plus wallet totals.
  • kind=safety&token — creator track record, creator holding and sells, top-10 concentration, anti-snipe window, launch-block buyers.
  • kind=candles&token&res=1m|5m|15m|1h|4h|1d&from&to[&wallet] — OHLC in WETH and USD with volume, at most 1,000 buckets, plus trade markers (the 300 largest in range and all of wallet’s own).
  • kind=feed[&kinds=whale,creator_sell,top_holder_sell,graduated,launch][&minUsd][&token][&before&beforeId] — whale and dev moves, keyset paged.
  • kind=markets-by&tokens=a,b,c — up to five markets with their safety payloads, for /compare. kind=holders rows now carry tags (creator, pool, smart, fresh, top10).
  • kind=watchlist, alert-rules, alerts, alerts-hub, following, telegram-status (all &wallet=) — the signed per-wallet features. Reads are by wallet parameter like the portfolio; writes go through POST kinds watch, alert-rule, follow and telegram-link, each an EIP-191 signature over a countdown.family … message with a five-minute window.

POST /api/upload

Multipart form with a file field. Writes to public/uploads/ and returns { success, url }. Used to produce a logo URL before launch. On a serverless host the filesystem is ephemeral; replace with object storage before production.

Running the indexer

scripts/pons-indexer.ts is a long-running worker. It must be its own process: the first pass sweeps the whole chain and would blow any serverless request budget, and it has to stay alive to keep snapshots fresh.

Shell
npm run pons:index            # poll forever (default every 20 s)
npm run pons:index -- --once  # single pass, for cron

# production
pm2 start ecosystem.pons.config.js
pm2 save

What one pass does

  1. Reads the cursor (indexer_state, key pons:4663) and the head block.
  2. eth_getLogs in spans of PONS_SCAN_SPAN blocks for launch topics on every factory in PONS_FACTORIES.
  3. For each new token: reads name/symbol/decimals/logo/description/socials from the token, liquidityPool(), getLaunchedToken from whichever factory knows it, and inserts a markets row with block/blockHash/hash.
  4. Refreshes every market’s snapshot: slot0 price, graduationStatus, pool balances for liquidity, 24h trade count. One bad market does not kill the pass.
  5. Backfills Swap logs on each pool into price_points (source swap) and writes a sample point from the current price.
  6. Bumps data_versions counters so kind=updates changes.

RPC pacing

Every call goes through paced(): minimum gap PONS_RPC_MIN_INTERVAL_MS, exponential backoff on 429 up to PONS_RPC_MAX_BACKOFF_MS, at most PONS_RPC_MAX_ATTEMPTS. On the public endpoint set the interval to ~1500 ms; for production use a dedicated provider.

Adding a factory
The cursor is global. Adding a factory to PONS_FACTORIES without rewinding PONS_START_BLOCK to that factory’s deployment block orphans every token it launched before the cursor. Rewind, then restart.

Environment variables

VariableDefaultPurpose
DATABASE_URL / DATABASE_AUTH_TOKENlocal.dbHosted libSQL/Turso. Required on any ephemeral filesystem.
NEXT_PUBLIC_ETH_PRICE_USD3000Fallback ETH/USD if every live source fails.
NEXT_PUBLIC_ROBINHOOD_CHAIN_MAINNET_RPCpublic RPCBrowser RPC for reads the wallet needs (balances, simulations).
ROBINHOOD_CHAIN_MAINNET_RPCpublic RPCServer/indexer/Hardhat RPC. Use a dedicated provider.
NEXT_PUBLIC_LAUNCH_FACTORYpons active factoryOur factory once deployed. Unlocks launching.
NEXT_PUBLIC_TRADE_ROUTEROur trade router. When set, trades pay creator + Countdown Family fees in ETH.
NEXT_PUBLIC_APP_URLhttps://countdown.familyPublic origin: canonical URLs, share cards, robots and sitemap.
TRUSTED_PROXY_HOPS0How many proxies sit in front of the app; rate limits read the client IP from X-Forwarded-For only when > 0.
PROTOCOL_TRADE_FEE_BPS100Countdown Family’s trade fee at deploy, ≤ 200.
PONS_FACTORIESlegacy,activeComma-separated factories to index, oldest first.
PONS_START_BLOCK0First block of the initial sweep.
PONS_SCAN_SPAN4000000Blocks per eth_getLogs.
PONS_POLL_MS20000Gap between passes.
PONS_RPC_MIN_INTERVAL_MS250Min gap between RPC calls.
PONS_RPC_MAX_ATTEMPTS8Retries per call.
PONS_RPC_MAX_BACKOFF_MS30000Backoff ceiling.
PONS_STALE_MS300000Snapshot age that reports stale.
PRIVATE_KEYHardhat deployer.
TREASURY_ADDRESSdeployerProtocol fee recipient at deploy.
PROTOCOL_FEE_SHARE30Percent to protocol, ≤ 50.
LAUNCH_FEE_ETH0.0005Launch fee at deploy.
BLOCKSCOUT_API_KEYOptional; verification works without it.

Deploying the factory

Shell
export PRIVATE_KEY=0x…
export TREASURY_ADDRESS=0x…          # multisig recommended
export PROTOCOL_FEE_SHARE=30
export LAUNCH_FEE_ETH=0.0005
export ROBINHOOD_CHAIN_MAINNET_RPC=https://…   # dedicated provider

npm run contracts:compile
npm run contracts:test                              # 41 offline tests
FORK=1 FORK_BLOCK=<recent> npm run contracts:test   # + 19 on a mainnet fork (60)
FORK=1 FORK_BLOCK=<recent> TREASURY_ADDRESS=0x…   npx hardhat --tsconfig tsconfig.hardhat.json deploy-launchpad --network hardhat   # rehearsal
npm run contracts:deploy:dry-run     # pre-flight + gas against mainnet, sends nothing
npm run contracts:deploy

Behind an ISP that filters the public RPC hostname (every Indonesian ISP does), run npm run rpc:proxy in a second terminal and use ROBINHOOD_CHAIN_MAINNET_RPC=http://127.0.0.1:8545. Pin FORK_BLOCK to a block from the last few minutes: the public node prunes older state.

What the script does

  1. Deploys CountdownTokenDeployer() — no arguments, no owner.
  2. Deploys CountdownLaunchLocker(owner, treasury, share).
  3. Deploys CountdownLaunchFactory(owner, locker, tokenDeployer, launchFee).
  4. Deploys CountdownTradeRouter(owner, SwapRouter02, WETH, 10000, treasury, protocolTradeFeeBps).
  5. locker.initialize(factory) — without this every launch reverts.
  6. factory.addDexConfig for Uniswap V3 at the 1% tier, tickSpacing 200.
  7. factory.addLaunchConfig: WETH pair, 1e9 supply, tick −204200, 4.2 WETH graduation, 5%/5.5% caps, 300 L2 blocks.
  8. Reads back launchEnabled, locker.factory() and factory.tokenDeployer(), verifies all four on Blockscout, and prints the env lines to paste into .env.local.

After deploy

  1. Set NEXT_PUBLIC_LAUNCH_FACTORY and NEXT_PUBLIC_TRADE_ROUTER, and append the factory to PONS_FACTORIES.
  2. Restart the indexer before anyone launches.
  3. Verify on Blockscout (no API key needed).
  4. Do the first launch and first swap with trivial amounts and reconcile the indexed rows against pool state.
  5. Transfer ownership of the locker, factory and router to a multisig with transferOwnership + acceptOwnership. The token deployer has no owner.

Local development

Shell
npm install
cp .env.example .env.local
npm run db:push          # create the libSQL schema
npm run pons:index -- --once   # populate markets from chain (needs RPC access)
npm run dev              # http://localhost:3000
CommandWhat it does
npm run dev / build / startNext.js.
npm run typecheckApp TypeScript.
npm run db:push / db:generate / db:studioDrizzle schema, migrations, browser.
npm run pons:indexThe indexer.
npm run contracts:compileHardhat compile (viaIR, shanghai).
npm run contracts:testContract tests. FORK=1 for the mainnet-fork suite.
npm run contracts:typecheckHardhat TypeScript (separate tsconfig for typechain).
npm run contracts:deployDeploy the token deployer, locker, factory and trade router to mainnet (contracts:deploy:dry-run simulates).
npm run rpc:proxyLocal JSON-RPC forwarder for when the public RPC hostname is DNS-filtered.
npm run brand:iconsRenders every raster icon from public/brand/*.svg.
npm testNode tests for the price and event maths in lib/pons-math.ts.

Repository map

Tree
contracts/launch/         CountdownTokenDeployer, CountdownLaunchFactory, CountdownLaunchLocker, CountdownLauncherToken,
                          CountdownTradeRouter, interfaces, libraries
lib/pons-contracts.ts     client-safe addresses + write ABIs
lib/pons.ts               server-only read layer (ABIs, paced RPC, price math)
lib/pons-indexer.ts       indexer logic
lib/use-pons-launch.ts    usePonsLaunch()
lib/use-pons-swap.ts      usePonsQuote(), usePonsBuy(), usePonsSell()
lib/use-pons-fees.ts      usePonsCreatorFees()
lib/use-trade-router.ts   routed buys/sells through CountdownTradeRouter
lib/use-markets.ts        client hooks over /api/data
lib/eth-price.ts          cached multi-source ETH/USD
lib/rate-limit.ts         sliding-window limiter (rate_limits table)
app/api/data, config      the public API
scripts/pons-indexer.ts   worker entry
scripts/deploy-launchpad.ts
test/CountdownLauncherToken.unit.test.ts, CountdownLaunchFactory.unit.test.ts, CountdownTradeRouter.test.ts
test/CountdownLaunchFactory.fork.test.ts
test/pons-*.test.ts       node:test suites for lib/ (npm test)
docs/CONTRACT-NOTES.md    the contract review and what it changed
docs/DEPLOY-MAINNET.md    the deployment runbook
db/schema.ts              markets, trades, balances, price_points, market_comments, rate_limits, data_versions

Testing

Four contract suites, 60 cases, last run in full on 20 September 2026.

SuiteRuns onCasesCovers
CountdownLauncherToken.unit.test.tsplain hardhat, mock pool and router7The transfer hook: window measurement, launch-block exclusivity and the exemption, the 5% wallet and 5.5% cumulative caps, the router-sweep leg, the one-time close, factory-only exemption control.
CountdownLaunchFactory.unit.test.tsplain hardhat22Construction and wiring, the EIP-170 size guard, event topic hashes the indexer keys on, config validation including the tick-spacing and zero-router checks, the launch gate, address prediction and the deployer salt namespace, two-step ownership.
CountdownTradeRouter.test.tsplain hardhat, mocks12Fee arithmetic on both legs of both directions, once-only ≤ 5% creator fees, the ≤ 2% owner-only protocol fee, the after-fee slippage floor, deferred fees, ETH refusal, token rescue, zero-amount rejection.
CountdownLaunchFactory.fork.test.tsfork of chain 4663, real Uniswap V319A real launch end to end: pool creation, full-supply mint with dust to the creator, the opening buy and its exemption, caps inside the window through the router and through a parked-and-swept buy, the lift after the window, fee payment, locking, graduation, fee split and redirects, the launch-block rule.

npm run contracts:test runs the 41 offline cases; FORK=1 FORK_BLOCK=<recent> adds the fork suite. npm test runs the 167 node:test cases over lib/ (price and event maths, screener parameters, signed messages, swap preferences, component recipes). The four client write hooks have no automated tests yet; they need a fork fixture to test honestly.

Security & audits

  • Source is published under MIT / GPL-2.0-or-later and is verified on Blockscout at deploy.
  • Ownable2Step on the locker, factory and router; ownership moves to a multisig before the first public launch. The token deployer has no owner.
  • ReentrancyGuard on the state-changing entry points; SafeERC20 throughout.
  • No proxies, no upgradeability, no selfdestruct, no arbitrary external calls.
  • Reporting: open an issue on the repository. There is no bug bounty programme at this time.

What the September 2026 review changed

The full record with measurements is docs/CONTRACT-NOTES.md. In short:

  • The launch window is measured in L2 blocks. block.number on Robinhood Chain is Ethereum’s height (measured, not assumed); the token now reads ArbSys.arbBlockNumber() with a gas-capped fallback.
  • The router-sweep route around the wallet cap is closed. Router-to-wallet transfers during the window are capped like pool buys.
  • initialTick is checked against the pool spacing before any fee is paid.
  • Dex configs without a router are rejected; mint rounding dust goes to the creator instead of sitting in the factory.
  • The factory is 8.9 KB under the size limit instead of 945 bytes over, by moving the token bytecode into CountdownTokenDeployer.
  • The router refuses stray ETH and can return stray tokens to their sender.

FAQ

Can the creator rug the liquidity?

No. The position NFT is in the locker, which has no function to move it. The creator never holds it.

Can Countdown Family rug the liquidity?

No. Same locker, same absence of a withdrawal function. The owner can change future fee policy only.

Can the creator mint more tokens?

No. The token has no mint function after construction and no owner.

Why does the launch button say launching is disabled?

The Countdown Family factory is not deployed yet, so the app falls back to pons’s factory, which is allowlist-only. Trading is unaffected.

Why is there no testnet?

Uniswap V3 is not deployed on Robinhood Chain testnet, and this protocol cannot exist without it.

Why does my sell revert?

Usually slippage: the pool moved between the quote and inclusion. Re-quote. If you are inside the launch window, buys can also revert with MaxWalletExceeded, MaxTxExceeded, LaunchBlockBuyBlocked, or their wrapped forms TF and ST; wait a moment or buy less.

How long is the launch window, exactly?

The launch block itself, in which only the creator’s opening buy can take tokens out of the pool, and then 300 L2 blocks in which every wallet is held to 5% of supply and 5.5% bought in total. L2 blocks arrive about every 0.1 s, so that is roughly 30 seconds. The token page counts the blocks down.

Why not measure the window in ordinary blocks?

Because on Robinhood Chain block.number is Ethereum’s block, not the chain’s own. We measured it: 26.0 million inside the EVM against 67.5 million from eth_blockNumber. A window written in that unit would be twelve seconds per block and could not be compared with anything the indexer stores.

Can a bot buy more than 5% during the window by routing around the cap?

Not through the sweep route it used to have: tokens the router hands out are capped exactly like tokens the pool hands out. What no per-wallet cap stops is a sniper with many wallets; that is why the launch block is reserved for the creator and why the caps are described as an ordering rule, not a guarantee.

What is the token deployer, and can someone take my token’s address?

A small contract that holds the token bytecode and performs the CREATE2 for the factory, so the factory stays under the 24 KB code limit. Every caller has its own salt space, so an address the factory predicts can only ever be created by the factory.

Can the router owner take my tokens or ETH?

No. The router holds your tokens only inside a single sell transaction. rescueERC20 returns tokens that someone sent to the router by mistake; it cannot reach anything in flight, and it does not cover ETH at all — the only ETH the router ever holds is a deferred fee waiting for its recipient.

Which contracts does a launch touch?

Four of ours — the token deployer, the factory, the locker and (for the creator fee) the trade router — and Uniswap V3’s factory, position manager and SwapRouter02. Addresses are in Contract addresses once deployed.

Why does volume look small?

It is a count of trades, not a dollar figure. We do not compute notional volume, so we do not show one.

My fees show zero.

Nothing has accrued since the last claim. Fees are never pushed automatically; zero means “not yet”.

Can the creator raise the trading fee later?

No. It is set once; the router reverts a second call. Only the receiving wallet can change.

Why does a trade on Uniswap directly cost less?

Because the creator and Countdown Family fees are charged by our router, not by the token. Trading elsewhere skips them by design; the alternative was a tax token that breaks V3 sells.

Is graduation a migration?

No. It is a flag: WETH principal in the locked position ≥ 4.2. Nothing moves when it flips.

What is the relationship with pons?

None. The contracts are adapted from their MIT source; the app indexes their public factories. pons does not operate or endorse Countdown Family.

Glossary

TermMeaning
TickUniswap V3’s log-price unit: price = 1.0001^tick.
sqrtPriceX96Square root of price as a Q64.96 fixed-point number, from slot0.
One-sided positionA V3 position holding only one asset because its range is entirely above (or below) the current price.
Position NFTThe ERC-721 the position manager mints to represent a liquidity position.
LockerThe contract that holds position NFTs permanently and splits collected fees.
Paired principalThe WETH inside the locked position at the current price; drives graduation.
Restriction windowlaunchBlock through launchBlock + 300, in L2 blocks, when pool and router buys are capped.
L2 blockA Robinhood Chain block as counted by eth_blockNumber and ArbSys.arbBlockNumber(); about 0.1 s apart. Distinct from block.number, which is Ethereum’s height on this chain.
Token deployerCountdownTokenDeployer: holds the token bytecode and runs CREATE2 for the factory, with salts namespaced per caller.
Opening buyThe creator’s purchase executed inside launchToken with msg.value − launchFee.
Fee redirectA per-token override of who receives the creator fee share.
Trade routerCountdownTradeRouter: the app’s swap entry point that charges creator + Countdown Family fees in ETH.
Creator feeThe 0–5% a deployer sets once on their token; paid in ETH per routed trade.
SnapshotA per-market row of live figures refreshed by the indexer; stale after 5 minutes.
swap / sampleThe two sources of a price point: a Swap log or a slot0 read.
ponsA third-party launchpad on Robinhood Chain whose contracts this protocol adapts and whose markets it indexes.