Trade Contracts¶
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.
📐 Design-only — no Contract model, service, or routes are committed yet. This page is the target spec.
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.
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 NPC identifier (npc) |
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) |
One of the canonical seven; 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 | Bulk procurement: cumulative units delivered to date. Monotonic counter per ADR-0049 — once N units are credited, remaining quota is fixed at (quantity − N) regardless of acceptor walk-away. Subsequent acceptors deliver toward the remainder; they cannot re-deliver previously-credited cargo. |
partial_fulfilled_payout |
Decimal(19,2) | not null, default 0 | Pro-rata credits paid out for partial fulfillment |
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 |
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
├─ acceptor walks (bulk_procurement) → posted (partial banked, fee forfeit)
└─ 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)
For bulk procurement, partial_fulfilled bridges accepted → in_progress to track multi-acceptor delivery (📐 reserved; single-acceptor at launch).
Contract boards¶
📐 Each station exposes a contract board — a per-station list of currently-posted contracts. A board is the union of:
- NPC contracts spawned by the generator at this station.
- Player-posted contracts whose
posting_stationsset includes this station. - Bounty-style acquisition contracts where this station is the destination.
Boards refresh on the same tick cadence as market prices (see trading.md § Pricing). Players see only contracts they're eligible for: faction-gated NPC contracts hide below the minimum reputation threshold; player contracts hide for parties on the issuer's blocklist.
A station's board capacity is bounded — a Class-0 trade hub posts more contracts than a Class-8 black hole. 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¶
📐 NPC contracts spawn dynamically on each station's contract board (Station.contract_board). The generator (contract_generator.py) seeds new entries on a tick and prunes expired ones. 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¶
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.
Gather N units of a commodity from anywhere and deliver to one station. No fixed origin — the player sources however they like. Single-acceptor at launch: one player accepts the contract (acceptor_player_id set), and only that player's deliveries credit toward fulfillment until the contract closes. Partial deliveries by the acceptor credit pro-rata at the per-unit rate up to the deadline; the acceptor may walk away at any time before the deadline, at which point the contract reverts to "open" with the prior partial delivery banked toward the next acceptor's quota. The 2% acceptance fee is debited once on the initial accept, regardless of how much is eventually delivered.
📐 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 walk-away covers the common case (a player commits to the haul, sources flexibly, and either completes or releases the contract for someone else to pick up).
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¶
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.
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 |
| Partial deliver (bulk) | POST /contracts/{id}/deliver |
— | +pro_rata payout | partial_fulfilled_amount += N |
| Complete | POST /contracts/{id}/complete |
−(early bonus, if any) | +(payment + bonus − premium) | state := released |
| Expired / failed | deadline lapse | — | — | state := released, acceptor forfeits acceptance fee + reserved cargo; insured acceptor: insurer pays penalty |
| 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 |
Escrow is never directly transferable between players — all settlement runs through the contract row.
Bulk-procurement walk-away example¶
Player A posts a bulk_procurement for 5,000 units, payment 500 cr → escrow 500. Player B accepts, debited 10 cr (2%). B delivers 2,500 units → wallet +250 cr (pro-rata), partial_fulfilled_amount = 2,500, contract remains posted (B walked). Player C accepts, debited a fresh 10 cr fee (no fee-stacking). C delivers the remaining 2,500 → wallet +250 cr, contract → completed. Acceptance fees forfeit by walked acceptors are sunk.
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 |
| failed | 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, failed, or cancelled (with kill-fee).
posted ──accept──▶ accepted ──load──▶ in_transit ──deliver──▶ completed
│ │ │
│ │ └──deadline_expired──▶ failed
│ │ └──cargo_destroyed───▶ failed
│ └──mutual cancel──▶ cancelled (kill-fee)
└──issuer withdraw──▶ cancelled (no fee)
API surface¶
📐 Target endpoints under /api/v1/contracts/:
| 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 (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}/deliver |
Record a partial bulk_procurement delivery |
POST |
/api/v1/contracts/{id}/complete |
Mark delivered (server verifies cargo at destination) |
POST |
/api/v1/contracts/{id}/cancel |
Cancel — kill-fee applied per state |
POST |
/api/v1/contracts/{id}/dispute |
File a dispute on a failed contract |
POST |
/api/v1/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.
POST /api/v1/contracts/{id}/deliver (bulk only)¶
// request
{ "units": 2500, "location": "stat-uuid" }
// response 200
{
"units_delivered": 2500,
"pro_rata_payout": 250,
"cumulative_fulfilled": 2500,
"remaining_quota": 2500,
"payout_issued_to": "player-uuid"
}
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 — 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.
- 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.
- Cooldown on contract eligibility from that issuer (default 24 game-hours).
- 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¶
📐 Optional contract insurance is a per-contract add-on, purchased at acceptance time:
| Coverage tier | Premium | Covers |
|---|---|---|
| Basic | 5% of contract value | Ship loss in low-security space during in-transit |
| Standard | 10% | Ship loss anywhere + 50% cargo replacement |
| Hazard | 15% | Ship loss anywhere + 100% cargo replacement + extended deadline grace |
If the insured ship is destroyed during in_transit, the policy pays out the contract penalty for the player, less a deductible. Insurance vs deductible model parallels the ship insurance system. Insurance does not cover wilful abandonment, deadline-only failures (the cargo arrived but late), or smuggling losses on hazardous-transport contracts.
Reputation effects¶
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.
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). 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¶
📐 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 refunded on completion. 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).
Disputes¶
📐 If a player believes a contract was completed but the system marked it failed (e.g. the destination station went offline mid-delivery), the player can file a dispute within 48 game-hours of the failure timestamp. Only the acceptor can file. Filing flips escrow_state := disputed and pauses the reputation penalty.
Resolution runs in two tiers:
Tier 1: automated arbitration (within 1 game-hour)¶
The server queries authoritative logs and resolves three common cases automatically:
- Cargo manifest match —
Cargo.logsshows the expected commodity/quantity arrived at the destination at delivery time ± 5 minutes → statuscompleted,escrow_state := released, payout issued, reputation reward applied. - Destination unreachable —
Station.statuswasoffline/destroyed/inaccessibleat the failure moment → statuscancelled, acceptance fee refunded (full, no kill-fee), reputation penalty forgiven. - Issuer unilateral cancellation — contract history shows issuer cancelled after acceptor accepted → contract voided; kill-fee owed by issuer to acceptor; reputation penalty reversed.
Unresolvable cases (no manifest, no station event, no cancellation) escalate to Tier 2.
Tier 2: admin review (within 24 game-hours)¶
Admin reviews dispute_notes, manifests, and player comms, then issues a ruling via POST /contracts/{id}/resolve-dispute:
| 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.
Source map¶
📐 None of the following exist yet — these are target paths for the implementation:
| Concern | Target path |
|---|---|
| Contract model | services/gameserver/src/models/contract.py |
| Contract service | services/gameserver/src/services/contract_service.py |
| Contract API routes | services/gameserver/src/api/routes/contracts.py |
| NPC contract generator | services/gameserver/src/services/contract_generator.py |
| Insurance hooks | extension to services/gameserver/src/services/insurance_service.py |
| Contract board UI | new admin/player-UI surface (TBD) |
Status¶
📐 Design-only. The entire trade-contract system is unimplemented. No Contract model, no contract service, no API routes, no UI. This spec defines the target shape; implementation order should be:
Contractmodel + database migration.- NPC
cargo_deliverygenerator (simplest type) — proves the lifecycle end-to-end. - API routes (
/api/v1/contracts/...) — list, accept, complete, abandon, post. - Player-issued posting flow + escrow handling.
- Remaining contract types (bulk, express, hazardous, refugee, escort, acquisition bounty).
- Insurance integration.
- Anti-griefing limits and reputation cooldowns.
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.