Auctions overview
Auctions are how Milky resolves a loan that didn't get repaid in time. The goal is simple: sell the collateral card, repay the lender what they are owed, return what is left to the borrower, and do it through a transparent on-chain process anyone can audit.
Milky runs the sale. A lender is never handed the card in place of being repaid — that path existed until August 2026 and has been removed.
Every Milky auction is a Dutch auction with a short duration and a defined starting price. The on-chain program carries a second format, English, but Milky does not offer it: nothing in the protocol ever starts an English auction, so it is not a choice available to a lender, a borrower or a bidder here. References to it below describe the shape of the stored auction account, not something you can obtain.
This page covers the shared lifecycle. Format-specific behavior lives in Dutch auctions.
When does an auction start?
An auction can start as soon as a loan becomes eligible for default —
that is, when the current cluster time is greater than maturity_ts + grace_period_secs. Until then, the borrower can still repay on the
original terms.
Once the loan is default-eligible, anyone can submit the
loan_mark_default_and_start_auction instruction. It atomically:
- Marks the loan as
Defaulted. - Initializes a new
Auctionaccount for that loan. - Opens a USDC bid escrow vault tied to the auction.
- Sets the auction's start and end timestamps.
loan_mark_default_and_start_auction is permissionless, and the reason is
that there is nothing for an authority to attest: the only claim the caller
makes is that the clock has passed maturity_ts + grace_period_secs, and the
program checks that itself against the Clock sysvar. A caller who is early is
refused with LoanNotMatured; a caller who is right is telling the chain
something it can already see.
A caller who supplies no parameters gets the protocol's defaults — a 24-hour
descending-price sale with anti-sniping off, which is what the protocol's own
keeper sends. The protocol admin can supply explicit AuctionParams
instead, which is the one thing on this instruction their signature still buys;
an operator might want a longer window for an unusually valuable card.
What this is for: the instruction is the only writer of the pool bookkeeping that stops a dead loan being counted at full value, so a loan nobody could mark would leave every lender in that pool exiting at a price the pool cannot support. Making it permissionless means that cannot depend on one key being available.
Whoever sends it pays rent for the Auction account and the bid escrow — the
payer signer, which does not have to be the same wallet as the admin when an
admin is supplying parameters.
That rent is not returned to whoever started the auction. auction_settle
and auction_cancel_if_no_bids close the loan, auction and lock accounts to
their own settler / caller, so the refund follows whoever finishes the
auction — and it is larger than the starter's outlay, because those two close
four accounts rather than one. Both of those are permissionless too, so one
wallet can start an auction and finish it and come out ahead; a wallet that
starts one and walks away is out of pocket by roughly the rent on two accounts.
The escrow token account is never closed by anything, so its rent is spent for
good either way.
What an auction account holds
A live Auction account stores:
- A reference to the loan it's resolving (and therefore to the collateral NFT and the pool).
- The reserve price — for a Dutch sale, the starting price, taken from the card's appraised value at origination.
- The floor price — the lowest bid the sale will accept: the loan's principal. Bids between the principal and the full payoff are partial recoveries; the pool absorbs the shortfall rather than the sale failing.
- Timing —
start_ts,end_ts,duration_secs(24 hours by default). - Format — Dutch or English (
auction_type). - Bid state — for English: highest bidder and amount; for Dutch: whether a bid has landed yet (which immediately ends the auction).
- A bid escrow vault — the USDC vault holding the current high bidder's funds.
- Anti-sniping config (English only) — the window before
end_tsin which a bid extendsend_ts, and a cap on total extensions.
The collateral during the auction
The card stays in the protocol's collateral vault (the loan's collateral
account) for the entire auction. The loan's loan_authority program-derived
address (PDA) remains the delegate, so when settlement happens the protocol
can sign the transfer to the auction winner without needing the borrower's
signature.
The borrower keeps the right to repay for as long as the loan account
exists — that is, until settlement or no-bid cancellation closes it. The
instruction is loan_redeem_from_auction; it refunds any standing bidder as
a BidCredit, closes the auction, and returns the card by the ordinary
repaid path. It is not gated on the sale still being live, so a borrower can
redeem while a winning bid stands unsettled.
What ends an auction
There are four ways an auction reaches a terminal state:
- A bid lands and settlement runs (English: at end_ts; Dutch: on bid). The card transfers to the winner; USDC is applied per the settlement waterfall; the loan account, locks, and auction account close and refund their rent.
- No bid is received before end_ts and
auction_cancel_if_no_bidsis called. The card moves into protocol custody; loan and auction accounts close. See no-bid outcome. - The borrower repays. The auction closes, any standing bidder is
refunded through a
BidCredit, and the card goes back to the borrower. - The auction is "stuck" — for example, if no keeper triggers either settlement or no-bid cancel. The auction account remains open until someone does. The protocol does not have an automatic timeout.
Permissioning
Different auction operations have different signers:
- Starting the auction. Anyone can submit
loan_mark_default_and_start_auctionagainst a loan the clock says is pastmaturity_ts + grace_period_secs. The protocol admin's signature is needed only to pass explicitAuctionParams; without it the auction opens on the protocol's defaults. A lender can start one on a pool they fund, which they could not before. - Bidding. Anyone can bid (English) or accept the current Dutch price.
- Settlement. Anyone can submit
auction_settle; the winner does not sign it.settleris an unconstrained signer, andwinneris passed as a non-signing account pinned byauction.highest_bid.bidder == winner.key()— that binding, not a signature, is what stops the card being routed to a third party. The settler pays the rent for the winner's token account and collects the refunds from every account the settlement closes. The Milky app bundles bid + settle into a single Dutch transaction, so in practice the winner is usually also the settler — but an abandoned auction can be finished by anybody, which is the point. - No-bid cancellation. Anyone can submit
auction_cancel_if_no_bidsafter the auction expires with zero bids; the rent refund creates a small keeper incentive. - Redemption. Only the borrower can repay their own loan.
- Claiming a residual.
claim_borrower_creditis permissionless to submit but pays only the borrower named on the credit. - Pause behavior. New bids are blocked while the protocol is globally paused; settlement is intentionally not pause-blocked, so winners can always claim cards they've legitimately bought.
Read next
- Dutch auctions — the format used in production.
- Settlement & waterfall — how the proceeds are split.
- No-bid outcome — what happens to a card when nobody bids.