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
For creators
For integrators
GET /api/data. Every row carries block and transaction provenance. See HTTP API.GET /api/config → featureFlags.launching for the live state.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:
| Field | Value |
|---|---|
| Network name | Robinhood Chain |
| Chain ID | 4663 |
| RPC URL | https://rpc.mainnet.chain.robinhood.com |
| Currency | ETH |
| Explorer | https://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
Swaplogs, each with a transaction hash) and samples (periodicslot0reads). A flat line means nobody traded.
Launching a token
What you provide
| Field | Required | Stored | Notes |
|---|---|---|---|
| Name | Yes | On chain (ERC-20 name) | Non-empty. Also determines the CREATE2 address with the salt. |
| Symbol | Yes | On chain (ERC-20 symbol) | Non-empty. |
| Logo | No | On chain (string) | A URL. The upload endpoint returns one for an image you pick. |
| Description | No | On chain (string) | Plain text. |
| Socials | No | On chain (5 strings) | twitter, telegram, discord, website, farcaster. |
| Fee wallet | No | Locker feeRedirects | Receives the creator fee share and the opening buy. Defaults to your wallet. |
| Opening buy (ETH) | No | Spent in the same tx | Anything you send above the launch fee. |
| Creator trading fee (%) | No | Trade router, second tx | 0–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
- A token contract with 1,000,000,000 supply, 18 decimals, no mint function, no owner.
- A Uniswap V3 pool token/WETH at the 1% tier, initialised at the opening price (≈ 1.36 × 10⁻⁹ WETH).
- The entire supply deposited as a one-sided position, and that position’s NFT locked forever.
- A launch record on the factory and a
TokenLaunchedevent the indexer picks up. - 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.
initialBuyAmount in the launch event and shown on the token page. A large one signals conviction to some buyers and a dump risk to others. With FDV ≈ 1.36 ETH at launch, 0.5 ETH buys roughly 27% of supply and nearly doubles the price; 1 ETH buys about 42% and triples it. Do the maths with the table in whitepaper §6 before you decide.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
- 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. - Your slippage tolerance (default in the panel; 0–50%) is applied to the quote to derive
amountOutMinimum. - You sign
exactInputSingleon SwapRouter02 with the ETH asmsg.value. The router wraps it to WETH. - Tokens arrive in your wallet. The pool emits
Swap; the indexer records the trade.
Selling
- First sale of a token: approve the router for the token (unlimited allowance, one transaction).
- Enter a token amount; the quoter returns the WETH output.
- You sign one
multicall:exactInputSinglewith recipientaddress(2)(leave output in the router), thenunwrapWETH9(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,TForST, wait a moment or buy less.
Creator fees
You earn from two places.
| Stream | Rate | Paid in | When |
|---|---|---|---|
| Your trading fee | 0–5%, chosen at launch | ETH | Instantly, on every trade routed through countdown.family |
| Pool fee share | 70% of Uniswap’s 1% | WETH + your token | When 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.
| Fee | Rate | Set by | Goes to | Charged by |
|---|---|---|---|---|
| Creator fee | 0–5% (default 2%) | Token deployer, once | Creator wallet, in ETH, instantly | Trade router |
| Countdown Family fee | 1% (cap 2%) | Protocol owner | Treasury, in ETH, instantly | Trade router |
| Pool fee | 1% (fixed) | Uniswap V3 tier | Locked position → 70/30 on claim | Uniswap pool |
The rules
- The creator fee is set once and can never be raised.
setTokenFeerevertsFeeAlreadySeton 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. - Only the token’s deployer can set it. The router reads
deployer()from the token contract; anyone else getsNotTokenDeployer. - It is capped at 5%.
MAX_CREATOR_FEE_BPS = 500is 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. - 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. - 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.
- The slippage floor is after fees.
amountOutMinimumon a sell is what the seller receives net; the app quotes the net figure and shows it. - 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
pendingand claims it later withclaimPending(); the trade goes through either way. - 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.
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
| Contract | Address |
|---|---|
| CountdownLaunchFactory | 0x1b79E530C86a3b3931f7240182359cac0E7B7af1 |
| CountdownLaunchLocker | Read locker() on the factory |
| CountdownTokenDeployer | Read tokenDeployer() on the factory |
| CountdownTradeRouter | 0xe6A96d79BB6B7F22460970AFe42636e275ced419 |
Shared infrastructure (Uniswap V3 on Robinhood Chain)
| Contract | Address |
|---|---|
| UniswapV3Factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA |
| NonfungiblePositionManager | 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3 |
| SwapRouter02 | 0xCaf681a66D020601342297493863E78C959E5cb2 |
| QuoterV2 | 0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7 |
| WETH9 | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 |
pons factories (indexed, not operated by us)
| Factory | Address | Creator share |
|---|---|---|
| pons active | 0xA5aAb3F0c6EeadF30Ef1D3Eb997108E976351feB | 70% |
| pons legacy | 0x0c37a24F5D23A486FA692d1500881d698B1F77a4 | 90% |
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
| Function | Access | Description |
|---|---|---|
launchToken(TokenParams, uint256 launchConfigId, uint256 dexId, bytes32 salt) payable → address | Anyone (when launchEnabled) or allowlisted | Checks 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) → uint256 | Owner | Register a V3 factory/positionManager/router/fee/tickSpacing set. |
setDexStatus(uint256, bool) | Owner | Enable or disable a DEX config. |
addLaunchConfig(LaunchConfig) → uint256 | Owner | Register a parameter set. Validates maxTxBps = 110% of maxWalletBps, supply ≥ 1e18, tick non-zero and in range. |
updateLaunchConfig(uint256, LaunchConfig) | Owner | Replace a parameter set. Tokens already launched keep their immutables. |
setLaunchFee(uint256) | Owner | Change the ETH launch fee. |
setLaunchEnabled(bool) | Owner | Open/close public launching. |
setWhitelistedLauncher(address, bool) | Owner | Allowlist while closed. |
Read
| Function | Returns |
|---|---|
getLaunchedToken(address) → LaunchedToken | token, 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) → address | CREATE2 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
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
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
| Error | When |
|---|---|
| NotWhitelisted | launchEnabled is false and caller is not allowlisted. |
| LaunchFeeNotPaid | msg.value < launchFee. |
| InvalidDexId / DexDisabled | dexId out of range or disabled. |
| InvalidLaunchConfigId / LaunchConfigDisabled | launchConfigId out of range, disabled, or an invalid tick on add. |
| InvalidTokenParams | Empty name or symbol. |
| PoolAlreadyExists | A V3 pool already exists for the predicted token address at this fee tier. Change the salt. |
| InvalidInitialTick | The launch config’s initialTick is not a multiple of the DEX config’s tickSpacing. Checked before the fee is paid. |
| FeeTransferFailed | The protocol fee recipient rejected the launch fee. |
| TokenDeploymentFailed | The token deployer returned zero or an address other than the prediction (salt reuse with identical params). |
| RouterNotSet | Declared for ABI parity; unreachable since a DEX config without a router is rejected on add. |
| InvalidBasisPoints / InvalidMaxTxBasisPoints / SupplyTooLow / InvalidDexConfig / ZeroAddress | Config validation. InvalidDexConfig includes a zero swapRouter. |
| TokenNotFound | graduationStatus for a token this factory did not launch. |
Reference: CountdownLaunchLocker
contracts/launch/CountdownLaunchLocker.sol · MIT · Ownable2Step, ReentrancyGuard, IERC721Receiver · MAX_PROTOCOL_FEE_SHARE = 50
| Function | Access | Description |
|---|---|---|
initialize(address factory) | Owner, once | Binds the locker to its factory. |
onERC721Received(operator, from, id, data) | Position manager callback | Accepts only when operator and from are both the factory. |
lockPosition(address token) | Factory | Verifies ownerOf(positionId) == locker, marks locked, snapshots protocolFeeShare, indexes deployer. |
collectFees(address token) → (amount0, amount1) | Owner, deployer, redirect recipient, or feeCollectors | Collects from the position, splits, transfers. Reverts NoFeesToCollect on zero. |
setFeeRedirect(address token, address wallet) | Deployer or factory | Sets the creator-share recipient; maintains the reverse index. |
setFeeCollector(address, bool) | Owner | Allow an operator to trigger collection. |
setProtocolFeeRecipient(address) | Owner | Where the protocol share (and launch fees) go. |
setProtocolFeeShare(uint256 ≤ 50) | Owner | Share copied by future launches. |
deployerTokens / deployerTokenCount / feeRecipientTokens / feeRecipientTokenCount | View | Profile indexes. |
tokenProtocolFeeShares(address) / feeRedirects(address) / getLaunchedToken(address) | View | Per-token state. |
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);collect. This is verifiable by reading the 280-line source on Blockscout.Reference: CountdownLauncherToken
contracts/launch/CountdownLauncherToken.sol · MIT · OpenZeppelin ERC20 · no owner, no mint, no pause.
| Function | Description |
|---|---|
liquidityPool() → address | The 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() → uint256 | The height the window is measured in: ArbSys.arbBlockNumber() on Robinhood Chain, block.number where the precompile is absent. |
restrictionsActive() → bool | True while pool and router buys are still capped. |
deployer, launchFactory, dexFactory, positionManager, pairToken, swapRouter, poolFee, launchBlock, restrictionBlocks, restrictionEndBlock, maxWalletBps, maxTxBps | Immutables. launchBlock and restrictionEndBlock are L2 block heights. |
logo, description | Public strings. |
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.
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
| Function | Access | Description |
|---|---|---|
buy(address token, uint256 amountOutMinimum) payable → amountOut | Anyone | Takes fees off msg.value, swaps the rest via SwapRouter02, tokens go to the caller. |
sell(address token, uint256 amountIn, uint256 amountOutMinimum) → ethOut | Anyone (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, once | 0–500 bps. Zero wallet = caller. |
setCreatorWallet(address token, address wallet) | Current wallet or deployer | Moves the recipient; the bps never change. |
setProtocolFeeBps(uint16) / setProtocolFeeRecipient(address) | Owner | 0–200 bps; where Countdown Family’s leg goes. |
claimPending() | Anyone with a balance | Withdraws fees that could not be pushed. |
rescueERC20(address token, address to) | Owner | Returns 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) | View | What the UI displays. |
tokenFees(address) / pending(address) / protocolFeeBps() / protocolFeeRecipient() | View | State. |
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);| Error | When |
|---|---|
| UnexpectedEther | Plain ETH sent to the router by anything other than WETH9.withdraw. Refused so nothing can be stranded. |
| NotTokenDeployer | setTokenFee from anyone but token.deployer(). |
| FeeAlreadySet | setTokenFee called a second time. |
| FeeTooHigh | Creator bps > 500 or protocol bps > 200. |
| NotCreatorWallet | setCreatorWallet from anyone but the current wallet or deployer. |
| InsufficientOutput | Sell output after fees below amountOutMinimum, or ETH transfer to seller failed. |
| ZeroAmount / ZeroAddress / NothingPending | Input 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.
| Function | Access | Description |
|---|---|---|
deploy(bytes32 salt, bytes constructorArgs) → address | Anyone | CREATE2 with the salt namespaced as keccak256(msg.sender, salt). Reverts TokenDeploymentFailed on a zero address. |
predict(address caller, bytes32 salt, bytes constructorArgs) → address | View | The address deploy would return for that caller. |
creationCodeHash() → bytes32 | View | keccak256 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
{
"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.
{ "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.
{
"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.
{
"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,pendingwhile 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 ofwallet’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=holdersrows now carrytags(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 throughPOSTkindswatch,alert-rule,followandtelegram-link, each an EIP-191 signature over acountdown.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.
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
- Reads the cursor (
indexer_state, keypons:4663) and the head block. eth_getLogsin spans ofPONS_SCAN_SPANblocks for launch topics on every factory inPONS_FACTORIES.- For each new token: reads name/symbol/decimals/logo/description/socials from the token,
liquidityPool(),getLaunchedTokenfrom whichever factory knows it, and inserts amarketsrow with block/blockHash/hash. - Refreshes every market’s snapshot:
slot0price,graduationStatus, pool balances for liquidity, 24h trade count. One bad market does not kill the pass. - Backfills
Swaplogs on each pool intoprice_points(sourceswap) and writes asamplepoint from the current price. - Bumps
data_versionscounters sokind=updateschanges.
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.
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
| Variable | Default | Purpose |
|---|---|---|
| DATABASE_URL / DATABASE_AUTH_TOKEN | local.db | Hosted libSQL/Turso. Required on any ephemeral filesystem. |
| NEXT_PUBLIC_ETH_PRICE_USD | 3000 | Fallback ETH/USD if every live source fails. |
| NEXT_PUBLIC_ROBINHOOD_CHAIN_MAINNET_RPC | public RPC | Browser RPC for reads the wallet needs (balances, simulations). |
| ROBINHOOD_CHAIN_MAINNET_RPC | public RPC | Server/indexer/Hardhat RPC. Use a dedicated provider. |
| NEXT_PUBLIC_LAUNCH_FACTORY | pons active factory | Our factory once deployed. Unlocks launching. |
| NEXT_PUBLIC_TRADE_ROUTER | — | Our trade router. When set, trades pay creator + Countdown Family fees in ETH. |
| NEXT_PUBLIC_APP_URL | https://countdown.family | Public origin: canonical URLs, share cards, robots and sitemap. |
| TRUSTED_PROXY_HOPS | 0 | How many proxies sit in front of the app; rate limits read the client IP from X-Forwarded-For only when > 0. |
| PROTOCOL_TRADE_FEE_BPS | 100 | Countdown Family’s trade fee at deploy, ≤ 200. |
| PONS_FACTORIES | legacy,active | Comma-separated factories to index, oldest first. |
| PONS_START_BLOCK | 0 | First block of the initial sweep. |
| PONS_SCAN_SPAN | 4000000 | Blocks per eth_getLogs. |
| PONS_POLL_MS | 20000 | Gap between passes. |
| PONS_RPC_MIN_INTERVAL_MS | 250 | Min gap between RPC calls. |
| PONS_RPC_MAX_ATTEMPTS | 8 | Retries per call. |
| PONS_RPC_MAX_BACKOFF_MS | 30000 | Backoff ceiling. |
| PONS_STALE_MS | 300000 | Snapshot age that reports stale. |
| PRIVATE_KEY | — | Hardhat deployer. |
| TREASURY_ADDRESS | deployer | Protocol fee recipient at deploy. |
| PROTOCOL_FEE_SHARE | 30 | Percent to protocol, ≤ 50. |
| LAUNCH_FEE_ETH | 0.0005 | Launch fee at deploy. |
| BLOCKSCOUT_API_KEY | — | Optional; verification works without it. |
Deploying the factory
docs/DEPLOY-MAINNET.md in the repository.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
- Deploys
CountdownTokenDeployer()— no arguments, no owner. - Deploys
CountdownLaunchLocker(owner, treasury, share). - Deploys
CountdownLaunchFactory(owner, locker, tokenDeployer, launchFee). - Deploys
CountdownTradeRouter(owner, SwapRouter02, WETH, 10000, treasury, protocolTradeFeeBps). locker.initialize(factory)— without this every launch reverts.factory.addDexConfigfor Uniswap V3 at the 1% tier, tickSpacing 200.factory.addLaunchConfig: WETH pair, 1e9 supply, tick −204200, 4.2 WETH graduation, 5%/5.5% caps, 300 L2 blocks.- Reads back
launchEnabled,locker.factory()andfactory.tokenDeployer(), verifies all four on Blockscout, and prints the env lines to paste into.env.local.
After deploy
- Set
NEXT_PUBLIC_LAUNCH_FACTORYandNEXT_PUBLIC_TRADE_ROUTER, and append the factory toPONS_FACTORIES. - Restart the indexer before anyone launches.
- Verify on Blockscout (no API key needed).
- Do the first launch and first swap with trivial amounts and reconcile the indexed rows against pool state.
- Transfer ownership of the locker, factory and router to a multisig with
transferOwnership+acceptOwnership. The token deployer has no owner.
Local development
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
| Command | What it does |
|---|---|
| npm run dev / build / start | Next.js. |
| npm run typecheck | App TypeScript. |
| npm run db:push / db:generate / db:studio | Drizzle schema, migrations, browser. |
| npm run pons:index | The indexer. |
| npm run contracts:compile | Hardhat compile (viaIR, shanghai). |
| npm run contracts:test | Contract tests. FORK=1 for the mainnet-fork suite. |
| npm run contracts:typecheck | Hardhat TypeScript (separate tsconfig for typechain). |
| npm run contracts:deploy | Deploy the token deployer, locker, factory and trade router to mainnet (contracts:deploy:dry-run simulates). |
| npm run rpc:proxy | Local JSON-RPC forwarder for when the public RPC hostname is DNS-filtered. |
| npm run brand:icons | Renders every raster icon from public/brand/*.svg. |
| npm test | Node tests for the price and event maths in lib/pons-math.ts. |
Repository map
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_versionsTesting
Four contract suites, 60 cases, last run in full on 20 September 2026.
| Suite | Runs on | Cases | Covers |
|---|---|---|---|
CountdownLauncherToken.unit.test.ts | plain hardhat, mock pool and router | 7 | The 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.ts | plain hardhat | 22 | Construction 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.ts | plain hardhat, mocks | 12 | Fee 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.ts | fork of chain 4663, real Uniswap V3 | 19 | A 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.numberon Robinhood Chain is Ethereum’s height (measured, not assumed); the token now readsArbSys.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.
initialTickis 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
| Term | Meaning |
|---|---|
| Tick | Uniswap V3’s log-price unit: price = 1.0001^tick. |
| sqrtPriceX96 | Square root of price as a Q64.96 fixed-point number, from slot0. |
| One-sided position | A V3 position holding only one asset because its range is entirely above (or below) the current price. |
| Position NFT | The ERC-721 the position manager mints to represent a liquidity position. |
| Locker | The contract that holds position NFTs permanently and splits collected fees. |
| Paired principal | The WETH inside the locked position at the current price; drives graduation. |
| Restriction window | launchBlock through launchBlock + 300, in L2 blocks, when pool and router buys are capped. |
| L2 block | A 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 deployer | CountdownTokenDeployer: holds the token bytecode and runs CREATE2 for the factory, with salts namespaced per caller. |
| Opening buy | The creator’s purchase executed inside launchToken with msg.value − launchFee. |
| Fee redirect | A per-token override of who receives the creator fee share. |
| Trade router | CountdownTradeRouter: the app’s swap entry point that charges creator + Countdown Family fees in ETH. |
| Creator fee | The 0–5% a deployer sets once on their token; paid in ETH per routed trade. |
| Snapshot | A per-market row of live figures refreshed by the indexer; stale after 5 minutes. |
| swap / sample | The two sources of a price point: a Swap log or a slot0 read. |
| pons | A third-party launchpad on Robinhood Chain whose contracts this protocol adapts and whose markets it indexes. |