Path A: peard_perps perps
A synthetic perpetual on something nobody can hold, where funding manufactures the counterparty.
peard_perps is the venue's own perpetuals engine: a virtual AMM where funding
manufactures the counterparty, so a market opens with no liquidity provider, no
counterparty waiting, and nothing to custody.
That is what makes it the right instrument for the 67 index-grade registry entries with no tokenised version to hold. It is also, structurally, a perp venue for anything the registry can price, whether or not a spot market exists anywhere.
| Program | AZdLGAaYiie1mwuhHUfHM9zA9CC3F3BP5tJoUiAE3umZ |
| Live on devnet | 48 markets, all migrated to the backing layout on 2026-08-25 |
| Funding | fired on 2026-08-25; 32 of the 48 carry a non-zero cumulative_funding |
| Pairables routing here | 67 of 111, on 2026-08-25 |
| Instructions | 17 in target/idl/peard_perps.json |
| Market PDA | ["market", pairable], so a market can never be re-created |
The problem funding solves
A perpetual has no expiry, so nothing forces its price back to the thing it tracks. Worse, on an underlying nobody can arbitrage, there is no natural counterparty at all. If everybody wants to be long pears, who is short?
Nobody is, and that is fine, because funding manufactures the counterparty. When the curve trades above the index, longs pay shorts. That payment is what makes being short attractive at exactly the moment nobody wants to be, and it is what drags the curve back toward the index without anyone having to deliver a pear.
This is not novel; it is how every perpetual futures market works. What is unusual here is the underlying. Normally a perp tracks something with a deep spot market, and arbitrage does most of the work while funding trims the rest. Here funding does all of it, because there is no spot market to arbitrage against. That puts far more weight on the index than a typical perp does, and most of the caution in this program is downstream of that fact.
The curve
Each market holds a constant-product curve with virtual reserves.
pub base_reserve: u128, // 9dp
pub quote_reserve: u128, // 6dp
pub k: u128, // base_reserve * quote_reserve
Nothing is in them. They are not a vault and no one can withdraw from them.
Their only job is to price size, so that a large order costs more than a small
one and the mark moves when people trade. state.rs says so directly:
"Reserves exist only to price size against."
pub fn mark_price(&self) -> u64 {
((self.quote_reserve * BASE_ONE) / self.base_reserve) as u64
}
At launch the reserves are sized so the mark starts exactly at the index:
base_reserve = depth_usd_e6 * 1e9 / price_twap
quote_reserve = depth_usd_e6
Collateral is real, and it is separate. It lives in a token vault, it is
counted in collateral_total, and it is denominated in the cluster's dollar
mint. The curve is imaginary; the money is not.
Depth is the free parameter, and also the refusal
depth_usd decides how much a trade moves the price. The router derives it as
max(50_000, 100 * price), with a ceiling of $10,000,000.
That formula is also where the router refuses. A pairable whose one unit is
worth $419,200, like HOME-US-MED, would need $41.9m of depth to be tradeable
at sane impact, so the router says so and skips it rather than launching
something unusable. The reasoning is stated as a rule: a curve holding fewer
than a hundred units cannot price a small trade, and a unit you cannot hold a
hundred of is not a unit anyone can trade.
Funding, and where it nearly went wrong
Every funding_interval_secs, anyone may call settle_funding.
let mut premium_bps = ((mark - index) * BPS) / index;
premium_bps = premium_bps.clamp(-cap, cap);
let step = (index * premium_bps * FUNDING_ONE) / (BPS * USD_ONE as i128);
m.cumulative_funding += step;
The clamp. max_funding_bps bounds one step, at 100bps by default. One
print far from the mark must not be able to empty a position in a single crank.
An unclamped rate is the difference between a manipulated index being a bad
quote and being a theft.
The cumulative index. Nothing iterates positions. Funding is a single number on the market, and a position pays the difference between that number now and what it was when the position last touched it:
pub fn funding_owed(&self, cumulative: i128) -> i64 {
let delta = cumulative - self.funding_snapshot;
((self.base_size * delta) / (FUNDING_ONE * BASE_ONE as i128 / USD_ONE as i128)) as i64
}
O(1) per position and O(1) per crank, which is what makes a market with many
positions affordable. It is the same lazy-index trick as peard's roll and
floorlaunch's debt.
The divisor in that expression is the whole of a bug that shipped and was
caught numerically rather than by a test. It was originally missing the
BASE_ONE / USD_ONE term, so funding came out a million times too small:
on a notional of $8,500, a 1% funding charge produced $0.000085 instead of
$85. Every test passed, because the test asserted cumulativeFunding > 0, and
a sign check passes at any magnitude. The lesson is in the test suite now:
assert the number, not its sign.
The index, and why it is treated as dangerous
sync_index copies price_twap from the pairable account in the peard
registry. It reads that account by hand, never by CPI, checking three
things: the program that owns it, the id it carries, and the address the market
names.
The registry program is pinned in Global at init and there is no setter,
because a market naming its own price source is a market writing its own
liquidation trigger.
pub fn index_ok(&self, now: i64) -> bool {
self.index_price > 0
&& self.index_ts > 0
&& (self.max_index_age_secs == 0
|| now - self.index_ts <= self.max_index_age_secs as i64)
}
Age is measured against the pairable's own timestamp, never the sync time. A registry nobody has pushed to in an hour should look an hour old however recently somebody cranked this market.
index_ok gates funding, opening, and liquidation. A market whose index has
aged out does not trade on the last good number. It stops.
max_index_age_secs may never be zero. Zero disables the check, and the
one place that matters is liquidation, where a stale index costs somebody
their position. init_market refuses a zero, and so does a zero
max_open_base, which would create a market on which no position could ever
be opened. Both refusals exist because the market PDA seed is
[b"market", pairable]: a market can never be re-created, a typo at creation
is permanent, and the only defence is refusing the typo.
Margin and liquidation
collateral = min(collateral, funding_owed) charged first, CLAMPED at zero
equity = collateral_after_funding + unrealised pnl
notional = |base_size| * index / 1e9
maintenance = notional * maintenance_margin_bps / 10000
The clamp on the first line is not a detail. liquidate charges funding
through apply_funding before it computes equity, and apply_funding pays at
most what the position holds: pay = owed.min(collateral), with the remainder
going to bad_debt. A position owing more funding than it has collateral lands
at exactly zero, never below. Reading it the naive way liquidates positions the
program keeps.
Below maintenance, anyone may liquidate. liquidation_fee_bps goes to whoever
did it, which is the only reason anybody would run a liquidator at all, and it
is a cut of what survives unwinding against the virtual reserves rather than
a slice of remaining collateral. At an index of $90 against a curve still marking
$100 the fee is $0.990009 rather than the $1.00 the simpler reading predicts. It
is exactly zero when the unwind settles below zero, so the incentive vanishes
precisely where the damage is.
Leverage is 1x today. initial_margin_bps is 10000, meaning a position must
be fully collateralised. At 1x a long can never be liquidated by price at all,
and a short only past a 90% rise, further than any breaker in the registry
admits, so no legal print jumps a healthy position straight into bad debt. At
that setting the 500bps maintenance margin is nearly unreachable, and that is
the point.
There is a liquidator, and no insurance fund. relayer/src/liquidate.ts
exists and is dry by default, but it has never been run against devnet; that
only became possible on 2026-08-25, when the first real position was opened.
The insurance fund is a prerequisite for leverage and for mainnet and does not
exist. Neither is needed at 1x on devnet, where a position cannot realistically
go under water. Market.bad_debt counts what the vault has had to absorb, and
it is not a rounding counter: a non-zero figure means liquidation was too slow
at least once, and it belongs in front of anyone depositing.
Parameters, and where they come from
Derived from the pairable, because the registry already knows how fast and how volatile each underlying is.
| Parameter | Derivation |
|---|---|
max_index_age_secs | min(30d, params.max_price_age_secs * 2), never equal |
funding_interval_secs | 3600, never faster than the index can move |
max_divergence_bps | max(3000, breaker_bps + 1000) |
base_reserve | depth_usd_e6 * 1e9 / price_twap |
max_open_base | base_reserve / 10 |
The max_index_age_secs rule is worth spelling out. sync_index refuses
anything older than max_price_age_secs, so an equal value would leave zero
window in which an open position could still be closed against the last good
print. Since nothing in the registry expires today, settle_market can never
fire to rescue it. Equal here means trapped forever.
max_open_base at a tenth of the virtual base bounds the mark one side can push
to about +23.5%, which is inside every tier's divergence gate by construction,
so the cap is always the binding constraint.
Policy, the same for every market at launch:
| Parameter | Value | Why |
|---|---|---|
initial_margin_bps | 10000 | 1x. No leverage yet |
maintenance_margin_bps | 500 | near unreachable at 1x, which is the point |
liquidation_fee_bps | 1000, or 2000 on a volatile breaker | the only reason anyone liquidates |
max_funding_bps | 100 | one step cannot empty a position |
fee_bps | 10, 20 or 30 | tiered by the breaker: a more volatile underlying costs more to trade |
Backing: turning "backed by" into an enforced ceiling
Added and deployed to devnet on 2026-08-25. What does not exist is anything to
point at: peard_vault is on no cluster, Global.backing_program is still
Pubkey::default(), and every market's backing_value_usd is zero.
A market may point at a reserve in a backing program, and then it can never carry more open interest than there is real backing for. That turns "backed" from a claim on a website into a constraint the program enforces.
pub fn effective_open_cap(&self, now: i64) -> Option<u128> {
if !self.is_backed() { return Some(self.max_open_base); }
if !self.backing_ok(now) { return None; }
Some(self.max_open_base.min(self.backing_cap_base()?))
}
Three properties are deliberate.
- It takes the lower of the two. Declaring backing can only tighten a market, never inflate one. Backing is a constraint, not a licence.
- The ceiling moves with the price. Backing is marked in dollars and the cap is in base units, so the index converts between them. The same reserve of pears backs more contracts when pears are cheap. Given that the September harvest collapses the pear price every year, that is a real and slightly funny consequence rather than a quirk.
- Stale backing refuses opens only. Closing is never gated on it. A reserve
nobody has marked lately is a reason to stop taking new risk, never a reason
to trap risk already taken. This mirrors how
index_okis used, on purpose.
set_market_backing is authority-gated and sync_backing is permissionless.
That split is only safe because the address is pinned first: without it, anyone
could stand up their own reserve, name someone else's market inside it, and take
over that market's ceiling.
migrate_market exists because Market grew by 52 bytes, from 303 to 355, and
the PDA seed means a market can never be re-created. realloc::zero = false
keeps the existing bytes and the runtime zeroes the new tail, which is exactly
the unbacked default, so a migrated market behaves precisely as it did before.
All 48 devnet markets were migrated on 2026-08-25. Re-reading them confirms every one is 355 bytes and decodes against the current IDL, which is a different and stronger check than the absence of an error. That is what unblocked confirming funding fires.
What is not solved by any of this: somebody still has to buy the pears and rent the cold storage. The mechanism is ready before any reserve exists to point it at.
What is honest to say about a Path A market today
Funding has fired. On 2026-08-25 a position was opened on PEAR-EA to
create a 40.1bps premium and the next interval charged it: cumulativeFunding
moved to 2,400,000,000 and a 166 pear long owed $0.399999, against a prediction
of 2.4e9 and about $0.40 computed beforehand from the program's own
arithmetic. As of that evening 32 of the 48 devnet markets carry a non-zero
cumulative_funding; the other 16 correctly carry zero, because mark equals
index on a market nobody has traded.
Ten of the 48 markets live on devnet are priced by a number a human typed into a
config file, and an eleventh, MINWAGE-HR, by a pinned constant. Fifteen such
attestations exist across the registry as of 2026-08-25, down from twenty, and
they now expire thirty days after the date they carry rather than publishing
forever; the app carries a visible badge on every market sitting on one. The
pinned MINWAGE-HR at $7.25 deliberately does not expire: a pinned constant is a
convention rather than a measurement, and it is true until governance says
otherwise.
The check that finds them does not use the resolver field, because 13 of
the 15 hand-typed attestations are registered as http pairables and the other
two as optimistic. It compares the on-chain TWAP against the operator's typed
figure instead.
A synthetic perp is a real instrument and a hand-typed index is not a real price. Both things are true at once, and the interface has to say which one you are looking at.
peard