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

Settlement & waterfall

When a sale ends with a winning bid, settlement runs the same waterfall regardless of whether the auction was Dutch or English. The waterfall determines how the proceeds are distributed among three parties: MLKY (a sale commission), lenders (via the pool), and the defaulted borrower (the remainder).

The waterfall

Let proceeds be the winning bid and debt = principal + fixed_interest_due be the loan's full payoff.

  1. Commissionproceeds × 500 / 10,000, a flat 5% of gross, charged on every settlement including a shortfall. 0% when MLKY or an affiliate is the buyer.
  2. The poolmin(proceeds − commission, debt), applied to principal first, then interest, then fees.
  3. The borrower(proceeds − commission) − debt, or zero if that would be negative.

The three always sum to exactly proceeds. That is asserted as a unit-test invariant on the settlement function for every input: no rounding may leak value in any direction.

A worked example

Suppose:

  • The defaulted loan has principal $1,000 and fixed interest $24.66, so debt = $1,024.66.
  • The card was appraised at $2,000 when the loan was written.
  • The sale clears at $1,500.

Then:

Proceeds$1,500.00
Commission (5% of gross)$75.00
Pool — principal$1,000.00
Pool — interest$24.66
Borrower residual$400.34

The pool's NAV moves up by $1,024.66 − $1,000 = $24.66 — it recovers its principal and earns its full interest, and that is the ceiling. MLKY earns $75. The borrower gets $400.34 and loses the card.

If the same sale had cleared at exactly the debt ($1,024.66):

  • Commission $51.23, pool $973.43, borrower $0.
  • The pool is short $51.23. Clearing at the debt no longer makes the lender whole, because the commission comes off the top. A lender is made whole at debt / 0.95, which is $1,078.59 here.

If the sale had cleared below the debt — the ordinary outcome, not an exotic one, because a Dutch price keeps falling past the debt down to the principal:

  • At $900: commission $45, pool $855, borrower $0. NAV drops by the $169.66 shortfall.

Why the borrower receives the residual

It is not a concession, and it is not a marketing decision. Under UCC Article 9 the surplus is not the secured party's to allocate: §9-615(d) requires it to be paid to the debtor, and §9-602 puts that among the rules a debtor cannot waive — alongside the notice requirements, the commercially-reasonable-disposition requirement, and the right of redemption.

It also happens to be commercially better for MLKY. 5% of gross beats 50% of surplus everywhere below proceeds = 1.111 × debt, and defaults concentrate below that line: a borrower whose card still holds most of its appraised value repays rather than walking away. Those are the liquidations the protocol previously collected nothing on.

The full reasoning, including the numbers, is in ADR-004.

Where the residual sits

The residual is written as a credit account, not pushed to the borrower's wallet.

Settlement is permissionless — anyone may finish an ended sale. A push transfer to a borrower whose token account is missing or frozen would let one bad destination brick every settlement in the protocol. So settlement writes a BorrowerCredit PDA seeded on ["borrower_credit", auction, borrower] rather than moving the money.

The borrower then collects it in a transaction of their own, with claim_borrower_credit, whenever they like.

This mirrors BidCredit, which already solves exactly this problem for outbid bidders, down to re-deriving the auction signer after the auction account has been closed.

  • The USDC stays in the sale's bid escrow, which settlement does not close.
  • The credit records created_at, the moment settlement wrote it.
  • claim_borrower_credit is permissionless to call but pays only the borrower, and emits BorrowerCreditClaimed { auction, borrower, amount }.
  • No interest accrues on a residual while it sits unclaimed.

The twelve-month claim window

A residual left unclaimed for twelve months can be taken by the protocol. This is the only instruction in the protocol that moves money away from a borrower, so it is worth stating exactly rather than in summary.

The window is BORROWER_CREDIT_CLAIM_SECS, which is 365 days measured from the credit's created_at. Once it has passed, the protocol admin may call sweep_expired_borrower_credit, which pays the residual to config.protocol_fee_recipient, closes the credit account, and emits BorrowerCreditReverted { auction, borrower, amount, created_at, reverted_at }.

Two details decide what this actually means for a borrower.

Your claim does not expire — the protocol's right to sweep begins. claim_borrower_credit never reads the clock. There is no expiry check on the borrower's path at all, so a residual stays claimable by its owner indefinitely, including long after the twelve months are up. What changes at twelve months is that the admin gains a competing right. The two paths overlap rather than hand over: past the window a residual is claimable by the borrower and sweepable by the admin, and whichever transaction lands first wins.

That shape is deliberate. Making the borrower's own claim conditional on the clock would mean a borrower arriving a slot late found their money had become somebody else's, with nothing on chain to explain why. Consumer pledge law — Wis. §138.10(12) and 10 Pa. Code §67.13 — gives that year to the borrower, so the borrower's path is the one kept unconditional.

The admin cannot route it to themselves. The destination is constrained to config.protocol_fee_recipient, not chosen by the signer. The admin receives only the account's rent, having paid for the transaction.

On-chain mechanics

The settlement instruction (auction_settle) does the following in one atomic transaction:

  1. Binds the winner

    auction_settle is permissionless: settler is an unconstrained signer and any wallet may finish an ended sale. What stops a random third party claiming someone else's card is not a signature but a constraint — the supplied winner account must equal auction.highest_bid.bidder, and the card is routed to that account's ATA. Requiring the winner to sign was the previous design; it let an absent winner hold the NFT, the escrow and the loan accounts hostage indefinitely.

  2. Computes the waterfall

    Calls the same shared function used for both auction formats. calculate_settlement_amounts returns (commission, pool_amount, borrower_residual).

  3. Moves the USDC

    Transfers pool_amount + commission out of the bid escrow — not the entire winning bid, which is what it used to transfer. The residual is deliberately left behind in the escrow, under the BorrowerCredit the same instruction writes.

  4. Moves the NFT

    Transfers the card from the loan's collateral vault to the winner's associated token account, signed by the loan authority program-derived address (PDA). This step has separate code paths for standard NFTs (freeze + transfer) and programmable NFTs (delegate + revoke).

  5. Updates pool aggregates

    Decrements the pool's outstanding_principal by the loan's principal and the accrued_interest by the loan's fixed interest, since both are now resolved.

  6. Closes loan and auction accounts

    Frees the rent on the loan account, the auction account, the collateral vault and the locks — but not the bid escrow, which has to survive to hold unclaimed credits. All of the rent goes to the settler; the accounts carry close = settler rather than a refund to whoever originally paid. This is the keeper incentive, and it is attached to finishing a sale rather than starting one.

Settlement closing the loan account is also what ends the borrower's right to redeem. loan_redeem_from_auction takes a Defaulted loan and closes its auction, so redemption is available for as long as both accounts exist, and after settlement neither does. The two race for the same accounts and the loser finds them gone: a redemption after a completed sale fails on the missing loan, a settlement after a redemption on the missing auction.

Note that redemption is bounded by settlement rather than by the sale ending. There is no check that the sale is still live, so a borrower who finds the money on the last day may still redeem while a winning bid stands unsettled — that bidder is refunded as a BidCredit rather than paid in collateral.

The protocol fees that accumulate on the pool are swept by a separate admin instruction (protocol_withdraw_fees), which routes them to the protocol fee recipient and a configured insurance fund split.