par docs
The first part is for people launching or buying tokens. The second part, from Contracts on, is for anyone writing code against the protocol.
Overview
par launches tokens on Robinhood Chain straight into Uniswap v4. One transaction deploys the token, creates the pool, puts the entire supply into it and locks that liquidity for good. There is no bonding curve phase and no migration: the pool is a plain v4 pool with no hook from its first block, so anything that trades v4 on this chain, wallets, aggregators, terminals, trades it immediately with no par specific code.
The quote side of the pool can be ETH, USDG or any token on the chain that has a real market, including tokens launched on other launchpads. Whatever the quote, every token opens at the same market cap: 1.3557 ETH, converted into the quote asset at launch.
Trading fees are the pool's own LP fee, set at launch and never changed. The base fee is 1% and is split in half between the protocol and the creator. The creator can add a tax of up to 10% on top and keeps all of it. Fees accrue to the locked position; a collection pays the protocol's part out directly and puts the creator's part in an escrow the creator withdraws from.
What a launch does
The launch transaction does the following, in order, and either all of it happens or none of it does.
The launch fee is 0.0005 ETH, paid to the protocol. Gas for the whole thing is usually a few cents on Robinhood Chain.
There is no snipe tax and no trading delay. The pool is open to everyone from the block it is created in, which is also why the dev buy is the only buy that is guaranteed to be first.
Price and market cap
The position covers a price range from the opening price to the maximum tick, so it behaves like a constant product pool whose quote side starts with a virtual balance of 1.3557 ETH (or the equivalent in the quote asset). Call that P. With S the supply and q the quote buyers have paid into the pool so far, the tokens left in the pool and the market cap are:
tokens in pool t = P * S / (P + q)
market cap = (P + q)^2 / P
q = 0 -> market cap 1.3557 ETH (opening)
q = 1 ETH -> market cap 4.10 ETH, 42.4% of supply sold
q = 5 ETH -> market cap 29.8 ETH, 78.7% of supply sold
q = 20 ETH -> market cap 336 ETH, 93.7% of supply soldNothing about this is special to par. It is the standard shape of a one-sided Uniswap v3 or v4 position, which is how most launchpads open a token. The numbers above are before fees; a buy pays the LP fee on the quote it puts in and the remainder moves the price, so q is the quote net of fees.
For a pool quoted in something other than ETH, P is 1.3557 ETH converted at the quote asset's reference pool price at the moment of launch. The interface shows the starting market cap in dollars; it is the same figure for every quote asset.
Quote assets
Three kinds of quote are accepted.
The pricer finds short routes by itself: the deepest pool pairing the asset with ETH or WETH (v3 at 0.05%, 0.3% or 1%, or v4), or two hops through USDG. That covers the stock tokens, the stablecoins and anything launched straight against ETH.
Longer routes are registered. A token launched against SPCX, which itself trades against USDG, is three pools from ETH: the launch form searches the chain's Uniswap pools for such a route, checks it with evaluatePath and offers registerPath(token, hops), one transaction anyone can send. Routes are up to four hops, may mix v3 and v4 pools in any order, must end in ETH or WETH and may not pass through ETH earlier. A registered route can only be replaced by a stronger one while it still qualifies; once it has fallen under the floor anyone can replace it. When both a registered and an automatic route qualify the stronger one is used. route(token) returns the hops in use and whether they qualify, describe(token) the same with the depth and floor of every hop and, when it fails, which hop failed and why.
v4 pool keys come from two places. Anyone can register an initialized v4 pool with registerV4Pool as long as its hook is on the pricer's allow list (no hook, or the PONS hook). And tokens launched by PONS or by par need no registration at all: the pricer asks the launchpad's factory for the pool terms and builds the key itself. Pick a PONS token as the quote and it is priced the moment its pool has 5 ETH in range.
There is no averaging. The trade-off is stated plainly: whoever launches can move the reference pool inside their own transaction and move it back afterwards, paying the reference pool's swap fees, and thereby open their own token at a different market cap than the one advertised. That only touches that one launch, the interface shows the live market cap regardless, and the 5 ETH floor sets a minimum size for the trade it takes. We considered a time weighted price and rejected it: it meant a half hour wait before a token could be used as a quote and a keeper that had to be awake for it.
The interface marks quotes it does not curate as "wild". That is a label for the trader, not a restriction: it means par has no opinion about the asset beyond the fact that it could be priced when the launch happened. A token quoted in a wild asset goes to zero if that asset does.
Fee-on-transfer tokens and tokens whose decimals() changes cannot be used as quotes. Fee collection moves exact amounts and a transfer that arrives short reverts it.
Multi-market launches
A token does not have to pick one quote asset. A multi-market launch opens up to five Uniswap v4 pools for the same token in one transaction, one per quote asset the creator chose: ETH, USDG, $par, a stock token, or any mix of assets the pricer can value. The supply is split equally between the pools and each opens at the same price with its slice of the virtual reserve, so the token has one opening market cap, the same as a single-market launch would have, spread over several books.
Everything else is as before. Every position is locked for good, every pool is a plain v4 pool with no hook and the same LP fee, and fees are split creator and protocol the same way. Because the pools share a token, arbitrage keeps their prices together: a buy in the AAPL pool that lifts the price there is met by sellers in the other four, so the trade lands on the depth of all of them. In return the token can be bought and sold directly with any of its quote assets, and a holder of any of them has a market without a swap in between.
The interface shows one price, one market cap and one volume for the token. The price is the markets' last prices weighted by how much of the supply each pool still holds, and the volume is the ETH value of the trades in all of them. Each market is listed on the token page with its own price, volume and share of the supply, and the trade panel buys and sells across all of them at once, with ETHsplit by depth and routed through each quote asset. Trading one market on its own is what any Uniswap v4 interface does with the pool key.
The first multi-market token is INDEX, quoted in the Apple, Microsoft, NVIDIA, Alphabet and Tesla stock tokens on Robinhood Chain. Multi-market launching is open to every wallet; the factory can be closed to a whitelist by its owner, and canLaunch(address) says where a wallet stands.
For code
The multi-market stack is a second factory, locker and router next to the first; they share the fee escrow, the pricer and the token contract. A launched token belongs to exactly one of the two factories. The events have the same names but different shapes, so an indexer keys them by contract address and topic. Addresses are under Contracts.
// Launch on 1..5 distinct quote assets; address(0) is native ETH, WETH is not allowed next to it.
// nativeIsReference() tells which: true on this chain.
// TokenParams is the same struct as the single-market factory's. Value must equal launchFee().
function launchToken(TokenParams params, uint256 launchConfigId, address[] pairTokens) payable returns (address token);
function previewLaunchEconomics(uint256 launchConfigId, address[] pairTokens) view returns (bytes32); // for expectedEconomics
function previewQuoteEconomics(uint256 launchConfigId, address[] pairTokens) view returns (uint256[] phantomQuotes);
struct Market { address pairToken; uint256 phantomQuote; int24 tickLower; int24 tickUpper; uint128 liquidity; uint256 positionId; }
function getLaunchedToken(address token) view returns (LaunchedToken); // deployer, fee terms, poolFee, tickSpacing
function getMarkets(address token) view returns (Market[]);
function poolKeyFor(address token, uint256 index) view returns (PoolKey);
function poolKeysFor(address token) view returns (PoolKey[]);
function canLaunch(address launcher) view returns (bool);// A leg is one market's share of the trade plus the route between ETH and that market's quote asset:
// the pricer's route(quote), reversed for a buy, empty when the quote is ETH. The launch pool itself
// is appended by the router.
struct Leg { uint8 market; Hop[] hops; uint256 amountIn; }
// Native ETH in, tokens from every leg delivered together; the legs' amounts add up to msg.value, the floor is on the total.
function buyWithEth(address token, Leg[] legs, uint256 minTokensOut, address recipient) payable returns (uint256 tokensOut);
// Tokens in (approve the sum), native ETH out. amountIn of each leg is that market's slice of tokens.
function sellToEth(address token, Leg[] legs, uint256 minEthOut, address recipient) returns (uint256 ethOut);
// The same where the reference is an ERC-20 (USDC on Arc): approve the router for amountIn, which is the sum of the legs.
function buyWithReference(address token, Leg[] legs, uint256 amountIn, uint256 minTokensOut, address recipient) returns (uint256 tokensOut);
function sellToReference(address token, Leg[] legs, uint256 minOut, address recipient) returns (uint256 amountOut);
// Tokens in, each market's quote asset out as it is, one floor per leg. Legs carry no hops here.
function sellToQuotes(address token, Leg[] legs, uint256[] minOuts, address recipient);
// One market, paid in its quote asset (approve the router first; send value for a native ETH market).
function buyWithQuote(address token, uint8 market, uint256 quoteIn, uint256 minTokensOut, address recipient)
payable returns (uint256 tokensOut);
// Launch and land the opening buy across the new markets in one transaction; value = launchFee + sum of legs.
function launchAndBuyWithEth(TokenParams params, uint256 launchConfigId, address[] pairTokens, Leg[] legs, uint256 minTokensOut)
payable returns (address token, uint256 tokensOut);
// ERC-20 reference: the buy is amountIn from the allowance, value must equal launchFee() exactly.
function launchAndBuyWithReference(TokenParams params, uint256 launchConfigId, address[] pairTokens, Leg[] legs, uint256 amountIn, uint256 minTokensOut)
payable returns (address token, uint256 tokensOut);Nothing requires the router. Each market is an ordinary v4 pool with the key from poolKeyFor, so the Universal Router, the v4 Quoter and any terminal that trades v4 trade it as they would any other pool; the router only saves the round trips when a trade should touch several markets at once.
function lockedPositions(address token) view returns (uint256[] positionIds); // one per market
function pendingFees(address token, uint256 index) view returns (uint256 amount0, uint256 amount1);
function pendingFeesAll(address token) view returns (uint256[] amount0, uint256[] amount1);
function collectFees(address token) returns (uint256[] collected); // every market
function collectMarketFees(address token, uint256 index) returns (uint256 amount0, uint256 amount1);// Multi factory. One TokenLaunched per launch, preceded in the same transaction by one MarketOpened per pool, in market order.
event TokenLaunched(address indexed token, address indexed deployer, uint256 launchConfigId, uint24 poolFee, address[] pairTokens);
event MarketOpened(address indexed token, bytes32 indexed poolId, uint256 marketIndex, address pairToken, uint256 positionId,
int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 tokenAmount, uint256 phantomQuote);
// Multi locker, one per market that had something to collect.
event FeesCollected(address indexed token, uint256 indexed marketIndex, address currency0, address currency1,
uint256 protocolAmount0, uint256 protocolAmount1, uint256 creatorAmount0, uint256 creatorAmount1);Fees
A par pool charges the ordinary Uniswap v4 LP fee, fixed when the pool is created. Like every Uniswap fee it is taken from whatever goes into the swap: a buy pays it in the quote asset, a sell pays it in the token. The fee lands in the liquidity position, and since the locked launch position is the only liquidity in the pool at launch, it earns all of it.
Fees sit in the position until someone calls collectFees on the locker. Anyone can; the keeper does it every fifteen minutes and the creator's claim on the token page does it as its first step. A collection pulls both currencies out of the position and splits each by the terms frozen at launch. The protocol's part of the quote is sent to the protocol wallet right there (if that transfer fails, it is credited to the escrow instead). The protocol's part of the token is burned: the locker calls burn on the token, the supply shrinks by that amount and ProtocolShareBurned is emitted. par never holds or sells a token somebody launched on it. The creator's part of both is credited to the fee escrow, where the creator has a balance to withdraw at will. The token page shows what the creator has earned and claimed, for the quote and for the token separately, and the running total burned.
Because the fee is a property of the pool, other people can add liquidity to the pool and earn a share of it proportional to their liquidity in range. The creator's and protocol's share is then the locked position's share. In practice the locked position is by far the largest, but the fee is not exclusively ours the way a hook fee would be.
Where the protocol's share goes
For every launch created since the PairPadFeeSplitter became the protocol fee recipient, the protocol's quote-side fees land in the splitter. Anyone may flush it: 60% goes to the buyback wallet, 40% to the protocol wallet. Together with the token-side share, which is burned outright, 80% of everything the protocol earns on these launches is burned or spent buying back $par; the protocol keeps 20%. The launch fee the factory charges is passed straight through to the protocol wallet and is not part of the split. Every hour the buyback wallet turns what it received into ETH, buys $par in the $par pool in a few slices, and burns it with burn, so the supply actually shrinks. The amount bought in one round is capped; anything above waits for the next round. Each burn is a Transfer to address zero from the buyback wallet and the running total is on the $par page and at /buybacks. Launches keep the recipient frozen in their launch record: launches from before the splitter never pay it, launches made under an earlier splitter keep paying that one (its share is readable on chain as buybackBps), and the fees the protocol had already collected before are not part of it. $par itself goes further: its creator share is claimed daily, sent to the buyback wallet and bought back and burned the same way, so with the protocol's token-side burn 75% of every fee paid on $par is burned.
Fees to holders
At launch the creator may give the creator's share away: the launch names the PairPadHolderVault as its creator fee recipient instead of a wallet. The vault has no owner and no function that could hand the share back, and only the recipient may change a launch's recipient, so the choice is permanent. Nothing else about the launch changes; the fee terms, the lock and the pool are the same.
Every hour the holder rewards wallet harvests the vault and pays what came in to the holders as it is, in proportion to their balances at that moment: the quote part of the share (ETH, USDG, whatever the pool trades in) as that quote, the part paid in the token as the token. Nothing is swapped, so nothing is lost to slippage or price impact, and holders see the reward land in their wallet in an asset they can spend. Payouts go through PairPadDisperseV2, which tags every payout with the launch and emits one Paid per recipient, so a wallet's rewards can be read off the chain without tracking the quote's transfers. Pool contracts, the lockers, the vault and the token itself are not holders. Payouts too small to be worth the transfer are carried over per asset until they are. Each round is one Dispersed event per asset; the token page lists them and what the connected wallet received, also at /distributions and /rewards. The launch's feesToHolders flag in the indexer says which launches chose this. Rounds before September 2026 went through the first PairPadDisperse, which bought the token back with the quote part and paid everything in the token.
Buyback & burn
The other way to give the creator's share away: the launch names the PairPadBurnVault as its creator fee recipient. Same permanence as the holder vault, and stricter: the contract has no owner and no function that moves value anywhere except into the token's own launch pool or to the zero address. The pool it buys in is the one the factory recorded for that token and quote, never one named by the caller.
Every hour the holder rewards wallet, as the vault's operator, triggers a round per token: the vault claims the quote part of the share from the escrow, buys the token with it in its own pool through the par router with a slippage floor from a fresh simulation, and burns what it bought together with the part already paid in the token, with burn, so the supply shrinks. The operator can delay or misprice a round; it cannot redirect one. The token part alone can be burned by anyone at any time with burnToken. Each round is a BoughtBack and a Burned event on the vault; the token page lists them and the indexer serves them at /burns. The launch's feeMode in the indexer is creator, holders, burn or floor; feeBurnedToken is what the vault has burned for it so far.
Floor mode
The third way to give the creator's share away: the launch names the PairPadFloorVault as its creator fee recipient. Same permanence and the same restriction as the burn vault: no owner, and no function that moves value anywhere except into the token's own launch pool or to the zero address. The share paid in the token is burned. The share paid in the quote asset is placed back into the launch pool as one single-tick, quote-only Uniswap V4 position, the wall, at the highest price at which that quote could buy every token in circulation. Below the wall nobody can sell, because the wall buys first; the pool itself is unchanged, so every terminal and bot trades it as before.
The wall's price is solved exactly, not estimated. On the way down from the market price the launch position itself buys tokens back (it holds the quote earlier buyers paid in), so the wall only has to absorb what is left when the price reaches it. With the curve's liquidity L, the current sqrt price s, the quote W in the wall and C tokens in circulation (total supply less what the launch position holds), the wall stands at the sqrt price x that solves W = (C − L(1/x − 1/s)) · x² (token as currency0; the mirror when it is currency1), rounded down to the tick spacing and kept strictly under the market price and no lower than the curve's opening tick. Sales burn tokens (fewer to absorb) and buys add quote (more to absorb with), so the floor only ever moves up.
rebalance(token) does a round and anyone may call it; the keeper does on every fee collection, in place of collectFees. It collects the launch's fees through the locker and books the escrow's balance change to that launch (the escrow keeps one balance per recipient, so the vault does the collection itself to know whose quote is whose), burns the token side, withdraws the standing wall, burns the tokens it absorbed and re-places all the quote at the new tick, one wall per market for a multi-market launch. Each placement is a WallMoved event with the ticks, liquidity, quote in the wall and the circulating supply it stands under; burns are Burned. Fees of a floor launch that someone collected directly land unattributed; the operator can assign them to a launch with assignUnattributed, never more than what is actually unattributed and never out of the vault. The launch's feeMode is floor, its floor field carries the standing wall per quote with the floor price and floor market cap, and /floor serves the history that draws the floor line on the token page.
A creator who keeps the fees can still burn the token side by hand: next to the claim on the token page, Burn claims what is owed in the token and calls its burn, so the supply shrinks. The indexer counts burns sent by the fee recipient as creatorBurnedToken.
Creator controls
The creator picks at launch: the quote asset, the creator tax, the fee wallet and the dev buy. None of the token's properties can be changed afterwards. One thing can:
The protocol owner can also move a fee wallet, but only with a three day notice period published on chain, after which the change has to be executed within another three days or it lapses. It goes through even if the creator moved the wallet in the meantime. This exists for lost keys and compromised wallets, and every use of it is visible in the factory's events, so anyone can check it has never been abused.
Fees to an X, GitHub or fomo account
The fee wallet does not have to be a wallet the launcher controls. In the launch form, under Fee wallet, pick X account or GitHub account and enter a handle or profile link. The creator share of every trade then goes to that account's fee account: a contract at an address fixed by the platform and the account's immutable user id (not the handle, which can change hands), the same address on every chain par runs on. Anyone can launch for anyone; the account holder decides whether to pick it up.
Community takeover (CTO)
A token whose creator walked away can be taken over by its community. On par that means one thing: moving where the creator share of trading fees goes. The token itself, its pool, supply, quote asset, creator tax and base fee never change; nobody, including par, can touch those.
Liquidity lock
The position that holds the supply belongs to the locker contract. The locker can receive positions from the factory, collect fees from them, and nothing else: there is no function that transfers them out or removes liquidity from them, for the owner or for anyone. The fee collection is a zero-liquidity decrease, which Uniswap uses as the way to pay out owed fees without touching the principal. The owner's only power is to set the factory address once. This is a short contract and worth reading yourself.
Anyone else may add liquidity to a par pool at any time and remove their own again. There is no lock on third party liquidity.
Risks
The contracts are not audited. The source is public and verified on the explorer; read it before putting in money you cannot lose.
The protocol owner can change the base fee, the protocol share and the maximum creator tax for future launches, disable launching, and move fee wallets with the notice period described above. It cannot touch the pool, the locked position, the token or fees already credited to the escrow, and it cannot change the fee terms of a token that has already launched: the LP fee is part of the pool key and the split is copied into the launch record.
A token quoted in another token carries that token's risk in full. A token quoted in a PONS launch, for example, is worth nothing if that launch goes to zero, no matter how well the par token itself is doing.
There is no snipe protection. Bots can buy in the block after the launch like anyone else. If that matters to you, use the dev buy.
Contracts
Robinhood Chain, chain id 4663. Public RPC https://rpc.mainnet.chain.robinhood.com, explorer robinhoodchain.blockscout.com. Every contract below is verified there and on Sourcify with an exact match, and so is each launched token (see Token metadata). Factory deployed at block 53890474.
The multi-market stack (see Multi-market launches), deployed at block 55587224. It shares the fee escrow, the pricer and the token contract with the stack above.
Who does what
SDK
Everything in the sections that follow is wrapped in a small TypeScript package built on viem, github.com/pardotfamily/par-sdk, MIT licensed. It resolves a token to its pool or pools, decodes launch and trade events, quotes and builds ETH-paid buys and sells (one transaction across every market of a multi-market token, floor on the total), reads token metadata and wraps the indexer API with types. Nothing in it signs; it returns transaction requests for whatever wallet library you use.
npm i viem github:pardotfamily/par-sdk
import { createPar, buildApprove } from "par-sdk";
import { parseEther } from "viem";
const par = createPar({ rpcUrl: RPC_URL }); // public node if omitted
const t = await par.getTradable(token); // null if not a par token
t.kind; // "single" | "multi"
t.markets.map((m) => m.quoteSymbol); // e.g. ["AAPL", "MSFT", "NVDA", "GOOGL", "TSLA"]
const buy = await par.buildBuy(t, parseEther("0.1"), wallet.address, 100); // 1% slippage
await wallet.sendTransaction(buy); // { to, data, value, expectedOut }
await wallet.sendTransaction(buildApprove(t.token, t.router));
const sell = await par.buildSell(t, tokensIn, wallet.address, 100);
par.watchLaunches((l) => console.log(l.token, l.kind, l.pairTokens));
par.watchTrades(t, (x) => console.log(x.side, x.tokenAmount, x.quoteAmount));
await par.indexer.trades(token, { limit: 100 });
await par.indexer.candles(token, "5m");Launching from code
Call launchAndBuyWithEth on the router, or launchToken on the factory if there is no dev buy. Config id 0 is the production config. expectedEconomics pins the opening economics: pass the digest from previewLaunchEconomics for an ETH quote or a quote with owner-set economics (pairTokenEconomics(token).phantomQuote > 0), whose terms only change when the owner changes a config. For every other quote (USDG, stocks, par, any pool-priced token) pass bytes32(0): their opening reserve is read off the reference pool at inclusion, so an exact pin reverts with LaunchEconomicsMismatch whenever that pool trades between your preview and your transaction. Estimate gas yourself and add headroom (about 50%): pricing a pool-quoted asset walks its pools, and that cost moves with their state.
import { parseEther, zeroAddress } from "viem";
const expectedEconomics = await client.readContract({
address: FACTORY, abi: factoryAbi,
functionName: "previewLaunchEconomics", args: [0n, zeroAddress],
});
const launchFee = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFee" });
const params = {
name: "Example", symbol: "EXMPL",
logo: "ipfs://bafk...", description: "What this is.",
socials: { twitter: "https://x.com/example", telegram: "", discord: "", website: "", farcaster: "" },
creatorFeeRecipient: account.address, // zero address = the launching wallet
creatorTaxBps: 0, // 0..1000
expectedEconomics,
salt: "0x" + "00".repeat(32),
};
const devBuy = parseEther("0.1");
await wallet.writeContract({
address: ROUTER, abi: routerAbi, functionName: "launchAndBuyWithEth",
args: [params, 0n, zeroAddress, [], minTokensOut], // [] = no ETH leg, the quote is ETH
value: launchFee + devBuy,
});
// returns (token, poolId, tokensOut)For a pool quoted in an ERC-20, pass that token as pairToken. To pay the dev buy in ETH anyway, pass the hops from ETH to the quote asset as the leg (next section); the router swaps along them first. To pay in the quote asset itself, approve the router and call launchAndBuyWithQuote.
The token address is known before the launch: launchDeployer.predictTokenAddress(LaunchDeployment) takes the same fields the factory will pass (your address as originalDeployer, the salt, supply, name, symbol, logo, description, socials, and the position minter as supplyRecipient). Since the pool key follows from the address, so is the pool id.
Before sending, simulate. Every revert reason is a named error (listed under Errors below), so a simulation tells you exactly what is wrong.
Trading from code
A par pool is a Uniswap v4 pool with no hook. Uniswap's Universal Router, the v4 Quoter and any aggregator or terminal that supports v4 on this chain trade it with the pool key from the next section, and their quotes already include the LP fee because it is the pool's own. Nothing on par's side has to be called.
Anything that reads the chain directly, bots, on-chain routers, terminals with their own indexers, can trade a par pool in the block after launch. Uniswap's hosted routing API, which app.uniswap.org and some aggregators use, works off a pool cache that is refreshed on a schedule and skips pools holding less than about 0.01 ETH of the anchor asset; in our tests a new par token showed up there within twenty minutes of launch. Until then the par router and the on-chain route are the way in.
The par router exists for two conveniences: the launch-and-buy call, and getting ETH into or out of a pool quoted in another ERC-20 in one transaction.
// One pool, exact input. Send value for a native quote buy; approve for anything else.
function swapExactIn(PoolKey key, bool zeroForOne, uint256 amountIn, uint256 minAmountOut, address recipient)
payable returns (uint256 amountOut);
// One pool on the way between ETH and an ERC-20 quote asset. The whole leg, v3 and v4 hops in
// any order, runs inside one PoolManager unlock; v3 hops go through SwapRouter02 from there.
// Nothing is held between hops and leftovers go back to the payer.
struct Hop {
PoolKey key; // v4: the pool key. v3: currency0/currency1 = the two tokens, fee = the v3 tier,
// tickSpacing and hooks zero.
bool v3;
}
// ETH into a pool quoted in an ERC-20: ETH -> quote along the leg (in trade order), then the buy.
function buyWithEth(PoolKey key, Hop[] leg, uint256 minTokensOut, address recipient)
payable returns (uint256 tokensOut);
// The reverse: sell the token for quote, then quote -> ETH along the leg (in trade order).
function sellToEth(PoolKey key, bool tokenIsCurrency0, uint256 tokensIn, Hop[] leg, uint256 minEthOut, address recipient)
returns (uint256 ethOut);The leg to use is the pricer's route: route(quote) returns the hops from the quote to ETH, which is the sell leg as it is and the buy leg reversed. A token with a v3 WETH pool is one v3 hop. A PONS token, whose market is a v4 ETH pool, is one v4 hop. A token that only trades against USDG is its USDG pool then the USDG/WETH v3 pool. A token quoted in a stock token is that pool, then the stock's pool, and so on up to four hops. minTokensOut and minEthOut bound the whole route; there are no per-hop floors.
A buy is zeroForOne = true when the quote is currency0 (always the case for ETH, since address zero sorts first) and false otherwise. To get a quote for the interface, simulate swapExactIn with minAmountOut = 1; the return value is the output after the LP fee. Then set minAmountOut from that. The same works for buyWithEth and sellToEth with a zero floor, or use Uniswap's v4 Quoter.
Pool key and price
The factory gives you the key and the id directly with poolKeyFor(token) and poolIdFor(token). If you would rather compute them, the two currencies are sorted by address, the tick spacing is 10, the hook is the zero address and the fee is (100 + creatorTaxBps) * 100 in v4's hundredths of a bip, so 10000 for a token with no creator tax. The fee is also in the TokenLaunched event and in getLaunchedToken(token).poolFee.
import { encodeAbiParameters, keccak256, zeroAddress } from "viem";
const [currency0, currency1] =
BigInt(pairToken) < BigInt(token) ? [pairToken, token] : [token, pairToken];
const key = { currency0, currency1, fee: poolFee, tickSpacing: 10, hooks: zeroAddress };
const poolId = keccak256(encodeAbiParameters(
[{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }],
[key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks],
));
// Spot price. slot0 lives at keccak256(abi.encode(poolId, uint256(6))) in the PoolManager.
const slot = keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "uint256" }], [poolId, 6n]));
const word = await client.readContract({ address: POOL_MANAGER, abi: poolManagerAbi, functionName: "extsload", args: [slot] });
const sqrtPriceX96 = BigInt(word) & ((1n << 160n) - 1n);
// price of currency1 in currency0 = (sqrtPriceX96 / 2^96)^2Swaps show up as Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee) on the PoolManager, indexed by this pool id. That is how terminals that already index Uniswap v4 on Robinhood Chain see par tokens without any par specific code. The fee field is the LP fee the swap paid, so the amount charged is amountIn * fee / 1e6.
Reading state
struct LaunchedToken {
address token; address deployer; address creatorFeeRecipient; address pairToken;
uint256 phantomQuote; // P in quote units
uint24 poolFee; // LP fee in pips: (baseFeeBps + creatorTaxBps) * 100
int24 tickSpacing; // 10
int24 tickLower; int24 tickUpper; uint128 liquidity; uint256 positionId;
uint16 baseFeeBps; // 100 at launch time, frozen
uint16 creatorTaxBps;
uint16 protocolFeeShareBps; // 5000, protocol's share of the base part
address protocolFeeRecipient;
uint64 launchedAt; bool exists;
}
function getLaunchedToken(address token) view returns (LaunchedToken);
function poolKeyFor(address token) view returns (PoolKey);
function poolIdFor(address token) view returns (bytes32);
function poolFeeFor(uint16 creatorTaxBps) view returns (uint24);
function previewQuoteEconomics(uint256 configId, address pairToken) view returns (uint256 phantomQuote);
function previewLaunchEconomics(uint256 configId, address pairToken) view returns (bytes32);
function launchFee() view returns (uint256); // 0.0005 ETH, in msg.value
function baseFeeBps() view returns (uint256); // 100
function protocolFeeShareBps() view returns (uint256);// 5000
function maxCreatorTaxBps() view returns (uint256); // 1000
function canLaunch(address who) view returns (bool);function lockedPositions(address token) view returns (uint256 positionId);
function isLocked(address token) view returns (bool);
// Fees the position has earned and not collected yet, in the pool's currency order.
// Exact to the wei: read from the pool's fee growth, the same way a collection computes it.
function pendingFees(address token) view returns (uint256 amount0, uint256 amount1);Token metadata
Everything the creator entered is stored on the token contract and never changes. Name, symbol and decimals are normal ERC-20 reads. The rest is exposed both as individual getters and as one getTokenInfo() tuple.
function name() view returns (string);
function symbol() view returns (string);
function decimals() view returns (uint8); // 18
function logo() view returns (string); // "ipfs://bafk..."
function description() view returns (string);
function socials() view returns (string twitter, string telegram, string discord, string website, string farcaster);
function getTokenInfo() view returns (address deployer, string logo, string description, Socials socials);
function contractURI() view returns (string); // ERC-7572
function deployer() view returns (address);
function launchFactory() view returns (address);contractURI() returns an inline data:application/json;utf8, document with name, symbol, description, image and, when a website was given, external_link. Explorers and indexers that read ERC-7572 get the logo without knowing anything about par. The URI never changes.
data:application/json;utf8,{"name":"Example","symbol":"EXMPL","description":"What this is.","image":"ipfs://bafk...","external_link":"https://example.com"}Logos uploaded through the interface are pinned to IPFS via Pinata. The indexer resolves ipfs:// to a gateway URL in its logoUrl field, and every launch is also listed in a Uniswap Token List at /tokenlist.json on this site.
Socials are stored as full URLs (https://x.com/name, not x.com/name or @name). The interface completes whatever was typed before it goes on chain; keep the same rule when launching from code. A launch that sets no website gets the token's page on this site as its website, in the form /t/<first 8 bytes of the salt>. The address itself cannot be used there because it is derived from the metadata, and the salt is chosen before the transaction. GET /resolve?id= on the indexer turns the short id back into the token.
Terminals differ in what they pick up. GMGN reads the logo off the token contract for any token on the chain, but shows socials only for launchpads it has integrated so par tokens appear there with a logo and without links until that integration exists. DexScreener, GeckoTerminal and the terminals built on their data do not read on-chain metadata at all; they take it from their own submission flows (DexScreener's "update token info", CoinGecko listing). A creator who wants a logo there submits it there, as for any token on the chain.
Source verification
Every launch is a fresh contract, and explorers verify addresses one at a time. Terminals show the explorer's verified flag as a safety signal, so an unverified token gets a red mark even though its code is the same as every other launch. The indexer therefore submits the token's standard JSON input to Blockscout and Sourcify as soon as TokenLaunched is indexed and retries until both report an exact match, normally within a minute of the launch. Constructor arguments are recovered from the launch transaction, so one artifact, the compiler input of PairPadLauncherToken from the contracts repository, covers every token. The state per token is at GET /verification?token= on the indexer.
Fee collection and claims
A swap does not pay anyone directly. Its fee is booked to the liquidity in range, which is the locked position. Collecting moves the owed fees out of the position and splits them by the launch terms. The creator's share of both currencies is credited to the escrow. The protocol's share of the quote asset is transferred to the protocol wallet; the protocol's share of the launched token is burned. The creator's payout cannot be blocked by anyone, because nothing is pushed to them; and a protocol wallet that rejected ETH would not block a collection either, its share would simply be credited to the escrow instead.
// Anyone may call. Returns what was pulled out of the position, in currency order.
// Split per currency: protocol gets baseFeeBps * protocolFeeShareBps / (baseFeeBps + creatorTaxBps) / 10000,
// the creator gets the rest. With no creator tax that is 50/50; with a 10% tax the creator gets 95.5%.
// The protocol's part of the launched token is burned rather than paid out.
function collectFees(address token) returns (uint256 amount0, uint256 amount1);function balanceOf(address recipient) view returns (uint256); // ETH
function balanceOfToken(address recipient, address token) view returns (uint256); // per ERC-20, quote or launch token
function claim() returns (uint256);
function claimToken(address token) returns (uint256);To show a creator everything they are owed, take their share of the locker's pendingFees(token) for each currency and add their escrow balances in the quote asset and in the token. The token page does this and gives the creator a claim button that collects first when needed. The keeper collects from every position whose pending fees are worth the gas, every fifteen minutes. A multi-market launch has one position per market in the multi locker; its pendingFees(token, index) and collectMarketFees(token, index) work per market and collectFees(token) takes them all (see Multi-market launches). The escrow is shared, so claiming is the same.
Events
The factory tells you about launches, the PoolManager about swaps, the locker about fee collections. Nothing needs par's servers.
// Factory
event TokenLaunched(address indexed token, bytes32 indexed poolId, address indexed deployer, address pairToken, uint256 launchConfigId, uint24 poolFee);
event LaunchPositionMinted(address indexed token, uint256 positionId, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 tokenAmount, uint256 phantomQuote);
event CreatorFeeRecipientUpdated(address indexed token, address indexed previousRecipient, address indexed newRecipient);
// PoolManager, filtered by poolId. fee is the LP fee in pips; the swap paid amountIn * fee / 1e6 of its input.
event Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee);
// Locker, one per collection that found something. Amounts in the pool's currency order.
// The protocol amount on the token's side is what was burned.
event FeesCollected(address indexed token, address currency0, address currency1, uint256 protocolAmount0, uint256 protocolAmount1, uint256 creatorAmount0, uint256 creatorAmount1);
event ProtocolShareBurned(address indexed token, uint256 amount);
// Escrow
event Credited(address indexed recipient, uint256 amount);
event CreditedToken(address indexed recipient, address indexed token, uint256 amount);
event Claimed(address indexed recipient, uint256 amount);
event ClaimedToken(address indexed recipient, address indexed token, uint256 amount);The amounts in Swap are signed from the pool's point of view: the input currency is negative and includes the fee, the output currency is positive. To attribute a fee to a trade, take the input side times fee / 1e6. The multi-market factory and locker emit events of the same names with different shapes; they are listed under Multi-market launches. Filter by contract address as well as topic.
Errors
Indexer API
The interface runs on a small indexer (Node and SQLite) that follows the factory, the locker, every launched token's transfers and the PoolManager swaps of every par pool. A trade is queryable about a second after it is mined. Everything it serves can be rebuilt from the events above.
GET /health
GET /launches?orderBy=createdAt&orderDirection=desc&limit=30
filters deployer=0x.. | token_in=0x..,0x..
orderBy createdAt | lastTradeAt | tradeCount | totalVolumeQuote
| marketCap (last price in ETH; same supply everywhere, so this is market cap)
| recentVolume (ETH value traded in the last window=60 seconds)
GET /launches/count -> { launched }
GET /launches/:token
GET /trades?token=0x..&limit=200
filters wallet=0x.. (the transaction sender behind the swap)
GET /candles?token=0x..&interval=5m&limit=300
interval 1m | 5m | 15m | 1h | 4h | 1d; before=<unix seconds> pages further back
-> { candles: [{ time, open, high, low, close, openEth, highEth, lowEth, closeEth, volumeQuote, trades }] }
open..close in quote units per token (x1e18); the *Eth fields are the same in ETH,
and the ones to use for a multi-market launch, whose trades are in several quotes
GET /holders?token=0x..&limit=20
GET /positions?owner=0x..
GET /fees?token=0x.. -> fee collections, one row per FeesCollected
GET /distributions?token=0x.. -> holder-rewards rounds (Dispersed events) with totals; launches?feesToHolders=1 lists the launches
GET /rewards?owner=0x..[&token=0x..] -> what one wallet received from holder rewards
GET /burns?token=0x.. -> buyback & burn rounds of a launch (the burn vault's burns) with totals; launches?feeMode=burn lists the launches
GET /floor?token=0x..["e=0x..] -> a floor launch's standing wall per quote (floor price, floor mcap, quote in the wall) and, with quote, its history; launches?feeMode=floor lists the launches
GET /buybacks -> $par bought with protocol fees and burned: { burned, ethSpent, count, items }
GET /stats -> platform totals: launches, holders, trades, volumeEth, creatorEarnedEth, par { burned, buybackEth, ... }
GET /verification?token=0x.. -> { blockscout, sourcify }: pending | submitted | verified | failed
GET /resolve?id=<16 hex> -> { token } for the short link /t/<id>
GET /events -> text/event-stream, one "batch" event per indexed block range
GET /tokenlist.json -> Uniswap Token List of every launch (served by the web app){
"launchpad": "par", "factory": "0x9d33ba78389c8772bc114cba47dc1985e933e76f", "locker": "0x8a6d37b2e6a2ac7970ef69d2932757f04be0a231",
"token": "0x…", "poolId": "0x…", "poolFee": 10000, "tickSpacing": 10,
"deployer": "0x…", "creatorFeeRecipient": "0x…",
"name": "…", "symbol": "…", "decimals": 18,
"logo": "ipfs://bafk…", "logoUrl": "https://ipfs.io/ipfs/bafk…",
"description": "…",
"socials": { "twitter": "https://x.com/…", "telegram": null, "discord": null, "website": null, "farcaster": null },
"pairToken": "0x0000000000000000000000000000000000000000",
"quoteSymbol": "ETH", "quoteDecimals": 18, "quoteRisk": "native",
"launchConfigId": 0, "phantomQuote": "1355700000000000000", "supply": "1000000000000000000000000000",
"baseFeeBps": 100, "creatorTaxBps": 0, "protocolFeeShareBps": 5000, "protocolFeeRecipient": "0x…",
"quoteRaised": "120000000000000000", "tokensOnCurve": "918700000000000000000000000",
"totalVolumeQuote": "310000000000000000",
"creatorFeesQuote": "1550000000000000", "creatorFeesToken": "410000000000000000000000",
"creatorCollectedQuote": "0", "creatorCollectedToken": "0", "burnedToken": "0",
"tradeCount": 14, "lastPriceQuoteX18": "1610000000", "lastPriceEth": 1.61e-9,
"createdAt": 1788308427, "createdBlock": 53116667, "lastTradeAt": 1788309901,
"launchTx": "0x…", "positionId": "1590997",
"marketCount": 1, "totalVolumeEth": 0.31
}{
"factory": "0x3ea29975a79900179f3e1aef93347ba4210c29c1", "locker": "0x5826fbb6201daacd924a3d292841da9142952d59",
"marketCount": 5, "totalVolumeEth": 79.66,
"lastPriceEth": 6.53e-9, // markets' last prices weighted by the supply each pool still holds
"pairToken": "0x…", "quoteSymbol": "AAPL", "poolId": "0x…", "positionId": "1980002", // = markets[0]
"markets": [
{ "index": 0, "poolId": "0x…", "pairToken": "0x…", "quoteSymbol": "AAPL", "quoteDecimals": 18, "quoteRisk": "verified",
"phantomQuote": "2104859783100182389", "positionId": "1980002",
"quoteRaised": "2500549688710347107", "tokensOnCurve": "91432841689928571032301158",
"totalVolumeQuote": "131755491491331151508", "tradeCount": 450,
"creatorFeesQuote": "370026320570184172", "creatorFeesToken": "4511390465115331823850422",
"creatorCollectedQuote": "0", "creatorCollectedToken": "0",
"lastPriceQuoteX18": "50380769485", "lastPriceEth": 6.47e-9, "quotePricedAt": 1788670471, "lastTradeAt": 1788670471 },
…
]
}
// trade rows of a multi-market launch carry the market they hit:
{ …, "market": 1, "pairToken": "0x…", "quoteSymbol": "MSFT", "quoteDecimals": 18, "quoteAmount": "…", "priceQuoteX18": "…", "priceEth": 6.55e-9 }uint256 values are decimal strings, timestamps are unix seconds, addresses are lowercase. A row's factory says which stack launched the token; a single-market launch has marketCount 1 and no markets array, and its top-level quote fields describe its one pool. For a multi-market launch the top-level quote fields (pairToken, poolId, totalVolumeQuote, creatorFeesQuote, quoteRaised, lastPriceQuoteX18) mirror the first market so older clients keep working; tradeCount, tokensOnCurve, totalVolumeEth and lastPriceEth are for the whole token, and the per-market figures are in markets. lastPriceQuoteX18 is the pool price after the last trade in quote base units per whole token, times 1e18, as a decimal string that may carry up to 18 fractional digits (a cbBTC-quoted token is worth a fraction of one satoshi); parse it as a float, not an integer. lastPriceEth is the same price converted to ETH through the quote pricer at that time (the field keeps its name on every chain). creatorFeesQuote and creatorFeesToken are the creator's share of every fee charged so far, computed per swap; the creatorCollected pair is how much of it has been moved into the escrow. burnedToken is every token sent to the zero address so far: the protocol's burned share of fees plus any burn by the creator or a holder. Holder balances come from Transfer events.
Source code
The contracts, their tests and deployment scripts are at github.com/pardotfamily/par, MIT licensed, and every deployed address above has its source verified on Blockscout and Sourcify. The SDK is at github.com/pardotfamily/par-sdk. The interface, indexer and keeper are operated by us and are not published; nothing in them is needed to launch, trade or read a par token, as the sections above show. If you find a security problem, use a private report on GitHub rather than a public issue.