DEVNETYou are on Solana devnet. Funds are not real. Behavior matches mainnet.

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).

t=0s2.0× reserve
t=150s1.5× reserve
t=300s1.0× reserve
A 5-minute Dutch auction at the production default settings: starts at 2× the payoff, ends at the payoff floor.

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_ts to 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_bps share (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.