Docs
Panier buys and sells baskets of Stock Tokens on Robinhood Chain: up to 8 stocks with your weights in one transaction. One router contract trades USDG and the 27 listed Stock Tokens through their Uniswap pools, and refuses any fill that lands too far from the Chainlink price of the stock. Next to baskets it runs market swaps, limit orders with brackets, stock to stock trades in one transaction and EIP-2612 permits instead of approvals.
Overview
Every Stock Token on Robinhood Chain has two prices: the price its Uniswap pool will pay right now, and the price Chainlink reports from the stock market. Most of the time they agree. When a pool is thin, has not been arbitraged since the last move, or is being sandwiched, they do not, and a normal swap fills at the pool's price anyway.
Panier reads Chainlink inside the contract and turns it into a hard minimum for every leg of every trade. You keep the pool's liquidity and the pool's price when it is fair; you get a revert when it is not.
- Market swaps with a floor from Chainlink at the band you choose.
- Limit orders that wait in the contract and never fill at a worse price than your limit.
- Stock to stock in one transaction, both legs checked.
- Permit: the first trade of a token is one signature and one transaction.
- Charts and data: candles of each pool, the Chainlink line, and the live gap between them.
Baskets
A basket is a list of up to 8 Stock Tokens with a weight for each. Panier buys all of them in one transaction, so you either get the whole basket or nothing.
Buying. buyBasket(usdIn, tokens, weights, slipBps, to) takes usdIn USDG, keeps 0.1% as the fee, and splits the rest by the weights, given in basis points that must add up to exactly 10,000. Each leg is a swap on that stock's pool with a minimum out set from Chainlink at your band (default 1.5%). If any leg misses its floor, the whole transaction reverts and your USDG stays put. The same stock may not appear twice. buyBasketWithPermit takes a USDG permit signature instead of an approval.
Rounding. The last leg gets whatever is left after the others, so every cent of the USDG is spent.
Selling. sellBasket(tokens, amounts, minOut, slipBps, to) sells each amount back to USDG on its pool, each leg checked against Chainlink, then checks the total against minOut and takes the 0.1% fee once. The app sells everything you hold of the stocks in the basket.
Nothing stays in the contract. Bought stocks go straight to to; the router keeps no balances between transactions.
Presets. The app starts from three lists: Big tech (NVDA 30, AAPL 20, MSFT 20, GOOGL 15, META 15), Chips (NVDA 35, AMD 25, TSM 25, MU 15) and Index (SPY 50, QQQ 30, GLD 20). They are only starting points; change any stock or weight before buying.
Gas. Each leg is one pool swap, so a 5 stock basket costs roughly five swaps of gas in one transaction, well under a cent on Robinhood Chain.
A market swap, step by step
Calling swap(tokenIn, tokenOut, amountIn, minOut, slipBps, to):
- The router checks both tokens are USDG or a listed Stock Token, and takes
amountInoftokenInfrom you. - USDG to a stock: 0.1% is set aside as the fee, the rest buys the stock on its pool with a minimum out from Chainlink, and the stock goes straight to
to. - A stock to USDG: the stock is sold on its pool with a minimum out from Chainlink, 0.1% of the USDG is the fee, the rest goes to
to. - A stock to another stock: sold to USDG, 0.1% fee, the rest buys the other stock, both legs with their own Chainlink minimum.
- Finally the total out must reach
minOut. The app setsminOutto the floor returned byquote.
If any check fails, the whole transaction reverts. The router never keeps a balance between trades.
The Chainlink floor
For each pool leg the router computes the amount Chainlink says you should get, then takes off your band. Buying NVDA with 1,000 USDG at a Chainlink price of $217.07 and a 0.5% band:
- After the 0.1% fee, 999 USDG buys the stock.
- Fair amount at Chainlink: 999 / 217.07 = 4.6022 NVDA.
- Floor at 0.5%: 4.6022 × 0.995 = 4.5792 NVDA.
- If the pool delivers 4.5792 NVDA or more, the trade goes through. If it would deliver less, the transaction reverts.
The same check runs on sells (minimum USDG out from the Chainlink value of what you sell) and on each leg of a stock to stock trade. quote(tokenIn, tokenOut, amountIn, slipBps) returns the fair amount and the floor, so you can see both before you sign.
Choosing a band
The band is how far from Chainlink you accept a fill. You pick it per trade: 0.3%, 0.5%, 1% or 1.5%. It can never be looser than the router's cap, 1.5% at launch.
- 0.3%: deep pools (NVDA, AAPL, GOOGL, SPY) during market hours.
- 0.5%: the default, fine for most trades of a few hundred dollars.
- 1% to 1.5%: thinner pools, bigger size, or a pool fee of 1% (TSM, DELL, SNDK, GME).
The pool fee is part of what the pool delivers, so a 1% fee pool cannot fill inside a 0.3% band. The app shows the live gap between each pool and Chainlink, which is the quickest way to pick.
Stock to stock
Rotating from TSLA to NVDA on a usual swap page takes two swaps through USDG and two approvals. Panier does it in one call: the TSLA leg is sold with a Chainlink floor, 0.1% is taken from the USDG in the middle, and the NVDA leg is bought with its own floor. The floor returned by quote counts the band twice, once per leg.
Permit instead of approve
USDG and the Stock Tokens implement EIP-2612. swapWithPermit and placeOrderWithPermit take the allowance as a signature for exactly the amount of the trade, so nothing stays approved after it.
- USDG signs with the domain name "Global Dollar", version 1, chain 4663.
- Stock Tokens publish their domain through
eip712Domain()(EIP-5267); the app reads it before asking you to sign. - If someone submits your permit first, the router ignores the failed permit and uses the allowance that is already there.
- If your wallet cannot sign typed data, the app falls back to a normal approve for the exact amount.
Orders and brackets
placeOrder(token, buy, amount, limit, stop, expiry) creates an order on one Stock Token with up to two triggers. Either price may be 0, not both:
- Sell with a bracket: take profit when Chainlink is at or over
limit, or stop out when it is at or understop. The stop must sit below the target. - Buy with a bracket: buy the dip at or under
limit, or the breakout at or overstop. The breakout must sit above the dip. - Whichever trigger Chainlink reaches first fills the whole order; the other goes with it.
- At the
limitthe fill must be no worse than the limit price. At thestopthe order fills like a market order, inside your band of Chainlink: a stop protects you from holding on, it does not promise the stop price.
The details of each side:
- Buy:
amountis USDG to spend. It fills when Chainlink is at or underlimit. - Sell:
amountis the Stock Token to sell. It fills when Chainlink is at or overlimit. limitis a USD price with 8 decimals, the same units as Chainlink.expiryis a Unix time, or 0 for an order with no end.- The minimum order is 2 USDG (or 2 USDG worth of the stock).
- What you put in is held by the router, in your order, until it fills, expires or you cancel.
The fill. fill(id) checks Chainlink has reached the limit, then trades on the pool with the usual Chainlink floor and one more check: the price you actually get must be no worse than your limit. A buy of 500 USDG with a $195 limit must deliver at least (500 − fees) / 195 NVDA; a sell must deliver at least the amount times $195. If the pool cannot meet that yet, the fill reverts and the order keeps waiting.
Cancel and expiry. cancel(id) returns everything in the order to you, at any time, from the owner of the order only. Once an order passes its expiry, any fill call closes it and returns the funds instead of trading.
due(from, count) lists the open orders that can be filled or closed now. getOrder(id) and ordersCount() read the book.
The keeper
A Cloudflare worker checks the book every few minutes and calls fill on every due order. It earns $0.05 per fill from the order, which pays its gas; it refuels its own ETH from that USDG. Anyone else may call fill too and earn the same fee, so orders do not depend on Panier's keeper staying up.
Fees
| Market swap | 0.1% of the USDG side, plus the pool fee and gas |
| Stock to stock | 0.1% once, on the USDG in the middle, plus both pool fees |
| Limit order | 0.1% of the USDG side plus $0.05 to whoever fills it |
| Cancel, expiry | free (gas only for a cancel) |
| Hard caps | the fee can never exceed 0.5%, the fill fee $0.50 |
Example: buying $1,000 of NVDA costs $1 to Panier and about $0.50 to the 0.05% pool. A $500 limit buy costs $0.50 plus $0.05.
Pools and routes
Each Stock Token trades through its deepest Uniswap pool against USDG. COIN, MSTR and SPY route through WETH, because that is where their liquidity sits; each of their legs is still checked against Chainlink as a whole.
| AAPL | USDG / AAPL, 0.05% pool |
| NVDA | USDG / NVDA, 0.05% pool |
| TSLA | USDG / TSLA, 0.3% pool |
| MSFT | USDG / MSFT, 0.3% pool |
| AMZN | USDG / AMZN, 0.3% pool |
| META | USDG / META, 0.3% pool |
| GOOGL | USDG / GOOGL, 0.05% pool |
| COIN | USDG / WETH 0.01%, then WETH / COIN 0.3% |
| AMD | USDG / AMD, 0.3% pool |
| TSM | USDG / TSM, 1% pool |
| MU | USDG / MU, 0.3% pool |
| INTC | USDG / INTC, 0.3% pool |
| PLTR | USDG / PLTR, 0.3% pool |
| DELL | USDG / DELL, 1% pool |
| SNDK | USDG / SNDK, 1% pool |
| MSTR | USDG / WETH 0.01%, then WETH / MSTR 1% |
| CRCL | USDG / CRCL, 0.3% pool |
| BABA | USDG / BABA, 0.3% pool |
| GME | USDG / GME, 1% pool |
| SPCX | USDG / SPCX, 0.05% pool |
| USAR | USDG / USAR, 0.3% pool |
| SPY | USDG / WETH 0.01%, then WETH / SPY 0.05% |
| QQQ | USDG / QQQ, 0.05% pool |
| GLD | USDG / GLD, 0.3% pool |
| SLV | USDG / SLV, 0.3% pool |
| USO | USDG / USO, 0.3% pool |
| SGOV | USDG / SGOV, 0.3% pool |
Market hours and weekends
Chainlink stock feeds update while the market is open and pause between sessions. The router accepts a Chainlink answer up to 3.5 days old, so it keeps working over weekends using the last close. Pools keep trading 24/7, so a pool that runs away from Friday's close is refused until Chainlink catches up on Monday. Limit orders likewise fill on Chainlink, not on weekend pool moves.
The app
- Markets: the 27 Stock Tokens with Chainlink price and 24h change.
- Chart: candles of the deepest pool from GeckoTerminal (15m, 1h, 4h, 1d), the Chainlink price as a dashed line, and your open limits as lines.
- Order panel: Market (buy or sell with a band), Limit (price, amount, expiry) and Stock to stock.
- Orders: your open orders with cancel, your history, and the limit book of the stock grouped by price.
For bots and integrators
Everything the app does is plain contract calls. A buy with viem:
const [fair, floor] = await client.readContract({ address: ROUTER, abi, functionName: "quote", args: [USDG, NVDA, 1_000_000000n, 50] });
await wallet.writeContract({ address: ROUTER, abi, functionName: "swap", args: [USDG, NVDA, 1_000_000000n, floor, 50, me] });
// sell 2.5 NVDA: target $230, stop $205, valid for 7 days
await wallet.writeContract({ address: ROUTER, abi, functionName: "placeOrder", args: [NVDA, false, 2_500000000000000000n, 23000000000n, 20500000000n, now + 7n * 86400n] });
// a basket: $1,000 into NVDA 60% and TSLA 40%, in one transaction
await wallet.writeContract({ address: ROUTER, abi, functionName: "buyBasket", args: [1_000_000000n, [NVDA, TSLA], [6000, 4000], 0, me] });
The site serves /api/prices (Chainlink and 24h change), /api/pegs (pool price, address, liquidity and volume per stock) and /api/orders?a=0x… (the open book and one trader's orders).
Events
Swapped(trader, tokenIn, tokenOut, amountIn, amountOut, usdSide)Placed(id, owner, token, buy, amount, limit, stop, expiry)Filled(id, atStop, usdSide, tokenSide, by)Closed(id, expired)for a cancel or an expiryBasketBought(trader, usdIn, tokens, amounts),BasketSold(trader, tokens, amounts, usdOut)
Owner limits
The owner of the router can list Stock Tokens and set their routes, change the price adapter, set the band cap (up to 5%), the fee (up to 0.5%), the fill fee (up to $0.50) and the treasury. There is no function that moves a trader's tokens or an order's funds anywhere except through a trade that passes the checks, or back to the trader.
Risks
- Unaudited software. Size accordingly.
- The floor relies on Chainlink. A wrong or stale feed would set a wrong floor; the router refuses answers older than 3.5 days.
- A floor stops bad fills. It does not make a trade a good idea, and it does not protect against the stock itself moving.
- Limit orders fill when Chainlink reaches the limit and the pool can meet it; in a thin pool an order may wait longer than the chart suggests.
- Stock Tokens track their stocks through Robinhood; their own terms and risks apply.
FAQ
Does Panier have its own liquidity? No. It routes through the existing Uniswap pools and adds the checks.
Why did my swap revert? The pool was outside your band of Chainlink, or the total was under minOut. Try a wider band, a smaller size, or wait for the pool to come back in line.
Can a limit order fill at a worse price? No. The contract checks the fill against your limit and reverts otherwise.
What happens to my order if the keeper stops? Nothing is lost. Anyone can fill it, and you can cancel at any time.
Token
The Panier token launches on pons. Its contract address appears on the home page and here once it is live. Always check the address on this site before trading.
Contracts
| Panier router | Opening soon |
| Token | Launching soon on pons |
| Chainlink adapter | 0xe9bBcd46F9B0Df04B418e0207876984F9f8336cb |
| USDG | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| Uniswap SwapRouter02 | 0xcaf681a66d020601342297493863e78c959e5cb2 |
| Uniswap QuoterV2 | 0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7 |
