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 − floor_price) × (elapsed / duration)

Concretely:

  • Starting price is the card's appraised value — the fair market value the oracle signed when the loan was written, carried on the loan account as oracle_snapshot. No new account and no oracle call is needed at default time; the number is already there.

  • Floor price is where the decay stops: the lowest bid that pays the pool in full. Not a percentage of anything, and not a pool setting.

    It is above the payoff, and that is the point. Two charges come off a sale before the pool sees a cent — the 5% buyer's premium, then the 5% sale commission on what is left — so the floor is the payoff grossed up for both: payoff ÷ 0.95 ÷ 0.95, which is payoff × 1.108. A sale at the floor returns the pool its principal and its interest, with nothing withheld.

  • Duration is the auction window, 24 hours by default.

  • Elapsed is current_time − start_ts.

Once elapsed reaches duration, the price is exactly the floor 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).

The payoff sits below the whole curve rather than somewhere along it. Every price the auction will accept covers it, so a completed sale is never a partial recovery: the lender's principal and interest come back in full or the card does not sell at all. That is the trade the floor makes, and the cost of it is on the other side — see the no-bid case below.

If the appraised value has fallen below the floor, the auction opens flat at the floor. An auction that does not clear at that price is the correct outcome, and it routes to the no-bid path.

t=0h$2,000 — appraised value
≈t=12h$1,567.67 — halfway down
t=24h$1,135.34 — floor, pool made whole
A 24-hour Dutch auction on a $1,000 loan against a card appraised at $2,000. It opens at the appraisal and stops at $1,135.34 — the $1,024.64 payoff grossed up for the buyer's premium and the sale commission — so the price never falls below what the pool is owed.

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.

There is one case where a winning Dutch bid is refunded: the borrower redeems before settlement. Bidding ends the auction but does not conclude the sale, and the borrower's right to repay runs until settlement, so a bid that is placed but not settled can be overtaken. The repayment writes the bidder a BidCredit — the same pull refund an outbid English bidder gets — and the bidder claims it with claim_bid_credit.

In practice the app's bundled bid-and-settle transaction closes this window to zero, because both halves land in the same atomic block. A bidder who splits the two steps is the one carrying the risk.

Anti-sniping (intentionally absent)

Dutch auctions on Milky do not extend their duration when bids land late. The production settings are anti_sniping_window_secs: 0, anti_sniping_extend_secs: 0 and max_extensions: 0. 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 program's other auction format, English, does support anti-sniping — but Milky does not offer English auctions, so that path is not one you can reach here.

A worked example

Suppose a loan with a $1,000 principal at 30% annual percentage rate (APR) for 30 days defaults. Its full payoff is $1,000 + $24.64 = $1,024.64, and the card was appraised at $2,000 when the loan was written. So:

  • Starting price (the appraisal): $2,000.00
  • Floor (the payoff, grossed up): $1,135.34
  • Duration: 24 hours

The floor is $1,024.64 ÷ 0.95 ÷ 0.95, rounded up to the cent the program works in. The two divisions are the buyer's premium and the sale commission, and they divide rather than subtract because each is charged on a base that already includes it.

After 6 hours (elapsed/duration = 25%), the listed price is:

$2,000 − ($2,000 − $1,135.34) × 25% = $1,783.83

A bidder who accepts at this point pays $1,783.83 in the bid transaction, which then settles atomically:

  • $89.19 is the 5% buyer's premium, taken off the bid first.
  • $84.73 is the 5% sale commission, charged on the $1,694.64 that is left.
  • $1,024.64 goes to the pool: $1,000 of principal, then $24.64 of interest. That is the whole of the lender's claim.
  • $585.27 is the borrower's residual, written as a credit they claim.
  • The card transfers from the loan's collateral vault to the bidder's wallet.

At the floor the same split lands on the boundary: a $1,135.34 bid pays $56.77 of premium and $53.93 of commission, and the pool still receives its whole $1,024.64. That is what the floor is for.

For the precise waterfall logic, see settlement and waterfall.

The more common case: no bid at all

The example above is a card that held its value. Most defaults are not that, because a borrower whose card is still worth more than the payoff will simply repay — and since August 2026 they can repay right up until the card sells, so the incentive holds even after the auction opens. The loans that reach an auction are disproportionately the ones where the card has fallen below the debt.

Same loan, payoff $1,024.64, but the card is now worth about $900:

  • Starting price: $2,000 (the appraisal is what it was at origination; the market moved, the snapshot did not).
  • Floor: $1,135.34.

Nobody bids at $2,000, or at $1,500, or at the $1,024.64 payoff — none of those make sense for a $900 card. The price falls all the way to the $1,135.34 floor and stops there, well above what the card is worth. No bid lands, and the sale routes to the no-bid path.

A card worth $1,100 does not clear either, and that is the honest cost of this floor. $1,100 is above the payoff — a sale there would once have recovered the loan in full — but it is below the grossed-up floor, so the auction refuses it. Raising the floor converts some completed-sale-at-a-small-loss outcomes into no-bids, and the card goes into protocol custody instead of turning into cash. That is a deliberate trade: a card that will not fetch the payoff plus the charges on it is a card whose appraisal was wrong, and that belongs in the underwriting conversation rather than being absorbed quietly as a lender loss.

A descending auction with a stale-high start handles the other direction gracefully — the price simply walks down through the staleness — so the accuracy cost of using the origination snapshot is small, and it costs no new account and no new instruction argument.

What if no one bids?

If the price reaches the floor and no bid lands, the auction expires. Anyone can then call auction_cancel_if_no_bids, which moves the card into protocol custody — not the lender's. See no-bid outcome.

Where the parameters live

Auction parameters are passed by the keeper on each loan_mark_default_and_start_auction call rather than stored on-chain as defaults, within the protocol's allowed bounds. The keeper uses auction_type: dutch and duration_secs: 86400.

The pricing inputs are no longer among those parameters. Both the start and the floor are read from the loan itself — the oracle snapshot and the principal — so a keeper operator cannot set them.