Documentation

LIQUOR.TRADE 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.

Updated September 2026WhitepaperBlog
Contents

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

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.
Current deployment status
The LIQUOR.TRADE factory has not yet been deployed to mainnet. Until 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/configfeatureFlags.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:

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.

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 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

  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.

Choosing an opening buy
The opening buy is uncapped and public — it is emitted as 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

  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 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 LaunchBlockBuyBlocked or MaxTxExceeded, wait a block.

Creator fees

You earn from two places.

StreamRatePaid inWhen
Your trading fee0–5%, chosen at launchETHInstantly, on every trade routed through liquor.trade
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 (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.

FeeRateSet byGoes toCharged by
Creator fee0–5% (default 2%)Token deployer, onceCreator wallet, in ETH, instantlyTrade router
Liquor 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. 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.
  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 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.

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 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

ContractAddress
LiquorLaunchFactoryNot yet deployed — read launchpad.contractAddress from /api/config
LiquorLaunchLockerNot yet deployed — read locker() on the factory
LiquorTradeRouterNot yet deployed — NEXT_PUBLIC_TRADE_ROUTER once it is

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: LiquorLaunchFactory

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

Write

FunctionAccessDescription
launchToken(TokenParams, uint256 launchConfigId, uint256 dexId, bytes32 salt) payable → addressAnyone (when launchEnabled) or allowlistedDeploys, pools, locks, records, 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.
getDexConfig(uint256) / dexConfigCount()DEX configs.
getLaunchConfig(uint256) / launchConfigCount()Launch configs.
launchFee() / launchEnabled() / whitelistedLaunchers(address) / locker()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.
FeeTransferFailedThe protocol fee recipient rejected the launch fee.
TokenDeploymentFailedCREATE2 returned zero (salt reuse with identical params).
RouterNotSetAn opening buy was requested but the DEX config has no router.
InvalidBasisPoints / InvalidMaxTxBasisPoints / SupplyTooLow / InvalidDexConfig / ZeroAddressConfig validation.
TokenNotFoundgraduationStatus for a token this factory did not launch.

Reference: LiquorLaunchLocker

contracts/launch/LiquorLaunchLocker.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);
There is no way out
The locker has no function that transfers, burns, or decreases a position. The only position-manager call after locking is 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.

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.
deployer, launchFactory, dexFactory, positionManager, pairToken, poolFee, launchBlock, restrictionBlocks, restrictionEndBlock, maxWalletBps, maxTxBpsImmutables.
logo, descriptionPublic strings.
Transfer hook (simplified)
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

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 Liquor’s leg goes.
claimPending()Anyone with a balanceWithdraws fees that could not be pushed.
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);
ErrorWhen
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.

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. Three monotonic counters and nothing else. Send If-None-Match with the previous ETag and receive 304 when nothing moved.

Response
{ "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.

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…" } ]
  }
}

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.

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_CHAIN_ENVmainnetChain for the general UI. Writes pin 4663 regardless.
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 + Liquor fees in ETH.
PROTOCOL_TRADE_FEE_BPS100Liquor’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

Mainnet only, real money
Uniswap V3 does not exist on Robinhood Chain testnet. The deploy script refuses any chain but 4663 and checks that every dependency address holds code. There is no rehearsal; the first launch after deploy is a real transaction.
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
FORK=1 npm run contracts:test        # 10 tests against a mainnet fork
npm run contracts:deploy:launchpad

What the script does

  1. Deploys LiquorLaunchLocker(owner, treasury, share).
  2. Deploys LiquorLaunchFactory(owner, locker, launchFee).
  3. Deploys LiquorTradeRouter(owner, SwapRouter02, WETH, 10000, treasury, protocolTradeFeeBps).
  4. locker.initialize(factory) — without this every launch reverts.
  5. factory.addDexConfig for Uniswap V3 at the 1% tier, tickSpacing 200.
  6. factory.addLaunchConfig: WETH pair, 1e9 supply, tick −204200, 4.2 WETH graduation, 5%/5.5% caps, 2 blocks.
  7. Prints the env lines to paste into .env.local and the block to start indexing from.

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 both contracts to a multisig with transferOwnership + acceptOwnership.

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:deploy:launchpadDeploy factory + locker to mainnet.

Repository map

Tree
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:

  1. opens launches to everyone, unlike the factory it was adapted from
  2. lets a wallet with no allowlist entry launch a token
  3. mints the whole supply into a live pool and keeps only rounding dust
  4. spends everything above the launch fee on the creator’s opening buy
  5. exempts the creator opening buy from the anti-snipe caps
  6. holds a later buyer to the max-transaction cap inside the window
  7. pays the launch fee to the protocol fee recipient
  8. locks the position in the locker and reports graduation progress
  9. snapshots the fee split per token so a later change cannot rewrite it
  10. 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

No independent audit has been performed
The contracts are adapted from verified pons source with two intentional changes. They have been fork-tested but not audited. Do not read anything on this site as a claim otherwise.
  • 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

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 + 2, when pool buys are capped.
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 routerLiquorTradeRouter: the app’s swap entry point that charges creator + Liquor 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.