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.
READYis reachable only through theAPPROVEtransition (the approve command). WhenapprovalRequiredis disabled,READYis never used —SCHEDULEDstarts directly.- The prebid window is not a persisted status. It is derived from timestamps as a
prebidPhase(NONE/OPEN/CLOSED): the window isOPENwhilenow < prebidEndAt(falling back to end-of-day of the cycle end date), andCLOSEDafter. The phase is orthogonal toauctionStatus— the caller policy decides which statuses the window matters for. PAUSEDfreezes the live auction and clears automatic advancement.RESUMEtransactionally 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 eventauction.state.changedwithdata.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:
- Asserts the transition (
STARTfromSCHEDULED/READY, bySTAFF). - Sets
auctionStatus = LIVE,startedAt = now,startedById = actor,endedAt = null,leadingBidId = null, andleadingBidAmount = null— the live auction opens with no prebid-derived base amount; winners are selected only after the auction ends. - Writes an action log entry:
action: 'START'. - Persists
nextTransitionAt = startedAt + auctionDurationSeconds. - Publishes
auction.state.changed(transition: 'started', withstateVersion) to theauction:<auctionId>room. - Enqueues the
AUCTION_STARTEDnotification 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:
prebidMutabilityPolicyis not accepted here — it is managed at the program/policy layers.nullclears a stale mode-specific override (e.g. dropauctionDurationSecondswhen switching toCALL_MODE); a non-nullvalue 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):
- Gate: the auction must be
SCHEDULEDand the effective policy must setapprovalRequired— otherwiseBAD_REQUEST(see The approval gate). - Effect:
auctionStatusmoves toREADY,nextTransitionAtis recalculated for the approved status, and an action log entryAPPROVEis 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 = nulland the action log isAUTO_START.- The published
auction.state.changeddoes not includeleadingBidAmount. AUCTION_STARTEDnotifications 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 —
leadingBidIdandleadingBidAmountarenull. Prebids never seed a leading bid; the leading bid is set only by live bids during the auction. - Lifecycle timing:
nextTransitionAtis set tostartedAt + durationin the same transaction as the start. - Realtime:
auction.state.changedwithpayload.transition: "started"is broadcast to theauction:<auctionId>room (staff and subscribers). - Notifications:
AUCTION_STARTEDis enqueued for every subscriber of the program. - State version:
stateVersionis 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/latestPrebidamount visible. - Subscriber-visible bid history omits prebid entries. Live bid amounts remain visible.
- Realtime
auction.prebid.createdis durable and staff-only. Its full prebid projection keepsamount: nullwhile 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
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.declarewithprebidId), - 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 |
Related¶
- rest/auction-and-prebid.md — prebid & auction overview
- rest/auction-lifecycle-config.md — policies, presets, snapshots, and automated triggers
- websocket/events/auction.md —
auction.status.updatestaff command andauction.state.changedevent - websocket/events/prebid.md — subscriber prebid contract