peard
Cranks

The fee route and the sweeper

The venue seam, which turns out to contain no venue at all.


cd relayer && npm run sweep            # claim, then credit, every market
npx ts-node scripts/fake-venue-fee.ts  # stand in for a claim, then sweep

peard owns denomination and settlement and owns nothing of the pool. That principle survives contact with a real venue, and the integration turns out to contain no venue at all.

The whole hook is one subtraction

sweep_fees is permissionless and credits the difference between what the reward vault holds and what the market has accounted for.

let unaccounted = actual.saturating_sub(accounted);
require!(unaccounted > 0, Err::NothingToSweep);

A peard_amm claim_fees, a Meteora DBC fee claim, a DAMM v2 fee position, a treasury top-up and a stray transfer all land in the same token account, and the program credits whatever turned up without knowing which happened.

saturating_sub rather than checked, because an under-accounted vault is the normal state and an over-accounted one means the vault was drained behind our back, in which case sweeping nothing is the right answer rather than aborting the crank forever.

The old vault invariant was vault_usdc == balance. Sweeping relaxes it to vault_usdc <= balance, which is strictly better: money that arrives by an unwitnessed route now reaches holders instead of sitting stranded.

Two facts about DBC make this fit without a CPI

  • Its collect_fee_mode: 0 collects in the quote token only, so a claim deposits exactly the asset the pool is quoted in.
  • Its fee_claimer is an arbitrary address, so fees can be routed at the pool rather than trusted to a keeper afterwards.

Where a venue's claim needs a signature, the crank bundles that instruction ahead of sweep_fees in the same transaction. That adapter is the only piece that differs per venue and it is deliberately outside the program.

The claim pass runs first, and it is not optional

  trader
    |  buy / sell
    v
  peard_amm Pool  ---- fees_quote accrues in the quote vault
    |
    |  claim_fees   (permissionless, destination fixed at creation)
    v
  peard Market's reward vault   (an ATA owned by the market PDA)
    |
    |  sweep_fees   (permissionless, credits the unaccounted delta)
    v
  Market.acc_units_per_token  rises
    |
    |  sync_position / claim / request_fulfillment   (each calls accrue)
    v
  Position.units_owed  rises

Running the sweep without the claim pass is, in the sweeper's own comment, "a crank that reports success every cycle while holders are owed nothing no matter how much the pool trades". The claim runs first because sweeping first only credits last cycle's fees.

The claim pass is keyed by fee_claimer, because that is the only thing tying a pool to a market. It walks every account the peard_amm program owns and skips anything that fails to decode as a Pool, since the program owns other account types.

One seam detail worth knowing

peard_amm checks its fee_destination as the claimer's associated token account. peard's sweep_fees derives the reward vault as the ATA too. The two only agree because the crank targets the ATA explicitly:

marketAta(pool.feeClaimer, pool.quoteMint, quoteProgram)

Fees claimed into some other market-owned account would be stranded: creditable by nothing, recoverable by nobody.

The inbox converter

cd relayer && npm run convert:dry      # quote every inbox, print the edge
cd relayer && npm run convert          # fill what clears minEdgeBps

A Native market's pool pays dollars and its vault holds the asset. The program does not swap; it offers. convert_inbox posts a standing offer to deliver the asset at the oracle price less a bounty and take the dollars.

That makes the converter a market maker rather than an arbitrage bot: it fills out of inventory, is paid in dollars, and buys the inventory back on its own account. Jupiter lives on this side of the line, where it can be swapped for another venue without touching a deployed program.

This is the Meteora-path fallback rather than the main path. Quoting a peard_amm curve in the asset itself means fees arrive already denominated in what a Native vault holds, so there is no inbox, no conversion, and no price read on any path.

Both divisions in convert_inbox round up, so every remainder lands on the vault's side. At the size a fee claim arrives in that is dust; at one base unit it is the difference between an offer and a faucet.

The protocol cut

credit_fees is the single helper deposit_fees and sweep_fees both funnel into, so the two can never drift.

let protocol_cut = ((amount * protocol_fee_bps) / BPS);
let holder_cut = amount - protocol_cut;
m.protocol_fees_usdc += protocol_cut;

The cut comes off the gross, before any units exist. It stays inside the same vault as a tracked field and is withdrawable to Global.fee_receiver by anybody, since the destination is fixed and there is no discretion to guard. protocol_fee_bps is capped at 2,000.

It is also subtracted out of holder_usdc(), so it never inflates coverage. See Quote modes and coverage.

What the route has actually done

On devnet, with real swaps:

pool accrues                       fees_quote  =  500,000
claim_fees   ->  reward vault      500,000 base units arrive
sweep_fees   ->  credit_fees       protocol_cut     50,000   (10%)
                                   holder_cut      450,000
                                   units_new       450,000

After a second swap with a position open, the accumulator landed at 8,987,113, units_outstanding at 899,999, and pending_units_undistributed at 1. Every one of those follows from the formula. See Units and the earning ledger.

scripts/devnet-buy.ts and scripts/devnet-holder.ts are the two scripts that did it, and both exist specifically to prove the join between programs that share no CPI.