Extending Auctions Without Imposing a Hard End Time

Extending Auctions Without Imposing a Hard End Time

Date: April 26, 2026 Branch: auctions/cashu-p2pk-path-oracle-v1 Commit reviewed: ed1baef (April 17, 2026) Latest upstream: bce6a096 (April 24, 2026)

Note: This post is based on commit ed1baef from April 17. The branch has since advanced to bce6a096 (April 24) with additional commits including progress bar UI, dev mode grace periods, and orphaned token cleanup. Some or all of the design suggestions below may already be implemented or superseded on the current tip. Treat this as a snapshot of a conversation, not the final word.


The problem

The current auction implementation uses a bounded soft-close to prevent sniping. When a bid arrives near the end of the auction, the closing time extends by a fixed amount — but only up to a seller-defined max_end_at. The AUCTIONS codex is explicit about this:

Anti-sniping is only acceptable with a hard upper bound (max_end_at). Without a hard upper bound, anti-sniping is not acceptable.

The concern is real. Without a bound, the auction could theoretically run forever, and lower bidders’ Cashu ecash would remain locked indefinitely.

But Chief Monkey wants auctions where the seller does not impose a hard end time on the bidders. The auction should last as long as bidders are genuinely competing. Setting max_end_at at creation time means the seller is declaring “this auction will definitely end by X” — and that constraint comes from the seller, not from bidder behavior.

This post explores whether we can remove that constraint while keeping the auction safe for all participants.


Background: why sniping matters

Sniping is when a bidder places a last-second bid just before a hard close, leaving no time for others to respond. In a fixed-deadline auction, sniping is the dominant strategy — you never want to show your hand early. The equilibrium is an auction where nobody bids until the final seconds, which breaks price discovery.

Soft-close auctions solve this by extending the clock when bids arrive near the end. The current implementation uses this approach with extension_rule: anti_sniping:<window_seconds>:<extension_seconds> plus a max_end_at cap.

The tension: bounded vs unbounded

The codex rejects unbounded extensions because of a specific concern: locktime.

Each bid locks Cashu ecash to a P2PK pubkey with a timelock. The locktime is baked into the proof at creation time — you can’t change it later. In the current design, the locktime is set to max_end_at + settlement_grace_seconds so that:

  1. The seller can always settle before the locktime expires
  2. Non-winners can reclaim after the locktime expires

Without max_end_at, there’s no single locktime that works for all bids. An early bid placed when the auction had 1 hour left needs a different locktime than a bid placed when the auction had 24 hours left.

The key insight: each bid carries its own deadline

Every bid already has its own locktime. That locktime is the deadline after which the bidder can reclaim their funds. What if we use that deadline as the auction’s effective end time?

Proposed rule:

effective_end_at = highest_bid_locktime - settlement_grace_period

The auction ends at a fixed interval before the current highest bidder’s locktime expires. When a new bid arrives, it comes with its own (later) locktime, which pushes the effective end forward.

This means:

  • The bound exists — the auction definitely ends before the highest bid expires
  • The bound is determined by bidders — not imposed by the seller at creation time
  • The bound is moving — it shifts forward with each new bid
  • The seller always has a settlement window — the grace period between effective end and locktime

How it works in practice

Bid flow

  1. Bidder requests a path grant from the oracle (as today)

  2. Oracle computes current_effective_end from the latest bid

  3. Oracle enforces a locktime constraint:

    locktime >= current_effective_end + settlement_grace + minimum_extension
    

    This guarantees each new bid genuinely extends the auction by at least minimum_extension (e.g., 5 minutes), and the seller always gets the full grace period.

  4. Oracle returns { derivationPath, childPubkey, locktime } — the locktime is oracle-dictated, not bidder-chosen

  5. Bidder locks ecash with the specified locktime

  6. Bidder publishes bid event

  7. effective_end_at updates to locktime - grace

Settlement

After effective_end_at passes with no new bids, the seller requests a settlement plan. The oracle validates bids, resolves the winner, and the seller derives the child private key to redeem the locked ecash — all within the grace window.

If the seller misses the window, the winner can reclaim. This is correct — the seller failed to perform.

Non-winners

Lower bidders’ locktimes expire normally. They reclaim their ecash after their locktime passes, exactly as today. No funds are trapped.

Why this avoids the problems with pure unbounded auctions

No evaporating bids

In a naive unbounded design, the current highest bidder’s locktime could expire, leaving the auction with no valid standing bid. The price would need to “drop” to the next-lower bid, which is confusing and potentially exploitable.

With the moving bound, the auction ends before the highest bid expires. The standing bid never evaporates.

No free-extension attack

In a naive unbounded design, an attacker could place a bid, wait for their locktime to expire, reclaim (net cost: zero), and repeat — extending the auction indefinitely at no cost.

With the moving bound, each bid’s locktime must extend beyond the current effective end. The attacker’s bid becomes the new highest bid, and the auction doesn’t end until that locktime minus grace. The attacker can’t reclaim without ceasing to be the highest bidder — and if they do, the auction settles with whoever is.

No unbounded exposure

With a naive unbounded design plus a doubling increment, bidders face infinite potential exposure. “I’ll bid up to 10,000 sats” is meaningless if 20 more doubling rounds could follow.

With the moving bound, the maximum exposure is bounded by the locktime — which is always known at bid time. A bidder can see “the current effective end is in 30 minutes” and make an informed decision.

Exponential increments as an optional deterrent

The moving bound already prevents sniping by giving other bidders time to respond. But we can layer on an optional exponential increment for sellers who want faster-concluding auctions.

When enabled, the minimum bid increment doubles with each extension:

Bid 1: 1000 sats (increment: 1)
Bid 2: 1001 sats (increment: 1)
Bid 3: 1003 sats (increment: 2)
Bid 4: 1007 sats (increment: 4)
Bid 5: 1015 sats (increment: 8)
...
Bid 20: ~1M sats (increment: ~524k)

After ~20 rounds of sniping, the increment reaches millions of sats. No rational bidder continues. This accelerates the auction’s conclusion without imposing an artificial time cap.

This would be configurable per auction at creation time:

  • increment_mode: fixed — standard linear increment (current behavior)
  • increment_mode: exponential — doubling per extension
  • increment_mode: none — no minimum increment at all (fully open)

Implementation strategy: settlement policy versioning

The current branch (cashu_p2pk_path_oracle_v1) is being hardened for merge into main. The moving-bound design described above should not block that merge — it should be introduced as a follow-up.

The codebase already supports this cleanly. Every auction event carries a settlement_policy tag (currently cashu_p2pk_path_oracle_v1), and the oracle gates on it. This is the version discriminator.

The vast majority of the auction infrastructure is policy-agnostic:

  • Path oracle (kind 30410, grant/redeem flow)
  • HD key derivation (auctionHd.ts, auctionP2pk.ts)
  • P2PK lock and ecash receive
  • Token envelopes (bid, refund, release DMs)
  • Settlement plan computation and winner resolution
  • Bid event structure (kind 1023)

The version-specific code is small and localized:

  • getAuctionEffectiveEndAt() formula
  • Locktime source (client-set vs oracle-dictated)
  • Auction creation validation (max_end_at, extension_rule vs minimum_extension, increment_mode)
  • Oracle endpoint validation rules

A suggested path forward:

  1. Consider merging v1 as-is — bounded max_end_at + anti_sniping extension rule. Solid, tested, and addresses the core use case.
  2. Introduce cashu_p2pk_path_oracle_v2 as a separate PR. Same auction kind (30408), same bid flow, same settlement infrastructure — just a different settlement_policy value and the moving-bound logic.
  3. Both policies coexist on relays. Old auctions settle under v1 rules forever (you can’t change rules retroactively). New auctions can use either policy at the seller’s choice.

This framing — settlement policies within the same auction kind, not “auctions v1 vs v2” — matches the existing settlement_policy tag design and avoids suggesting that the two approaches are fundamentally different products.

Open questions

  1. What should minimum_extension default to? Something small like 5 minutes seems right — enough for a response but not so large that a single bid adds hours.

  2. What should settlement_grace_period be? The current AUCTION_SETTLEMENT_GRACE_SECONDS is 3600 (1 hour), which is very conservative. With the moving bound, the grace period directly adds to the auction duration (each bid extends by grace + minimum_extension). A shorter grace (5-10 minutes) might be more appropriate.

  3. Should there be an absolute maximum duration? Even with the moving bound, an auction could theoretically run for weeks if bids keep arriving. A very generous absolute cap (e.g., 7 days from start_at) might be worth considering as a safety net.

  4. How does this interact with the existing AUCTIONS.md codex? Section 6.1 explicitly states “Without a hard upper bound, anti-sniping is not acceptable.” This proposal replaces the hard upper bound with a moving bound — the codex would need updating.

  5. Locktime oracle enforcement: currently the client computes the locktime. With this design, the oracle must dictate it. This shifts trust slightly — the oracle could set an unreasonably short locktime. But the oracle is already trusted (it holds the derivation paths), so this is consistent with the existing threat model.


Summary

The moving-bound approach gives us unbounded auctions that are safe for all participants:

  • Bidders get an auction that lasts as long as genuine competition continues
  • Seller gets a deterministic settlement window and can always claim the winner’s ecash
  • Non-winners reclaim their funds after their locktime expires, as today
  • Snipers face both a time response window (moving bound) and an optional economic deterrent (exponential increments)
  • No party imposes an arbitrary end time on the others

The bound exists — it’s just determined by the bidders’ own locktimes rather than by a seller-chosen max_end_at.

Feedback welcome. This is a design exploration, not a committed implementation plan.


Write a comment
No comments yet.