Overview
LIQUOR.TRADE 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.NEXT_PUBLIC_LAUNCH_FACTORY is set, the launch button targets pons’s factory, which has public launching turned off, and the app says so instead of letting anyone pay for a revert. Trading works today on every market the indexer lists, because trades go through Uniswap and do not depend on which factory minted the token. Check 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.
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 curve 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 Liquor’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 trading during the launch block for anyone but the creator, and caps for two blocks after. If you get
LaunchBlockBuyBlockedorMaxTxExceeded, wait a block.
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 liquor.trade |
| 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 (LIQUOR.TRADE’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 liquor.trade pays three fees. Two are charged by LiquorTradeRouter 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 |
| Liquor 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. - Liquor’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 Liquor’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 Liquor, 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 Liquor, 0.9603 ETH to the seller.
Contract addresses
All addresses are on Robinhood Chain mainnet (4663). Click to open in Blockscout.
LIQUOR.TRADE
| Contract | Address |
|---|---|
| LiquorLaunchFactory | Not yet deployed — read launchpad.contractAddress from /api/config |
| LiquorLaunchLocker | Not yet deployed — read locker() on the factory |
| LiquorTradeRouter | Not yet deployed — NEXT_PUBLIC_TRADE_ROUTER once it is |
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: LiquorLaunchFactory
contracts/launch/LiquorLaunchFactory.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 | Deploys, pools, locks, records, 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. |
getDexConfig(uint256) / dexConfigCount() | DEX configs. |
getLaunchConfig(uint256) / launchConfigCount() | Launch configs. |
launchFee() / launchEnabled() / whitelistedLaunchers(address) / locker() | 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. |
| FeeTransferFailed | The protocol fee recipient rejected the launch fee. |
| TokenDeploymentFailed | CREATE2 returned zero (salt reuse with identical params). |
| RouterNotSet | An opening buy was requested but the DEX config has no router. |
| InvalidBasisPoints / InvalidMaxTxBasisPoints / SupplyTooLow / InvalidDexConfig / ZeroAddress | Config validation. |
| TokenNotFound | graduationStatus for a token this factory did not launch. |
Reference: LiquorLaunchLocker
contracts/launch/LiquorLaunchLocker.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: LiquorLauncherToken
contracts/launch/LiquorLauncherToken.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. |
deployer, launchFactory, dexFactory, positionManager, pairToken, poolFee, launchBlock, restrictionBlocks, restrictionEndBlock, maxWalletBps, maxTxBps | Immutables. |
logo, description | Public strings. |
if (from != 0 && to != 0 && block.number <= restrictionEndBlock && isPairPool(from)) {
bool atomic = block.number == launchBlock && to == _initialBuyRecipient && _initialBuyRecipient != 0;
if (!atomic && block.number == launchBlock) revert LaunchBlockBuyBlocked(to);
if (!atomic) {
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.
Reference: LiquorTradeRouter
contracts/launch/LiquorTradeRouter.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 Liquor’s leg goes. |
claimPending() | Anyone with a balance | Withdraws fees that could not be pushed. |
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);| Error | When |
|---|---|
| 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. |
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. Three monotonic counters and nothing else. Send If-None-Match with the previous ETag and receive 304 when nothing moved.
{ "launches": "118:12", "prices": 902, "markets": 340 }
// 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.
{
"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…" } ]
}
}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.
Retired routes
/api/launch, /api/swap, /api/tokens and /api/token/… belong to the retired database-settled bonding-curve mode. They still compile but are not on any UI code path and will be removed once the V3 path has traded on mainnet. Do not build on them.
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_CHAIN_ENV | mainnet | Chain for the general UI. Writes pin 4663 regardless. |
| 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 + Liquor fees in ETH. |
| PROTOCOL_TRADE_FEE_BPS | 100 | Liquor’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
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 FORK=1 npm run contracts:test # 10 tests against a mainnet fork npm run contracts:deploy:launchpad
What the script does
- Deploys
LiquorLaunchLocker(owner, treasury, share). - Deploys
LiquorLaunchFactory(owner, locker, launchFee). - Deploys
LiquorTradeRouter(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, 2 blocks.- Prints the env lines to paste into
.env.localand the block to start indexing from.
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 both contracts to a multisig with
transferOwnership+acceptOwnership.
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:launchpad | Deploy factory + locker to mainnet. |
Repository map
contracts/launch/ LiquorLaunchFactory, LiquorLaunchLocker, LiquorLauncherToken, interfaces, libraries contracts/ retired bonding-curve contracts (history; not on any code path) 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-markets.ts React Query hooks over /api/data lib/eth-price.ts cached multi-source ETH/USD lib/rate-limit.ts sliding-window limiter (per-instance) app/api/data, config the public API scripts/pons-indexer.ts worker entry scripts/deploy-launchpad.ts test/LiquorLaunchFactory.fork.test.ts db/schema.ts markets, price_points, data_versions (+ retired tables)
Testing
test/LiquorLaunchFactory.fork.test.ts runs against a fork of chain 4663 and the real Uniswap V3 deployment. Set FORK=1 and a mainnet RPC. The ten cases:
- opens launches to everyone, unlike the factory it was adapted from
- lets a wallet with no allowlist entry launch a token
- mints the whole supply into a live pool and keeps only rounding dust
- spends everything above the launch fee on the creator’s opening buy
- exempts the creator opening buy from the anti-snipe caps
- holds a later buyer to the max-transaction cap inside the window
- pays the launch fee to the protocol fee recipient
- locks the position in the locker and reports graduation progress
- snapshots the fee split per token so a later change cannot rewrite it
- emits a TokenLaunched signature the existing indexer already matches
test/LiquorTradeRouter.test.ts runs offline against mock WETH / SwapRouter02 / token on every npm run contracts:test: ten cases covering fee arithmetic on both legs of both directions, deployer-only / once-only / ≤ 5% creator fees, the ≤ 2% owner-only protocol fee, the after-fee slippage floor, deferred fees for a recipient that rejects ETH, and zero-amount rejection.
test/bonding-curve.test.ts and test/LaunchpadFactory.test.ts cover the retired design and remain only until it is deleted. The client hooks have no automated tests yet.
Security & audits
- Source is published under MIT / GPL-2.0-or-later and will be verified on Blockscout at deploy.
- Ownable2Step on both admin contracts; ownership intended to move to a multisig after deploy.
- ReentrancyGuard on the two 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.
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 LIQUOR.TRADE 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 LIQUOR.TRADE 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 two-block launch window, buys can also revert with MaxTxExceeded or LaunchBlockBuyBlocked; wait a block.
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 Liquor 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 LIQUOR.TRADE.
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 + 2, when pool buys are capped. |
| 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 | LiquorTradeRouter: the app’s swap entry point that charges creator + Liquor 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. |