Trade Contracts¶
Status: π§ Partial β
cargo_delivery,express_delivery,hazardous_transport, andbulk_procurementlifecycle shipped;refugee_transport,acquisition_bounty, andescortenum values have no generator yet. (re-verified 2026-08-21 vs Sectorwars2102 HEAD46bce720.)
Trade contracts let players (and NPCs) commit to deliveries with deadlines and rewards. They are pure economic exchange between two parties (NPC-to-player or player-to-player). Faction reputation moves emergently per ADR-0032 β honoring a contract issued by a faction-aligned NPC moves rep through the trade-volume channel like any other transaction, not a quest-completion bonus.
π§ Partial. The Contract model, contract_service.py, contract_generator.py, an expiry-sweep scheduler, and POST/GET routes/contracts.py (/board, /mine, /{id}, /{id}/accept, /{id}/complete, /{id}/abandon, create, /{id}/cancel) are all shipped and wired. The posted β accepted β completed / abandon / expire lifecycle is live for cargo_delivery, express_delivery, hazardous_transport, and bulk_procurement β the NPC generator produces all four contract_types; the remaining three values in the schema below (refugee_transport, acquisition_bounty, escort) exist in the enum but have no generator yet. Player-to-player posting (post_player_contract, cargo_delivery or bulk_procurement) and cancellation are also shipped. Bulk procurement is fulfilled via a rented station locker, not a direct per-delivery payout β the acceptor deposits the commodity across installments, and the deposit that brings the locker to the contract's full quantity auto-completes the contract for the full payment in one settlement; a walk-away or deadline-lapse charges a penalty computed from the locker's fill at that moment (see Bulk procurement below). Status markers are omitted on individual subsections β per-section prose describes the target spec; the shipped scope is exactly the lifecycle above plus bulk procurement's locker-fulfillment path.
Overview¶
Two contract directions:
| Direction | Issuer | Browsed at |
|---|---|---|
| NPC-to-player | A station, corporation, or colony NPC | The station's contract board |
| Player-to-player | Any player who posts an offer | One or more chosen stations' boards |
Both directions share the same lifecycle, payment model, and dispute rules. Differences live in how the contract is generated and who pays the penalty on failure.
A ship too small to carry a contract's full required quantity in one trip can rent a storage locker at the destination station and fulfill it over multiple installments instead.
Contract schema¶
The Contract table is the central ledger for every contract β NPC-posted cargo runs, player acquisition bounties, escort jobs. Each row tracks state, parties, escrow, and deadlines.
| Column | Type | Constraints | Notes |
|---|---|---|---|
id |
UUID | PK | |
issuer_type |
Enum (npc, player) |
not null | Source of the posting |
issuer_id |
UUID / Integer | not null | FK β Player.id (player) or the posting station's Station.id (npc β NPC contracts are posted by a station's own board, not a distinct NPC identity) |
acceptor_player_id |
UUID | nullable, FK Player.id |
Set on accept; remains for the lifecycle (single-acceptor at launch) |
contract_type |
Enum | not null | cargo_delivery, bulk_procurement, express_delivery, hazardous_transport, refugee_transport, acquisition_bounty, escort |
status |
Enum | not null, default posted |
posted, accepted, in_progress, partial_fulfilled, completed, cancelled, disputed, expired (state-transition matrix below) |
origin_station_id |
UUID | nullable, FK Station.id |
Null for multi-source contracts (e.g. acquisition_bounty) |
destination_station_id |
UUID | not null, FK Station.id |
Final delivery point |
commodity_type |
String(50) | not null (or null for escort) |
Validated at the service layer against the live ResourceType registry (resource.py), not a fixed enum β the older "canonical seven" phrasing predates the registry and a hardcoded column-level enum would drift the moment a new commodity ships; or colonists for refugee contracts |
quantity |
Integer | not null (or null for escort) |
Units to deliver |
payment |
Decimal(19,2) | not null | Base payment, frozen at posting |
penalty |
Decimal(19,2) | not null | Penalty on failure (default 1.0Γ payment for cargo) |
acceptance_fee_pct |
Decimal(5,2) | not null, default 2.0 | Percentage of payment charged on accept |
escrow_amount |
Decimal(19,2) | not null | Player contracts: payment + insurance_pool_reserve. NPC: 0 |
escrow_state |
Enum | not null, default held |
held, released, disputed, refunding |
faction_id |
UUID | nullable, FK Faction.id |
NPC contracts: issuing faction |
reputation_reward |
Integer | nullable | Frozen at posting |
reputation_penalty |
Integer | nullable | Frozen at posting |
deadline |
Timestamp | not null | Game-world time |
posted_at |
Timestamp | not null | |
accepted_at |
Timestamp | nullable | Set on accepted |
completed_at |
Timestamp | nullable | Set on completed |
partial_fulfilled_amount |
Integer | nullable | Reserved for a future multi-acceptor partial-fulfillment model (π design-only). The shipped single-acceptor bulk-procurement fulfillment path (a rented storage locker) tracks deposited units through the locker's own ContractCargoDeposit rows, not this column β it stays null on every bulk contract today. |
partial_fulfilled_payout |
Decimal(19,2) | not null, default 0 | Reserved alongside partial_fulfilled_amount for the same future model β stays 0 on every bulk contract today. The shipped locker path pays the full payment in one settlement at completion, not pro-rata. |
dispute_filed_at |
Timestamp | nullable | Triggers escrow_state := disputed |
dispute_resolution |
Enum | nullable | full_payout, partial_payout, refund, split, penalty |
dispute_resolved_at |
Timestamp | nullable | |
dispute_notes |
Text | nullable | Free-form evidence |
escalated_to_admin |
Boolean | not null, default false | Per ADR-0062 E-I3 β set when the dispute meets one of: (a) both parties dispute, (b) evidence trail incomplete (combat / market / delivery log gap), or (c) disputed value > 100,000 cr. Other disputes resolve via the automated rule engine. Escalated disputes enter the admin review queue per ADR-0058. |
insurance_coverage_tier |
Enum | nullable | basic, standard, hazard β premiums 2% / 5% / 10% of contract commodity value respectively; see Risk & insurance |
insurance_premium_paid |
Decimal(19,2) | not null, default 0 | Held in escrow. On completion: released to insurer (not refunded). On mid-term cancellation per ADR-0062 E-I2: pro-rata refund based on (1 - elapsed/duration), minus a 10% cancellation fee retained by the insurer. Refund credits to the contract holder in the same transaction as the cancellation. |
insurance_claim_filed |
Boolean | not null, default false | True after acceptor claims insurance for ship loss |
posting_stations |
UUID[] | not null (player contracts) | Stations where the contract is visible |
Indexes: (status, destination_station_id, posted_at DESC) for board listings; (issuer_id, status) for "my posted"; (acceptor_player_id, status) for "my accepted"; (deadline) for expiration polling; (status, dispute_filed_at) for dispute queue.
State transitions¶
posted
ββ issuer cancels (pre-accept) β cancelled, escrow β refunding (issuer refund: 99%)
ββ accept β accepted (acceptance fee debited)
ββ deadline expires unaccepted β expired (escrow returned to issuer)
accepted
ββ load cargo / begin escort β in_progress
ββ bulk_procurement: deposit reaches full quantity β completed, escrow β released (no in_progress bridge β see below)
ββ acceptor walks (bulk_procurement) β expired (dynamic penalty from the locker's fill; locker β claimable, acceptor keeps deposited goods)
ββ mutual cancel β cancelled, escrow β refunding (kill-fee = 2% accept + 10% cancel)
in_progress
ββ cargo verified at destination β completed, escrow β released
ββ deadline expires β expired (penalty applied; reputation penalty)
ββ cargo destroyed in transit β cancelled (insurance pays if held)
ββ acceptor files dispute (within 48h) β disputed (escrow frozen)
disputed
ββ resolution β completed | cancelled | expired (per outcome)
Bulk procurement is fulfilled by depositing the commodity into a rented storage locker at the destination station; the contract stays accepted through every deposit installment β there is no in_progress bridge for the locker-fulfillment path. The deposit that brings the locker's accumulated total to the contract's full quantity auto-completes the contract in the same transaction, paying the full payment in one settlement (see Bulk procurement below). π A future multi-acceptor partial-fulfillment model (tracked via partial_fulfilled_amount/partial_fulfilled_payout) is reserved but unbuilt; single-acceptor is the shipped model.
Contract boards¶
π§ Partial. Each station exposes a contract board β GET /board?station_id= is shipped (routes/contracts.py:189-217, get_contract_board) and returns the union of contracts where issuer_id == station_id (NPC-issued, and player-issued once posted with that station as issuer) or posting_stations.any(station_id) (player-posted contracts listing this station), status-filtered to POSTED and not-yet-expired. β
Shipped (player UI): ContractBoardVenue.tsx (SpaceDock / GameDashboard) β board browse, my-contracts, accept/complete/abandon/insure/dispute/cancel/post via contractsAPI. Not yet built on the board route: reputation-gated hiding (faction-gated NPC contracts are not filtered by minimum reputation threshold today) and issuer-blocklist hiding for player contracts β both are π design-only.
Boards refresh on the same tick cadence as market prices (see trading.md Β§ Pricing).
A station's board capacity is bounded by station class, ranging from a Class-0 trade hub down to a Class-8 black hole β this ladder was adopted by ADR-0093 item 37 (folded from ADR-0093, re-verified 2026-08-07). β
Shipped: CONTRACT_BOARD_CAPACITY_BY_CLASS + board_capacity_for_class in contract_generator.py; get_contract_board applies .limit(capacity) on the board route (api/routes/contracts.py). (re-verified 2026-08-16 β LEG-13 landed; LEG-36 doc sync)
| Station class | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Board capacity | 40 | 32 | 26 | 20 | 16 | 12 | 10 | 8 | 6 |
The generator fills up to capacity on each tick, biased toward contract types that match the station's class trading pattern. (Station classes.)
NPC-issued contracts¶
π§ Partial. generate_npc_contracts (contract_generator.py) seeds new entries on a tick and prunes expired ones for four contract types β cargo_delivery, express_delivery, hazardous_transport, and bulk_procurement (compute_cargo_delivery_payment / compute_express_delivery_payment / compute_hazardous_transport_payment / compute_bulk_procurement_payment respectively). The remaining three contract types below (refugee_transport, acquisition_bounty, escort) are π design-only; the enum values exist but no generator produces them yet. Generator inputs:
- Station class and current commodity surplus / deficit.
- Faction control of the surrounding region.
- Time of day in the game world (express-delivery rates spike during high-traffic windows).
- Active galaxy-wide events (war, plague, blockade) that modulate refugee and hazardous demand.
Categories:
Cargo delivery¶
Pick up commodity X at station A, deliver to station B by time T. Payment on delivery. Cargo is reserved at the origin: accepting the contract grants the player a one-time pickup right at a fixed price (often free or below-market).
Bulk procurement¶
β
Shipped. A station generates a bulk_procurement contract when its live sell-stock at the origin drops below MIN_CONTRACT_QUANTITY Γ 2 (a 40-unit deficit floor) β the station is short on the commodity, not sitting on a surplus to move out. The generated contract's quantity is pinned to MAX_CONTRACT_QUANTITY (150 units), the same per-haul ceiling every other NPC-generated type uses, regardless of how thin the triggering stock was. Players can post a bulk_procurement contract too β see Delivery contract below.
Gather N units of a commodity from anywhere and deliver to one station. No fixed origin β the acceptor sources however they like.
ARIA surfaces the contract board on first dock at any station with available contracts, and narrates contract-acceptance moments β see ../gameplay/aria-companion.md#aria-narration-hooks-event-catalog entries P-F7 + P-I1.
Fulfillment is via a rented station locker at the destination, not a direct per-delivery payout. The acceptor (acceptor_player_id, single-acceptor at launch) rents a storage locker at the contract's destination station and deposits the commodity across as many trips as it takes. The contract stays accepted through every installment β the deposit that brings the locker's accumulated total to the contract's full quantity auto-completes the contract in the same transaction, paying the acceptor the full payment in one settlement. The 2% acceptance fee is debited once on the initial accept, regardless of how many installments fill the locker.
Walking away β by explicit abandon or by letting the deadline lapse mid-fill β charges the acceptor a penalty computed from the locker's fill at that moment: payment Γ (undelivered units Γ· quantity). A near-complete locker costs far less to walk away from than an empty one. The penalty is a destroyed sink (not paid to the issuer), reduced by any insurance offset the acceptor holds. The acceptor keeps whatever was already deposited β the locker converts to claimable rather than forfeiting its contents (that forfeiture is a separate consequence, triggered only by failing to pay the locker's own rent β see storage-lockers.md Β§ Missing the deadline). The issuer's escrow refunds in full either way: immediately on an explicit abandon, or after the standard 48-hour dispute window on a deadline lapse (see Escrow handling). This terminal walk-away path also closes the historical SK13 re-delivery exploit (ADR-0049 SK13 / ADR-0062 E-V1) β there is no reject-then-re-accept delivery loop under the locker model, so a monotonic fulfillment_count counter is not required.
π Multi-acceptor partial fulfillment (where two or more players each fulfill a slice of N) is reserved for a future iteration. The escrow split, fee proration, and per-acceptor walk-away semantics are non-trivial and aren't load-bearing for launch β single-acceptor with a locker covers the common case (a player commits to the haul, sources flexibly across trips, and either completes it or walks away, paying the dynamic penalty above).
Express delivery¶
High-priority cargo with a tight deadline. Payment is higher than standard cargo delivery, plus an early-arrival bonus (see Rewards). Express contracts use a stricter penalty on failure.
Hazardous transport¶
Illegal or contraband cargo routed via black-market channels. Issued by criminal NPCs at black-market terminals. See black-market.md for the goods list and detection mechanics. Hazardous transport pays significantly more, applies a faction penalty if completed, and exposes the carrier to scans during transit.
Refugee transport¶
Move colonists from one region to another. Interregional only β single-region jobs use the standard colonist trade flow described in planets/colonization.md. Refugee contracts are gated behind a passenger-rated ship and pay per surviving colonist on arrival.
Player-issued contracts¶
Players post offers visible at one or more stations they control or have docking rights at. The poster pays the contract value into escrow at posting time.
Delivery contract¶
π§ Partial. Shipped: post_player_contract (contract_service.py:1778) β escrow debit, validation, and the 10-per-region posting cap. The player has cargo they need moved to a destination they can't easily reach. The player nominates origin, destination, commodity, quantity, deadline, and offered payment. Other players accept and execute the run.
A player can post a bulk_procurement contract through this same function instead of a cargo_delivery β same escrow math (payment + insurance_pool_reserve), same posting cap. Fulfillment, the walk-away penalty, and NPC-generation parity are described under Bulk procurement above; nothing about the player-issued path changes that model.
Acquisition bounty¶
π The player offers credits for any cargo of type X delivered to port Y by the deadline. Multiple acceptors can fulfill partial quantities until the bounty is met or the deadline lapses.
Escort contract¶
π The player pays another player to fly with them through a sequence of sectors (combat support during traversal). Escort contracts complete when the protected player arrives at the destination intact, or when a defined number of hostile encounters are survived. Cross-link: ships.md.
Escrow handling¶
Player-issued contracts use server-held escrow. NPC contracts draw from the NPC's credit pool (no lock; treated as infinite).
| Phase | Trigger | Issuer | Acceptor | Escrow row |
|---|---|---|---|---|
| Post (player) π§ | POST /contracts |
β(payment + insurance_pool) | β | escrow_amount := payment + insurance_pool, state := held |
| Accept π§ | POST /contracts/{id}/accept |
β | β(payment Γ 0.02) acceptance fee | unchanged |
| Insure β | POST /contracts/{id}/insure |
β | βpremium | insurance_premium_paid := premium β contract_insurance.insure / _guarded_insure (contract_insurance.py) |
| Deposit (bulk) β | POST /storage/lockers/{locker_id}/deposit |
β | β(units, from ship cargo, into the locker) | unchanged β the contract stays accepted; the locker's own deposit rows track cumulative fill |
| Complete π§ (cargo_delivery) / β (bulk) | POST /contracts/{id}/complete (cargo_delivery); the completing deposit call above (bulk β same transaction, no separate request) |
β(early bonus, if any) | +(payment + bonus β premium) | state := released |
| Expired (cargo_delivery) | deadline lapse (accepted contract) | +(escrow refund in full, after the 48h dispute window) | β | state := refunding, per the repo-root DECISIONS.md entry contract-escrow-deadline-refund. Acceptor forfeits acceptance fee + reserved cargo; insured acceptor: insurer pays penalty |
| Expired / abandoned (bulk) β | deadline lapse, or acceptor abandon (POST /contracts/{id}/abandon) |
+(escrow refund in full β immediate on abandon, deferred to the 48h dispute window on deadline lapse) | β(payment Γ undelivered Γ· quantity, from the locker's fill at that moment; insurance offsets the cash debit) | state := refunding. The locker converts to claimable β the acceptor keeps whatever was already deposited |
| Cancel pre-accept π§ | POST /contracts/{id}/cancel |
+(escrow Γ 99%) | β | state := refunding. 1% posting-fee sink |
| Cancel post-accept (mutual) π§ | same | +(escrow β accept_fee β 10% kill-fee) | 0 | state := refunding. Kill-fee β escrow sink |
| Disputed β | POST /contracts/{id}/dispute |
β | β | state := disputed. Escrow frozen pending arbitration β contract_dispute.file_dispute / _guarded_file_dispute (contract_dispute.py) |
Escrow is never directly transferable between players β all settlement runs through the contract row.
The Cancel pre-accept and Cancel post-accept actions above are the first venue for the platform-wide inline-confirm UX standard: any credit consequence over β‘1,000 (which most contract cancellations cross, via escrow refund or kill-fee) confirms inline rather than via modal. See ../../OPERATIONS/ui-flows.md Β§ 3.6 Confirmation standard for the pattern definition.
Bulk-procurement walk-away example¶
Player A posts a bulk_procurement for 1,000 units of ore, payment 1,000 cr β escrow 1,000. Player B accepts, debited 20 cr (2% acceptance fee). B rents a locker at the destination and deposits 800 of the 1,000 units across two trips, then fails to finish β either by explicitly abandoning or by letting the deadline lapse with the locker still at 800/1,000.
Penalty = 1,000 Γ (200 Γ· 1,000) = 200 cr, debited from B's wallet as a destroyed sink (not paid to A). B keeps the 800 deposited units β the locker converts to claimable rather than forfeiting its contents. A's 1,000 cr escrow refunds in full: immediately if B explicitly abandoned, or after the 48-hour dispute window if the deadline lapsed instead. Had B deposited nothing before failing, the penalty would be the full 1,000 cr; had B completed all 1,000 units, B would instead be paid the full 1,000 cr in one settlement on the completing deposit. The 20 cr acceptance fee is never refunded either way.
Contract lifecycle¶
| Status | Trigger | Effect |
|---|---|---|
| posted | Issuer creates the contract | Visible on contract board(s); awaiting acceptance |
| accepted | A player accepts | Acceptance fee charged; cargo (if delivery) reserved at origin; clock starts |
| in_transit | Cargo loaded into the acceptor's ship | Deadline timer running; player carries the load |
| completed | Cargo delivered at destination station before deadline | Payment + reputation reward issued; insurance refunded if held |
| expired | Deadline expires or cargo lost in transit | Penalty applied; reputation penalty; escrow paid to issuer |
| cancelled | Cancellation before acceptance, or by mutual agreement after | Partial penalty (kill-fee) β see Anti-griefing |
Status transitions are one-way except posted β cancelled. Once accepted, the only exits are completed, expired, or cancelled (with kill-fee). This table's terminal-state name follows the canonical expired naming used by the schema (see Contract schema) and contract.py's state-machine docstring.
posted ββacceptβββΆ accepted ββloadβββΆ in_transit ββdeliverβββΆ completed
β β β
β β βββdeadline_expiredβββΆ expired
β β βββcargo_destroyedββββΆ expired
β βββmutual cancelβββΆ cancelled (kill-fee)
βββissuer withdrawβββΆ cancelled (no fee)
API surface¶
β
Shipped endpoints under /api/v1/contracts/ β every route in the table below is live in routes/contracts.py (confirmed by name/verb: /board, /mine, /{id}, POST /contracts, /{id}/accept, /{id}/complete, /{id}/abandon, /{id}/cancel, matching the header status line above). Bulk-procurement fulfillment itself happens through the separate /api/v1/storage/lockers/ surface β see storage-lockers.md Β§ Renting a locker and Β§ Depositing cargo β rather than a /contracts/{id}/... endpoint of its own.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/contracts/board?station_id=... |
List contracts visible at a station |
GET |
/api/v1/contracts/mine |
List the caller's posted + accepted contracts |
GET |
/api/v1/contracts/{id} |
Detail for a single contract |
POST |
/api/v1/contracts |
Post a new player-issued contract (cargo_delivery or bulk_procurement, escrow check) |
POST |
/api/v1/contracts/{id}/accept |
Accept a posted contract (charges acceptance fee) |
POST |
/api/v1/contracts/{id}/insure |
Buy insurance on an accepted contract |
POST |
/api/v1/contracts/{id}/complete |
Mark delivered (server verifies cargo at destination) β for bulk_procurement this fires automatically from the completing locker deposit rather than a direct call |
POST |
/api/v1/contracts/{id}/cancel |
Cancel β kill-fee applied per state |
POST |
/api/v1/contracts/{id}/abandon |
Walk away from an accepted contract β for bulk_procurement, charges the dynamic locker-fill penalty and converts the locker to claimable |
POST |
/api/v1/contracts/{id}/dispute |
File a dispute on a failed contract |
POST |
/api/v1/admin/contracts/{id}/resolve-dispute |
Admin: issue final dispute ruling |
WebSocket events fire on every status transition for the issuer, acceptor, and any subscribed faction-management clients.
Request and response shapes¶
POST /api/v1/contracts¶
// request
{
"contract_type": "bulk_procurement",
"destination_station_id": "stat-uuid",
"commodity_type": "ore",
"quantity": 5000,
"payment": 500,
"deadline": "2026-05-08T14:00:00Z",
"posting_stations": ["stat-uuid-1", "stat-uuid-2"],
"insurance_pool_reserve": 50
}
// response 201
{
"id": "contract-uuid",
"status": "posted",
"escrow_amount": 550,
"escrow_state": "held",
"posted_at": "2026-05-04T12:00:00Z",
"acceptance_fee_pct": 2.0
}
Validation: caller has credits β₯ payment + insurance_pool_reserve; destination exists and not offline; deadline β₯ 1 hour out; active postings by caller < 10 per region; caller not blocklisted.
POST /api/v1/contracts/{id}/accept¶
// response 200
{
"id": "contract-uuid",
"status": "accepted",
"acceptor_player_id": "player-uuid",
"accepted_at": "2026-05-04T12:05:00Z",
"acceptance_fee_charged": 10,
"remaining_balance": 490,
"deadline": "2026-05-08T14:00:00Z"
}
Validation: wallet β₯ acceptance_fee; status is posted; caller β issuer; not blocklisted.
Bulk-procurement fulfillment¶
Bulk-procurement deliveries go through the storage-locker API, not a /contracts/{id}/... endpoint β see storage-lockers.md Β§ Renting a locker and Β§ Depositing cargo for the request/response shapes. The deposit that brings the locker to the contract's full quantity completes the contract automatically, in the same call.
POST /api/v1/contracts/{id}/dispute¶
// request
{ "reason": "Cargo manifest shows delivery occurred", "evidence_snapshot": "manifest-url" }
// response 202
{
"status": "disputed",
"dispute_filed_at": "2026-05-05T09:00:00Z",
"escrow_frozen": 500,
"estimated_resolution": "2026-05-06T09:00:00Z",
"arbitration_tier": "automated"
}
Rewards¶
Base payment is a function of:
payment = base_rate
Γ commodity_value(commodity_type, quantity)
Γ distance_factor(origin, destination)
Γ urgency_factor(deadline_tightness)
Γ contract_type_multiplier
commodity_value derives from the live midpoint price (see trading.md Β§ Pricing). distance_factor uses warp-jump count between origin and destination. urgency_factor rises as deadline tightness increases (express deliveries pay roughly 1.5β2.0Γ their non-express equivalents).
Bonuses¶
- β
Early-completion bonus β shipped for
express_delivery. Up to +25% of payment if delivered with greater than 50% of the time window remaining, linear scale between 0β25% above the 50% threshold β_compute_early_arrival_bonus(contract_service.py:327-364), wired intocomplete()atcontract_service.py:453and returned asearly_arrival_bonusin the completion response. Gated tocontract_type == EXPRESS_DELIVERYonly; every other contract type still getspaymentwith no bonus. - Reputation reward β completion grants reputation with the issuing faction (NPC contracts) or a small mutual reputation bump between poster and acceptor (player contracts).
- Insurance refund β if the player paid an insurance premium and completed cleanly, the unused premium is not refunded β see Risk & insurance for why.
Penalties¶
On failure (deadline expired or cargo lost):
- Forfeit reserved cargo (if a delivery contract).
- Reputation penalty with the issuing faction (NPC) or the posting player.
- π Design-only β cooldown on contract eligibility from that issuer (default 24 game-hours).
services/gameserver/src/services/contract_dispute.py's own module docstring: "no cooldown/ban model exists anywhere in this codebase (grepped)." - Acceptance fee is not refunded.
- Penalty credits are debited from the acceptor's account; if insufficient, the deficit is recorded as a debt that must be cleared before posting new contracts.
Worked example¶
π A Class-2 station posts a cargo_delivery for 150 units of organics to a Class-3 station 8 jumps away, deadline 90 minutes:
base_rate = 1.0
commodity_value = 150 Γ midpoint(organics) β 150 Γ 16.5 = 2,475 cr
distance_factor = 1.0 + 0.05 Γ 8 = 1.40
urgency_factor (90 min) = 1.10
contract_type_multiplier = 1.0 (standard cargo_delivery)
payment β 2,475 Γ 1.40 Γ 1.10 β 3,810 cr
acceptance_fee β 76 cr (2%, refundable)
early-completion bonus cap β +953 cr (25%) if delivered with > 45 min left
penalty on failure = forfeit reserved cargo + 1Γ payment debit
Numbers are illustrative; the actual coefficients live in contract_service.py config.
Risk & insurance¶
β
Shipped β optional contract insurance is a per-contract add-on bought after accept via POST /contracts/{id}/insure (contract_insurance.insure / _guarded_insure; premiums INSURANCE_PREMIUM_PCT BASIC/STANDARD/HAZARD = 2% / 5% / 10% of contract value β the premium percentages were ratified by ADR-0093 items 7/32, folded from ADR-0093, re-verified 2026-08-07). On expiry/failure, coverage is a penalty offset (not a positive credit payout): apply_claim_offset draws from insurance_pool_reserve after the deductible ladder (BASIC 5% / STANDARD 10% / HAZARD 15%), so the acceptor owes less rather than receiving credits (contract_insurance.py).
| Coverage tier | Premium | Covers |
|---|---|---|
| Basic | 2% of contract commodity value | Ship loss in low-security space during in-transit |
| Standard | 5% | Ship loss anywhere + 50% cargo replacement |
| Hazard | 10% | Ship loss anywhere + 100% cargo replacement + extended deadline grace |
The tier table's narrative coverage bullets (low-sec-only / cargo-replacement / deadline grace) remain descriptive of the product intent; the live claim path today is the penalty-offset on expiry/abandon sweeps above. Insurance does not cover wilful abandonment as a free walk-away (abandon still charges the dynamic/static penalty, then offsets it), and insurance_claim_filed is unused schema (no separate player-filed claim endpoint). Deductible model parallels the ship insurance system β same three-rung deductible ladder (5% / 10% / 15%), per ADR-0061, but the premium percentages differ between the two systems and the top rung is named differently: contract insurance's Hazard tier (10% premium) maps to the identical 15% deductible slot as ship insurance's Premium tier (22% premium) β same deductible, different premium base and different name. Do not assume the two tier tables share a premium scale just because they share a deductible ladder.
Reputation effects¶
π§ Partial. reputation_penalty is now read on completion for hazardous_transport β contract_service.py:517-527 applies a Federation apply_faction_rep_delta penalty via the first real reader of that column (contract_dispute.py's own module docstring previously called it write-only; that note is now stale for this one path). reputation_reward on NPC complete is β
Shipped on tip (88883100 / PR #629 MERGED β tip 46bce720): first real reader at contract_service.py:530-554 via apply_faction_rep_delta(..., reason="npc_contract_reputation_reward") when issuer_type=NPC and a truthy row value is present. NPC generator freeze of non-null reputation_reward when faction_id resolves is β
Shipped on tip (6ef60c0c / PR #631 MERGED β tip 46bce720): NPC_CONTRACT_FACTION_REP_REWARD = abs(HAZARDOUS_TRANSPORT_FEDERATION_REP_PENALTY) (30) β magnitude is the existing [NO-CANON] penalty-symmetry pin already used by the hazardous Federation penalty writer, not a ratified FEATURES balance number. abandon() still does not read either column. The mechanics below describe the target behavior beyond those completion hooks.
Completing NPC contracts boosts faction standing with the issuing faction. Failing damages it. The reward/penalty values are stored on the contract row at posting time, so they're stable across the lifecycle even if the faction's general standing thresholds shift.
π Design-only. Persistent failure β 3 or more failures in a row with the same faction β bans the player from accepting that faction's contracts for a cooldown (default 7 game-days). No cooldown/ban model exists anywhere in this codebase today (contract_dispute.py module docstring, grepped) β this describes target behavior, not shipped mechanics. The cooldown clears by waiting it out, or sooner by accruing positive faction rep through emergent activity (defending a faction sector, trade volume at faction ports, etc. β see ../gameplay/factions-and-teams.md#reputation-triggers).
Player-to-player contracts also feed a lightweight trader-reputation stat (separate from faction reputation). Persistent contract reliability on the trader side becomes a public visible badge on the player's profile.
Anti-griefing¶
π§ Partial. Rules to keep contracts from being weaponised:
- π Contracts cannot be accepted from players the acceptor has active hostility with (negative direct-relationship reputation between the two parties).
- β An acceptance fee (small, fixed percentage of contract value, default 2%) is charged at accept time and is not refunded on completion, failure, or cancellation β shipped, a pure friction sink. This discourages frivolous picks that lock the contract for the deadline window without intent to complete.
- The issuer cannot cancel a contract after it has been accepted without paying a kill-fee equal to the acceptance fee plus 10% of the contract value, paid to the acceptor.
- A player cannot post a contract whose escrow they cannot afford. Escrow is held server-side at posting time.
- π§ Contract boards rate-limit per-player postings to prevent spam (default 10 active postings per player per region) β shipped.
- π A posting-side player blocklist β
_is_player_blocklisted(contract_service.py:254) is a documented no-op seam, always returningFalse, until a real blocklist model exists to wire it to.
Disputes¶
β
Shipped β acceptor-only filing via POST /contracts/{id}/dispute (contract_dispute.file_dispute / _guarded_file_dispute). Verified window: DISPUTE_FILING_WINDOW_HOURS = 48 in contract_dispute.py (wall-clock hours from contract.deadline as the failure-timestamp proxy; module pins wall-clock to match sibling contract timed checks). Filing flips status/escrow_state β disputed and freezes held escrow. π Reputation-penalty pause remains Design-only in effect β _is_reputation_penalty_paused is a real gate. Completion-path reputation is no longer wholly unread (see Reputation effects for the hazardous_transport penalty + LEG-122 NPC reputation_reward reader + LEG-125 generator freeze, both tip-merged); abandon() and the dispute path still do not apply those columns.
Resolution runs in two tiers:
Tier 1: automated arbitration (within 1 game-hour)¶
Tier-1 runs synchronously inside file_dispute (milliseconds, not a separate sweep). Of the three canon cases:
- Cargo manifest match β π Design-only seam (
_tier1_cargo_manifest_matchalways returnsFalse; no per-contract delivery-event log exists yet). - Destination unreachable β β
Shipped proxy: current destination
Station.status == ABANDONED(_tier1_destination_unreachable) β cancels, refunds acceptance fee; no historical offline/destroyed/inaccessible snapshot. - Issuer unilateral cancellation β π Design-only seam (
_tier1_issuer_unilateral_cancellationalways returnsFalse; cancel-after-accept is not a live transition that leaves a disputable EXPIRED row).
Unresolvable cases escalate to Tier 2 (escalated_to_admin per ADR-0062 E-I3 criteria).
Tier 2: admin review (within 24 game-hours)¶
β
Admin reviews dispute_notes and issues a ruling via POST /admin/contracts/{id}/resolve-dispute (contract_dispute.resolve_dispute, routed in admin_contract_disputes.py):
| Outcome | Settlement | Reputation | Cooldown |
|---|---|---|---|
full_payout |
Escrow β acceptor in full | Reward applied retroactively | None |
partial_payout |
Pro-rata: (delivered / expected) Γ payment β acceptor; remainder β issuer | Proportional reward | None |
refund (acceptor non-negligent) |
Escrow β issuer; acceptance fee β acceptor | Penalty β issuer (acceptor absolved) | Acceptor: 24h cooldown on that issuer |
penalty (acceptor fault or fabrication) |
Escrow β issuer; acceptance fee forfeit | Acceptor penalty doubled (orig + β50) | Acceptor: 72h cooldown; account flag on repeat |
split (shared responsibility) |
Escrow split 50/50; acceptance fee refunded | Half reward + half penalty | Acceptor: 24h cooldown |
A player who files 2+ false disputes in 30 days is flagged for manual review (potential contract-system suspension).
Black-market contracts¶
Hazardous-transport NPC contracts and certain illegal-goods player contracts route through the black-market system. They pay 2β4Γ standard rates, carry detection risk during transit, and apply a faction penalty on completion (the law-side faction loses standing). See black-market.md for the full mechanics, terminal locations, and detection model.
Player-facing affordances¶
- β
Contract board venue β
ContractBoardVenue.tsxmounted fromSpaceDockInterface.tsxandGameDashboard.tsx; board / mine / post tabs plus accept / complete / abandon / insure / dispute / cancel / post viacontractsAPI(origin/feat46bce720). - β
Locker multi-trip bulk procurement β acceptor fulfills
bulk_procurementby renting a station locker and depositing across trips (storageAPI+ locker deposit routes); the deposit that reaches full quantity auto-completes the contract; walk-away / abandon charges the dynamic locker-fill penalty (see Bulk procurement). - π
refugee_transport,acquisition_bounty,escortcontract types β enum values exist; no NPC generator or dedicated player UI on tip; do not conclude these flows are live. - π Board reputation-gated hiding + issuer blocklist β design-only on
get_contract_boardtoday (see Contract boards).
Source map¶
| Concern | Path | Status |
|---|---|---|
| Contract model | services/gameserver/src/models/contract.py |
β Shipped |
| Contract service | services/gameserver/src/services/contract_service.py (2,131 lines) |
β Shipped |
| NPC contract generator | services/gameserver/src/services/contract_generator.py (859 lines) |
β
Shipped (cargo_delivery, express_delivery, hazardous_transport, bulk_procurement; remaining three types Design-only) |
| Expiry-sweep scheduler | services/gameserver/src/services/scheduler/contract_sweeps.py |
β Shipped |
| Contract API routes | services/gameserver/src/api/routes/contracts.py (10 routes) |
β Shipped |
| Contract insurance | services/gameserver/src/services/contract_insurance.py (insure, _guarded_insure, apply_claim_offset) |
β Shipped |
| Contract disputes | services/gameserver/src/services/contract_dispute.py (file_dispute, resolve_dispute; window DISPUTE_FILING_WINDOW_HOURS = 48) |
β Shipped |
| Admin dispute routes | services/gameserver/src/api/routes/admin_contract_disputes.py |
β Shipped |
| Admin UI β dispute arbitration | services/admin-ui/src/components/pages/ContractDisputeArbitration.tsx (/contract-disputes) |
β Shipped |
| Contract board UI | services/player-client/src/components/spacedock/ContractBoardVenue.tsx (wired via SpaceDockInterface.tsx + GameDashboard.tsx; contractsAPI) |
β Shipped |
| Contract board UI tests | services/player-client/src/components/spacedock/ContractBoardVenue.test.tsx (+ bulkPost.test.tsx for posting path) |
β Shipped |
Status¶
π§ Partial. See the header above for the full shipped/unbuilt breakdown. Remaining build order:
- NPC generators for the remaining three contract types (
refugee_transport,acquisition_bounty,escort) β the enum values exist;cargo_delivery,express_delivery,hazardous_transport, andbulk_procurementall generate today. - Multi-acceptor bulk-procurement partial fulfillment; wire the two Tier-1 dispute seams (cargo-manifest log + issuer-unilateral-cancel history) and reputation application on dispute/complete β insurance purchase/claim-offset and dispute filing/Tier-2 resolve are already shipped (see markers above).
- Anti-griefing hostility-block and posting blocklist (currently a permanent no-op stub, see
contract_service.py:254).
Related¶
- ./trading.md β main trading flow; contract payments use the same pricing midpoints.
- ./black-market.md β illegal-goods contracts, detection model.
- ../gameplay/factions-and-teams.md#reputation-triggers β emergent faction-rep system; trade-volume rep applies to contract completion through the same channel as direct port trades.
- ../gameplay/factions-and-teams.md β faction-issued contracts impact standing.
- ../gameplay/ships.md β escort contracts and ship-insurance overlap.