Dutch auctions
Dutch auctions are the format used for every default auction in production today. The price starts high, falls linearly over time, and the first bidder to accept the current price wins the card. There is no second round and no escalation.
This page is the practical guide: pricing curve, timing, and what to expect as a bidder.
How the price moves
A Dutch auction's listed price is a function of elapsed time:
current_price = starting_price − (starting_price − reserve_price) × (elapsed / duration)
Concretely:
- Starting price is computed from the reserve (full payoff) plus a configurable premium. With the production keeper's settings, the starting price is 2× the reserve.
- Reserve price is the loan's full payoff:
principal + fixed_interest_due. - Duration is the auction window. The production keeper uses 300 seconds (5 minutes).
- Elapsed is
current_time − start_ts.
Once elapsed reaches duration, the price is exactly the reserve and
stops moving. After that the auction is past its end timestamp and any
new bid is rejected; only auction_cancel_if_no_bids is valid (assuming
no bid landed earlier).
Bidding
A Dutch bid does two things in one instruction:
- Validates the bid amount against the current Dutch price. The bidder may pay exactly the current price or more; less is rejected.
- Atomically ends the auction by setting
end_tsto the bid's timestamp. From that moment on, no further bids can be accepted on this auction.
The Milky app submits a bundled transaction that performs the bid and
the settlement in a single atomic block (bidAndSettleDutchAuction). This
means the winner doesn't need a separate "claim" step — the card
transfers to them in the same transaction that placed the bid.
Bundling also defeats certain front-running scenarios where a third party might try to settle a different auction in between bid and settle.
Refunds
Because Dutch is first-bid-wins, only one bid is ever escrowed per auction. There are no losing bids to refund. If two parties race to bid, exactly one transaction lands first and the others fail at the network level — no funds were ever taken from the losers because their transactions never executed.
Anti-sniping (intentionally absent)
Dutch auctions on Milky do not extend their duration when bids land
late. The production setting is min_increment_bps: 0 and no extensions.
This is by design: the descending-price mechanism already incentivizes
bidders to commit early at higher prices; extending the window would
distort that incentive.
The English-auction format does support anti-sniping. See English auctions for that flow.
A worked example
Suppose a loan with a $1,000 principal at 30% APR for 30 days defaults.
Its full payoff is roughly $1,000 + $24.66 ≈ $1,024.66, so:
- Reserve: $1,024.66
- Starting price (production keeper): ~$2,049.32
- Duration: 5 minutes
After 1 minute (elapsed/duration = 1/5 = 20%), the listed price is:
$2,049.32 − ($2,049.32 − $1,024.66) × 20% = $2,049.32 − $204.93 ≈ $1,844.39
A bidder who accepts at this point pays $1,844.39 in the bid transaction, which then settles atomically:
- $1,024.66 goes to the pool to repay the debt.
- Surplus is
$1,844.39 − $1,024.66 = $819.73. - Of that surplus, the protocol takes its
auction_fee_bpsshare (default 50% of surplus = $409.86), and the rest accrues to the pool's NAV (the other $409.86, on top of the $1,024.66 debt repayment). - The card transfers from the loan's collateral vault to the bidder's wallet.
For the precise waterfall logic, see settlement and waterfall.
What if no one bids?
If 5 minutes elapse and no bid lands, the auction expires. Anyone can then
call auction_cancel_if_no_bids, which moves the card into pool-held
custody. See no-bid outcome.
In practice, fast Dutch auctions on small or thinly-priced cards sometimes do expire without bids; the no-bid path is part of the expected operating model, not an edge case.
Where the parameters live
The production Dutch settings come from the keeper service that triggers
defaults. They are not stored on-chain as defaults — each
loan_mark_default_and_start_auction call passes its own auction
parameters. So while today's keeper uses auction_type: dutch,
duration_secs: 300, and min_increment_bps: 0, a different keeper
operator could in principle pass different values within the protocol's
allowed bounds.
Read next
- Settlement & waterfall — how the bid amount is split.
- No-bid outcome — what happens when nobody bids.
- English auctions — the alternative format also supported on-chain.