Skip to content

Starting an Auction (Live Start Guide)

This document is the complete contract for how an auction transitions from scheduled/prebid to LIVE in the Dichit auction flow. It is written for staff-facing frontends (company & superadmin consoles) and for teams that need to reason about the auction lifecycle end-to-end.

Reference: rest/auction-and-prebid.md · rest/auction-lifecycle-config.md · websocket/events/auction.md · websocket/events/prebid.md


TL;DR — the ways an auction can start

There are exactly two execution paths that move an auction to LIVE:

# Path Trigger Action log startedById
1 Manual start Staff WebSocket command auction.status.update (status: START) in the auction room START the staff user
2 Auto-scheduled start Database lifecycle timestamp becomes due at auctionStartAt AUTO_START null

Path 2 must be armed first: the start time is set via PATCH .../auctions/:auctionId/schedule, and the policy must opt into autoStart (see rest/auction-lifecycle-config.md).

Approval is a policy gate, not a start mode: when the effective policy sets approvalRequired, staff must approve the auction (POST .../auctions/:auctionId/approve) to move it from SCHEDULED to READY — and both start paths then require READY. When approvalRequired is disabled, the auction starts directly from SCHEDULED and the approve endpoint rejects the call. Approval and start mode are orthogonal: a manually-started auction and an auto-started auction both respect the same approvalRequired gate.

There is no REST endpoint that starts an auction. Manual start is WebSocket-only (see Manual start).


The status model (what gates a start)

The auction lifecycle is modelled by a single persistent auctionStatus (AuctionStatus):

PENDING ──(SCHEDULE)──▶ SCHEDULED ──(APPROVE, when approvalRequired)──▶ READY
                          │                                                │
                          └───────────── start paths ──────────────────────┘
                                        (SCHEDULED or READY)
                                               │
                                               ▼
                                            LIVE ──(PAUSE)──▶ PAUSED
                                               ▲                    │
                                               └────(RESUME)────────┘
                                               │
                                               ▼
                                            ENDED ──(declare winner / record lot)──▶ COMPLETE
                                               │
                                               └──(REOPEN_DIVISION, after disqualify)──▶ ENDED

CANCELLED is reachable from PENDING / SCHEDULED / READY / LIVE / PAUSED via CANCEL.
  • READY is reachable only through the APPROVE transition (the approve command). When approvalRequired is disabled, READY is never used — SCHEDULED starts directly.
  • The prebid window is not a persisted status. It is derived from timestamps as a prebidPhase (NONE / OPEN / CLOSED): the window is OPEN while now < prebidEndAt (falling back to end-of-day of the cycle end date), and CLOSED after. The phase is orthogonal to auctionStatus — the caller policy decides which statuses the window matters for.
  • PAUSED freezes the live auction and clears automatic advancement. RESUME transactionally calculates the new end timestamp from the remaining time.

The strict lifecycle (no winner selection from prebids)

The prebid window is a sealed bidding round: prebid amounts are secret and no winner can be selected from them until the live auction has ended. In particular:

  • CLOSED (prebid window) offers no amount review and no winner-selection actions (no direct declare, no lot, no prebid disqualification) — starting the live auction (or, when required, approving it first) is the only way out.
  • Winner selection happens after the live auction ends, from ENDED. If the auction ended with no live bids, the sealed prebids become the fallback candidate pool: staff can declare or run a lot among them — see After the auction ends.

Preconditions for any start

Every start path enforces these gates first:

Gate Failure
Auction exists for the cycle NOT_FOUND
Program bid type is AUCTION or AUCTION_AND_LOT BAD_REQUEST ("Auction is not enabled for this program.")
Company owns the auction (staff calls with a companyId) FORBIDDEN
Cycle is ACTIVE BAD_REQUEST ("Auction operations are only allowed for active cycles.")
An open division slot exists (not every division's winner declared) BAD_REQUEST ("All division winners are already declared for this cycle.")
The prebid window is closed BAD_REQUEST ("Close the prebid window before starting the live auction.")
Approval gate satisfied (see below) BAD_REQUEST ("Approve this auction before starting the live auction.")

The approval gate

approvalRequired Startable status Action needed
false (default) SCHEDULED None — start directly
true READY Approve first (POST .../approve), then start

Approving an auction whose policy does not require approval is rejected ("This auction does not require approval. Approving an auction without the approval-required policy is not allowed."). Starting an approval-required auction that is still SCHEDULED is rejected ("Approve this auction before starting the live auction."). The approvalRequired flag is resolved into the auction's config snapshot when the lifecycle is armed; see rest/auction-lifecycle-config.md.


1. Manual start (WebSocket staff command)

Command

Sent over the auction room WebSocket (the same /ws connection used for auction.subscribe), staff roles only:

{
  "type": "auction.status.update",
  "id": "req_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
  "data": {
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "status": "START"
  }
}
  • Roles: COMPANY, SUPERADMIN (staff).
  • Permission: auction:staff.
  • Response: immediate command.acknowledged — no business data. The actual transition arrives as the domain event auction.state.changed with data.payload.transition: "started".
  • Rate limit: 100 messages / configured window (shared room limit).

When is a manual start allowed?

The prebid window must be closed before the live auction can start — a start attempt while prebidPhase is OPEN is rejected (BAD_REQUEST "Close the prebid window before starting the live auction."). Staff close the window manually (POST /prebid/close, see auction-and-prebid.md) or wait for its deadline.

Beyond the closed window, the start is accepted only from:

Auction status Condition
SCHEDULED Only when approvalRequired is disabled (the default)
READY Approval-required auctions, after the approve command

So when approvalRequired is enabled, the approve step is required for manual starts too.

Effects

On success the server, inside a single transaction:

  1. Asserts the transition (START from SCHEDULED/READY, by STAFF).
  2. Sets auctionStatus = LIVE, startedAt = now, startedById = actor, endedAt = null, leadingBidId = null, and leadingBidAmount = null — the live auction opens with no prebid-derived base amount; winners are selected only after the auction ends.
  3. Writes an action log entry: action: 'START'.
  4. Persists nextTransitionAt = startedAt + auctionDurationSeconds.
  5. Publishes auction.state.changed (transition: 'started', with stateVersion) to the auction:<auctionId> room.
  6. Enqueues the AUCTION_STARTED notification for every subscriber enrolled in the program.

2. Auto-scheduled start

Two steps arm it, and a per-auction delayed lifecycle job executes it when due.

Step A — Schedule the start time

PATCH /v2/admin/auctions/:auctionId/schedule          (SUPERADMIN)
PATCH /v2/companies/auctions/:auctionId/schedule      (COMPANY)

Body (partial updates allowed — omitted fields keep the stored value; null clears, {"auctionStartAt": null} also clears an untouched duration):

{
  "auctionStartAt": "2026-08-10T09:00:00.000Z",
  "auctionDurationSeconds": 1800,
  "reason": "Standard Monday auction"
}

Validation:

Rule Failure
At least one schedule/settings field BAD_REQUEST
DURATION_MODE scheduling without an effective auctionDurationSeconds BAD_REQUEST
auctionDurationSeconds / auctionExtensionSeconds set while in CALL_MODE BAD_REQUEST (both are DURATION_MODE-only)
auctionFirst/Second/ThirdCallSeconds set while in DURATION_MODE BAD_REQUEST (all three are CALL_MODE-only)
Duration ≥ 1 and ≤ 1440 minutes BAD_REQUEST
auctionStartAt strictly in the future BAD_REQUEST ("Auction start time must be in the future. Use manual start for immediate auctions.")
When prebid is enabled: auctionStartAt ≥ endOfDay(cycleEndDate) (payment deadline) BAD_REQUEST
Auction status is SCHEDULED BAD_REQUEST ("Auction schedule can only be changed before the auction starts.")

Scheduling alone does not approve auto-start. It records the start time, freezes the effective config snapshot, calculates nextTransitionAt, and (if a schedule already existed) sends the AUCTION_RESCHEDULED notification.

Notes:

  • prebidMutabilityPolicy is not accepted here — it is managed at the program/policy layers. null clears a stale mode-specific override (e.g. drop auctionDurationSeconds when switching to CALL_MODE); a non-null value for an inapplicable mode is rejected.

Step B — Approve (only when the policy requires it)

POST /v2/admin/auctions/:auctionId/approve        (SUPERADMIN)
POST /v2/companies/auctions/:auctionId/approve    (COMPANY)

Body (optional):

{
  "reason": "Prebid review passed"
}
  • Gate: the auction must be SCHEDULED and the effective policy must set approvalRequired — otherwise BAD_REQUEST (see The approval gate).
  • Effect: auctionStatus moves to READY, nextTransitionAt is recalculated for the approved status, and an action log entry APPROVE is written.
  • There is no rejection path — staff cancel the auction instead.

Step C — The delayed lifecycle job advances the auction

After the scheduling transaction commits, BullMQ receives a deterministic per-auction job for the persisted nextTransitionAt. It invokes process-scheduled-start, which locks and rechecks the auction and is a no-op (processed: false) if any of the following no longer hold. A 30-second reconciler scans due database rows and repairs any missed enqueue operation.

Guard No-op if…
Auction identity the locked auction id differs from the selected candidate
Bid type program bid type is not AUCTION / AUCTION_AND_LOT
Still pending cycleStatus ≠ ACTIVE, auctionStatus ∉ {SCHEDULED, READY}, or (when approvalRequired) not READY
Still due nextTransitionAt is absent or later than now
Prebid closed the effective prebid window is still open

If the guards pass, it performs the same transition as the manual start, with these differences:

  • startedById = null and the action log is AUTO_START.
  • The published auction.state.changed does not include leadingBidAmount.
  • AUCTION_STARTED notifications are enqueued as usual.

Manual start replaces nextTransitionAt with the live auction end timestamp, so the post-commit hook enqueues the end transition instead of replaying the start.


What both start paths have in common

  • Opening bid: the auction opens with no bid — leadingBidId and leadingBidAmount are null. Prebids never seed a leading bid; the leading bid is set only by live bids during the auction.
  • Lifecycle timing: nextTransitionAt is set to startedAt + duration in the same transaction as the start.
  • Realtime: auction.state.changed with payload.transition: "started" is broadcast to the auction:<auctionId> room (staff and subscribers).
  • Notifications: AUCTION_STARTED is enqueued for every subscriber of the program.
  • State version: stateVersion is incremented on every transition.

Prebid amount visibility

When hasPrebid is enabled, prebid amounts are secret until the live auction has ended. Server-side auction logic still uses the real amounts internally (for the post-auction fallback review), but never for a pre-auction winner decision.

Until the auction ends (auctionStatus !== ENDED):

  • Staff/admin console responses include prebid rows, but prebid-derived amount fields are null — including the auction base / leading amounts and the prebid list rows. Pre-auction "decision" flags no longer exist — the closed prebid window is a pure go-live gate with no amount review.
  • Subscriber responses keep the current subscriber's own activePrebid / latestPrebid amount visible.
  • Subscriber-visible bid history omits prebid entries. Live bid amounts remain visible.
  • Realtime auction.prebid.created is durable and staff-only. Its full prebid projection keeps amount: null while sealed.

Once the auction status is ENDED, staff may review the sealed prebid amounts (needed for the post-auction fallback review). Subscribers still see only their own prebid until staff explicitly reveal the amounts:

POST /v2/admin/auctions/{auctionId}/prebids/reveal
POST /v2/companies/auctions/{auctionId}/prebids/reveal
{
  "reason": "Auction completed"
}

The reveal is idempotent, increments stateVersion on the first reveal, writes the REVEAL_PREBIDS action log, and broadcasts auction.state.changed with payload.transition: "prebids_revealed". Auction responses include prebidAmountsRevealed and prebidAmountsRevealedAt.


After the auction ends (winner review)

Winner selection happens only after the live auction has ended (ENDED):

  • Live bids exist → the winner is chosen from live bids (auction.winner.declare).
  • No live bids were received → the sealed prebids become the fallback candidate pool. Staff can then, among the active prebids:
  • declare the current lowest prebid winner (auction.winner.declare with prebidId),
  • resolve a tie at the lowest amount by lot (auction.winner.record_lot).

Declaring a winner sets the auction to COMPLETE when the last open division is filled; a prebid-fallback winner on an auction that still has remaining divisions reopens it back to SCHEDULED for the next division. None of these actions are available before the live auction ends — the closed prebid window only gates the live round.


Sequence diagram

sequenceDiagram
    participant Staff as Staff console (WS)
    participant API as REST API
    participant Svc as Auction service
    participant Job as Delayed lifecycle job
    participant Notif as Notification queue
    participant Room as Auction room (WS)

    Note over Staff,Room: Manual start
    Staff->>Room: auction.status.update {status: START}
    Room->>Svc: startAuctionV2Command
    Svc->>Svc: gates (prebid closed, approval gate)
    Svc-->>Room: auction.state.changed {transition: started}
    Svc->>Svc: persist nextTransitionAt = auction end
    Svc->>Notif: enqueue AUCTION_STARTED notifications

    Note over Staff,Room: Auto-scheduled start
    Staff->>API: PATCH .../auctions/:auctionId/schedule {auctionStartAt, duration}
    Staff->>API: POST .../auctions/:auctionId/approve {reason} (when approvalRequired)
    API->>Svc: persist nextTransitionAt = auctionStartAt
    Job->>Svc: process due auction (processScheduledStart)
    Svc->>Svc: guards (idempotent no-op if state moved on)
    Svc-->>Room: auction.state.changed {transition: started}
    Svc->>Svc: persist nextTransitionAt = auction end
    Svc->>Notif: enqueue AUCTION_STARTED notifications

Error reference

Code Message (abridged) When
NOT_FOUND Auction not found Bad cycle
FORBIDDEN You do not have access to this auction Company mismatch
BAD_REQUEST Auction is not enabled for this program Bid type not auction
BAD_REQUEST Auction operations are only allowed for active cycles Cycle not ACTIVE
BAD_REQUEST All division winners are already declared for this cycle No open division slot
BAD_REQUEST Close the prebid window before starting the live auction prebidPhase is OPEN at start time
BAD_REQUEST Approve this auction before starting the live auction approvalRequired enabled and auction status is SCHEDULED
BAD_REQUEST This auction does not require approval Approve called while approvalRequired is disabled
BAD_REQUEST Auction start time must be in the future Schedule not in the future