peard
Built and deployed

Path B: peard_amm pools

A constant-product curve with a virtual quote leg, for the mints no other venue will quote against.


programs/peard_amm is the venue's own AMM: a constant-product curve that quotes a launched token directly against a real tokenised asset.

It exists because of one line in Meteora's DBC. is_supported_quote_mint waves through any classic SPL mint but accepts a Token-2022 mint only when MetadataPointer and TokenMetadata are its sole extensions. Thirty-five of the 44 hard entries are Token-2022 carrying more than that, so DBC refuses all 35: every tokenised equity, PAXG and all three Etherfuse bonds. The nine classic-SPL entries, syrupUSDC among them, it would take.

There is therefore no existing venue at which a token can be quoted against SPYx, PAXG or an Etherfuse bond. Rather than wait for one, this is one.

ProgramEdAdkPJMR6wpZq2ehxubV9Cy8opDrD8XisbXNUwXYi4C
Live on devnettwo pools, one quoted in wSOL (2 swaps) and one in EURC (0 swaps)
Instructions5 in target/idl/peard_amm.json
Fee capMAX_FEE_BPS = 500

The curve

Constant product with a virtual quote leg, so a pool opens one-sided: the creator supplies base tokens and no quote at all.

pub fn quote_total(&self) -> u128 { self.quote_reserve + self.virtual_quote }
pub fn k(&self) -> u128 { self.quote_total() * self.base_reserve }

The opening price is virtual_quote / base_reserve. The virtual leg shapes the curve and is not money. A sale large enough to draw on it is refused rather than promising tokens nobody deposited:

require!(gross_out <= p.quote_reserve, Err::CurveExhausted);

k is derived rather than stored. That means a reconciliation which lowers a reserve reprices the curve automatically, with no second number to keep in step. Storing k and lowering a reserve would leave the pool quoting a curve it can no longer honour.

The swap maths

Quote in, base out. The fee comes off what arrived, not what was sent, because on a transfer-fee mint the two differ and pricing the difference would hand it to the trader.

received  =  quote_vault.amount_after - quote_vault.amount_before
fee       =  received * fee_bps / 10_000
net_in    =  received - fee
q1        =  quote_total() + net_in
b1        =  ceil_div(k, q1)
base_out  =  base_reserve - b1

ceil_div, so the base left on the curve is rounded up and the trader never receives the remainder.

Fees are taken in quote on both sides, matching DBC's collect_fee_mode: 0. That is what makes the fee stream land in one mint instead of two, and it is what makes everything downstream single-asset.

Quoting the curve in the asset is the part that pays off downstream: fees arrive already denominated in what a Native vault holds, so there is no inbox, no conversion, and no price read on any path. That makes convert_inbox the Meteora-path fallback rather than the main path.

Reserves are stored, and reconciled downward only

This is the design that falls out of permanentDelegate, and it is the part worth arguing about.

Stored, not measured

A pool that priced off its live balance could be repriced by a stranger sending it tokens.

Reconciled downward

The issuer of every asset in this tier can move tokens straight out of the pool's vault. A curve still quoting the old reserve would pay the first seller in full and leave everyone behind them with nothing.

A bank run settled by transaction ordering is the failure being designed against. Writing the books down reprices for everybody at once, which is the same principle as a coverage haircut rather than a first-come queue.

The asymmetry is the whole mechanism:

  • Down, on a shortfall: charged to accrued fees first, and only then to reserves. The fee claimer is the protocol and can wear it; the traders did not sign up for it.
  • Up, never. Tokens sent to a pool are a gift to nobody.
let short = booked - actual_quote;
let from_fees = short.min(p.fees_quote);
p.fees_quote -= from_fees;
p.quote_reserve = p.quote_reserve.saturating_sub(short - from_fees);
p.shortfall_quote_lifetime += short;

shortfall_quote_lifetime records how much has been taken. A pool with a non-zero figure there has been reached into, and that number belongs in front of anyone about to trade on it.

Every swap reconciles before it prices. sync_reserves is permissionless and exists for the quiet case: a pool that has been reached into should show it on the chart before the next person finds out by selling into it.

scaledUiAmountConfig cannot trip this. base_reserve, quote_reserve and vault.amount are all raw base units, and k is derived from raw, so a multiplier change moves none of them. The pool holds the same tokens after a split as before.

The seam with peard is one address

A pool's fee_claimer is written once in create_pool and has no setter anywhere in the program. The comment on the field is blunt about why: "a mutable fee route is a rug with extra steps."

claim_fees has no signer in its accounts struct at all. Anyone may crank it and nobody may redirect it. It zeroes fees_quote, transfers the whole balance to fee_destination, and emits FeesClaimed. That is the entire crossing between the two programs.

fee_destination is checked as the claimer's associated token account, not merely an account the claimer owns. That is tighter than it used to be, and the reason is a seam this program cannot see: peard derives its reward vault as the ATA, so fees claimed into some other market-owned account would satisfy a looser check, arrive somewhere real, and then be creditable by nothing and recoverable by nobody.

Launch parameters

scripts/launch.ts sets these, in this order, and the ordering constraint is real.

1  read the asset mint account          ->  assetProgram = account.owner
2  getMint(asset, assetProgram)         ->  assetMint.decimals
3  createMint(payer, me, null, 6)       ->  the launched token, classic SPL
4  peard.create_market(100, Native)  ->  Market PDA + reward vault ATA
5  getOrCreateAssociatedTokenAccount    ->  creator's base account
6  mintTo  1_000_000_000 * 10^6         ->  1e15 base units of supply
7  peard_amm.create_pool(100, vq, 90%)  ->  Pool PDA + both vaults
8  peard.set_pool(pool, PeardAmm)   ->  provenance for the UI
9  setAuthority(MintTokens, null)       ->  mint authority revoked
1
Step 4 must precede step 7

The pool's claim_fees sends to an account owned by the fee claimer, and that account is created by create_market. Nothing in peard_amm creates it and nothing in peard_amm can: fee_claimer is an UncheckedAccount that never signs and is never read. Create the pool first and you have a venue that accrues fees it cannot pay out until somebody separately creates the ATA.

2
Step 6 must precede step 7

For a duller reason: create_pool transfers base_amount out of creator_base, so the supply has to be there.

3
Everything else is order-independent

Steps 8 and 9 are deliberately last.

virtual_quote is the parameter worth understanding

const virtualQuote = BigInt(Math.round((fdvUsd / price) * 10 ** assetMint.decimals));

price is the pairable's TWAP in dollars per unit and fdvUsd defaults to $30,000. So fdvUsd / price is the opening value of the curve expressed in the asset, and multiplying by the asset's own decimals puts it in that mint's base units.

Decimals come off the mint and never off a constant, because the tokenised assets are not uniform: xStocks are 8dp, PAXG is 6dp, Backpack's equities are 6dp, Ondo's SLVon is 9dp, wSOL is 9dp.

The rest of the numbers

ParameterValueNote
fee_bps (pool)100one percent, the real one, capped at 500
fee_bps (market)100informational only: peard never collects it, it only receives deposits
Supply1e9 * 10^6 = 1e15 base units
onCurve90% of supplythe retained 10% is the only earning balance until somebody buys
Launched mintclassic SPL, 6dp, no freeze authority"one less thing that can refuse a transfer"

Step 9, revoking the mint authority, has a reason specific to this design. Minting more would not move the pool price, since peard_amm stores reserves rather than measuring them, but it would dilute every holder's pro-rata share of the reward accumulator. The curve is immune and the ledger is not.

Not built

Graduation. A DBC pool migrates to DAMM v2 at a threshold and this does not migrate anywhere, so a pool that fills its curve simply stops having base to sell. Market.pool_kind already anticipates the address changing.