Panier

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.

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

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:

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.

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.

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:

The details of each side:

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 swap0.1% of the USDG side, plus the pool fee and gas
Stock to stock0.1% once, on the USDG in the middle, plus both pool fees
Limit order0.1% of the USDG side plus $0.05 to whoever fills it
Cancel, expiryfree (gas only for a cancel)
Hard capsthe 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.

AAPLUSDG / AAPL, 0.05% pool
NVDAUSDG / NVDA, 0.05% pool
TSLAUSDG / TSLA, 0.3% pool
MSFTUSDG / MSFT, 0.3% pool
AMZNUSDG / AMZN, 0.3% pool
METAUSDG / META, 0.3% pool
GOOGLUSDG / GOOGL, 0.05% pool
COINUSDG / WETH 0.01%, then WETH / COIN 0.3%
AMDUSDG / AMD, 0.3% pool
TSMUSDG / TSM, 1% pool
MUUSDG / MU, 0.3% pool
INTCUSDG / INTC, 0.3% pool
PLTRUSDG / PLTR, 0.3% pool
DELLUSDG / DELL, 1% pool
SNDKUSDG / SNDK, 1% pool
MSTRUSDG / WETH 0.01%, then WETH / MSTR 1%
CRCLUSDG / CRCL, 0.3% pool
BABAUSDG / BABA, 0.3% pool
GMEUSDG / GME, 1% pool
SPCXUSDG / SPCX, 0.05% pool
USARUSDG / USAR, 0.3% pool
SPYUSDG / WETH 0.01%, then WETH / SPY 0.05%
QQQUSDG / QQQ, 0.05% pool
GLDUSDG / GLD, 0.3% pool
SLVUSDG / SLV, 0.3% pool
USOUSDG / USO, 0.3% pool
SGOVUSDG / 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

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

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

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 routerOpening soon
TokenLaunching soon on pons
Chainlink adapter0xe9bBcd46F9B0Df04B418e0207876984F9f8336cb
USDG0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
Uniswap SwapRouter020xcaf681a66d020601342297493863e78c959e5cb2
Uniswap QuoterV20x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7