The fulfilment valve
Burn units, get the physical thing. A budget, never an obligation, and nothing ships today.
request_fulfillment burns units from the holder's ledger, quotes them in USDC
at the print, and reserves rather than moves the money. An off-chain agent
ships the thing and calls settle_fulfillment.
The off-chain agent does not exist. Every on-chain path below works and is tested; nothing ships anything today. It is the one piece of this with real operational surface (shipping, regional availability, fraud) and it should start with one pairable and one product.
Denominated only
require!(market.quote_mode == QuoteMode::Denominated,
Err::FulfillmentNeedsDenominated);
The valve spends dollars to buy an object. A Native vault holds SPYx, so the mode simply does not apply, and refusing is better than quietly quoting a shipment against an equity balance.
What request_fulfillment checks
It accrues first, then checks four things against Pairable.fulfillment, in
this order:
let quoted = usdc_from_units(units, price);
require!(quoted <= cfg.max_request_usd, Err::RequestTooLarge);
require!(quoted >= cfg.min_request_usd, Err::RequestTooSmall);
require!(quoted <= cfg.remaining_budget(now), Err::BudgetExhausted);
require!(quoted <= market.holder_usdc(), Err::InsufficientVault);
Then it burns the units from both the position and units_outstanding,
reserves the dollars rather than moving them, charges the epoch, and writes
a FulfillmentRequest at ["fulfillment", market, seq].
Reserving rather than moving is what keeps the refund path from depending on
the agent holding funds. Reserved dollars are excluded from available_usdc()
and therefore from coverage and from claims, because a bottle that has been
promised is not buffer.
No coverage haircut applies. A request either fits inside the budget at face value or it is refused. A budget is a queue; a haircut would be a partial shipment, which is not a thing.
commitment is a 32-byte hash of the shipping payload. No address ever
touches the chain; the agent proves it holds the preimage.
Why there is a minimum as well as a maximum
A ceiling alone bounds the blast radius of a bad print. A floor is what keeps
every request from being economically absurd, and the program needs both:
register_pairable enforces min_request_usd <= max_request_usd.
The Coke case is the cleanest statement of it. A 20oz Coke is $2.49 and a parcel is $8 to $10, so a one-bottle request costs four times the bottle to honour and, in the registry's own words, "reads as the protocol being trolled rather than used". The floor is $25, roughly a case, which is what these things actually ship as.
Pears exposed the gap first: produce moves by the carton and shipping a single pear costs several times the pear.
Ship versus Voucher
A Big Mac is a hot prepared item with a ten-minute quality life, and a gallon of petrol is not going in a parcel. For those the honest deliverable is a claim on the good, not the good: a gift card or a fuel card worth N units.
| Preset | Floor | Ceiling | TTL | Epoch budget | Used by |
|---|---|---|---|---|---|
cheapSku | $25 | $50 | 7 days | $500 / day | Coke, eggs, ramen |
midSku | $15 | $150 | 7 days | $1,000 / day | 20lb of rice |
produce | $20 | $60 | 7 days | $500 / day | pears |
voucher | $25 | $100 | 2 days | $500 / day | Big Mac US, Big Mac CH, gas, milk |
The voucher floor is higher because a $6 voucher is not worth the transaction, and the TTL is much shorter because a redeemable claim nobody has actioned in two days is not coming.
Delivery is carried on FulfillmentConfig and no instruction branches on
it. Grep programs/peard/src/lib.rs for Delivery and you find nothing.
It is disclosure, in the same way ItemKind is metadata in peard_vault: it
stops the UI implying a parcel is coming for something that cannot be posted.
What actually differs between the two lives in the numbers beside it.
Three tiers across the registry: ship the object (Coke, eggs, ramen, rice, pears), voucher a claim on it (Big Mac US, Big Mac CH, gas, milk), or nothing at all (Drake's listeners, the Phoenix high).
Settling
settle_fulfillment(shipped) is signed by Global.fulfillment_authority.
Releases the reserve to the agent's own account and counts
units_fulfilled_lifetime.
expire_fulfillment closes a real hole. Without it, an agent going dark holds a
holder's units, the vault's reserved dollars, and a slice of the epoch budget
hostage indefinitely. Perishables make that acute: "we will ship when they are
back in season" is indistinguishable from never.
A config update cannot reset the epoch
One related guard sits in update_pairable. A config update carries the
existing epoch_start_ts and epoch_spent_usd forward rather than taking them
from the new struct, because resetting the rolling epoch would be a budget
bypass dressed as a parameter change.
peard