Turn any protoAgent into an autonomous, self-improving SpaceTraders fleet commander. Play the live SpaceTraders v2 API — a persistent, shared galactic economy — and grow your operator's treasury from a fresh start toward 1,000,000 credits, hands-off: contracts seed it, trade compounds it, scouting informs it, guards protect it.
This is a full-bundle plugin (ADR 0027): one directory contributes the whole extension set, all auto-discovered.
Just want to run it? protoTrader-in-space is a ready-to-run reference agent that consumes this plugin —
git clone,plugin sync, drop in a token,python -m server, and watch an autonomous fleet commander play. Spin-up-and-go.
| Contribution | What |
|---|---|
| Tools (38) | register, agent/fleet status, fuel-aware st_travel, markets, st_trade_routes, contracts, mining, buy/sell, shipyard, the background growth engine (st_autopilot_start/stop/status), and the control surface — st_report (telemetry), st_strategy (doctrine), st_tune (knobs), st_assign (per-ship pins) |
| Subagents (5) | navigator, trader, miner, fleet-commander, strategist (the OODA brain) |
| Workflows | procurement-run, mining-run, fleet-bootstrap (workflows/) |
| Skills | manage-the-fleet (the OODA strategist loop), maximize-credits-per-hour, play-spacetraders, run-a-procurement-contract (skills/) |
| Console view (ADR 0026) | a Fleet rail dashboard — a dense header strip (treasury · autopilot · strategy · ships · net worth), ships + live ETAs, contracts, the jump-gate construction card (per-material progress bars + the funding rung's armed state; connections once built), the tripwire panel (the live st-* watch suite), the decision log tail with relative timestamps, the galaxy leaderboard standing, the wipe countdown, and the agent's learned routes |
| Knowledge | LESSONS.md + seed_kb.py (durable lessons) + epoch-stamped trade-route memory (routes.py — learned routes recall only within the CURRENT universe; a wiped epoch's markets can't mislead) + self-written lessons (lessons.py — every lesson_every-th window the fleet distills its telemetry into one durable lesson via sdk.complete) |
| Events (ADR 0039) | the engine broadcasts spacetraders.* bus topics — the dashboard live-refreshes on them, the rail icon lights a dot, and any plugin can subscribe without importing this one. See Events |
| Chat command | /spacetraders — instant fleet status (credits · ships · engine · recent log) with no model turn; a user-only control command the agent can't invoke |
| Tripwires (ADR 0067) | eight always-on WATCHes ground-truthed against the live game — universe reset, treasury drawdown (measures the liquid treasury — credits + cargo valued at its realizable in-system sell price, hulls excluded — under half its high-water mark, over the whole paged fleet; counting cargo makes a deployed trade fleet's credits↔cargo working-capital cycle treasury-neutral, so a heavily-deployed fleet no longer false-fires when working capital sits in in-flight cargo, while a genuine loss — bought-high/sold-low, unsellable stranded cargo, a fuel/refit drain — still trips. Cargo is liquidated into the observed market depth: each good's pooled position clears the best buyers first (up to each one's per-transaction tradeVolume) and values the remainder at the cheapest observed price. A single healthy buyer absorbs the whole position at its price (so a normal multi-hauler or gate-material hold is valued in full — no re-fired false positive), but a crashed primary market can't hide behind a thin waypoint that still posts high: the hoard overflows the thin volume and the remainder drags to the crashed price, so the loss surfaces. The book prefers a fresh observation over a stale one: a wide 1-day window keeps recently-traded cargo valued through a multi-hour engine outage — without it every price ages out mid-outage and the treasury collapses back to raw credits — while a fresh 1-hour re-observation of a market collapse (the fleet's own oversupply, or another agent dumping a shared-universe market) supersedes a stale high at another waypoint so a real loss can't hide behind an un-re-observed price; a good with no in-system buyer values at 0. Residual (honest): a good held across a crash never re-observed inside the 1-day window still books at its stale high — a real but bounded blind spot while the engine RUNS (not engine-stopped's case), self-healing the moment any ship re-visits the market or the row ages out. Hulls are excluded because a SpaceTraders ship has no resale market, so their book value would desensitize the ratio and mask a real crash), contract deadline, net-worth flatline, trade opportunity, plus three ops tripwires: engine stopped (the autopilot sat STOPPED past a grace period with a token configured — flatline only covers "running but earning nothing"), API errors (a bounded client-side tally of non-429 error responses by endpoint shape; one endpoint failing the same way ≥10× in 30 min reads as a request-shape bug — the 157×422 ?traits=FUEL_STATION class — and the trip detail quotes the last concrete request, query string included), and idle capital (credits sat over idle_capital_floor for the whole lookback with no ship purchase and no construction delivery — the 2.4M incident where reinvestment silently wedged). A trip wakes the agent in the Activity thread with a playbook prompt; armed at host start + engine start (watches don't survive a restart — the suite must be up while the engine is down), self-healing via window_closed |
| Goal ladder | 12 goal verifiers (credits, fleet_size, cargo_capacity, net_worth, charted_count, + the tripwire checks); achieving a rung enqueues a follow-up turn proposing the next goal |
| A2A card skills | typed fleet_report / quote_route (output_schema + result_mime, finalizer-enforced) — another agent's delegate_to gets parseable JSON, not prose. The Syndicate multi-agent runbook builds on them |
| Exploration (ADR 0070) | st_explore launches a detached background campaign: an explorer subagent charts the system's uncharted waypoints (nearest-first sweep) and reports back through the host's background-results pipeline — report card, KB index, Activity-thread nudge. Plus st_chart/st_scan_*/st_jump/st_warp and construction supply (st_construction/st_supply_construction — the jump-gate community goal) |
Architecture: the fleet runs as two loops — a deterministic engine (the muscle)
steered by an agentic OODA strategist (the brain) between windows. See
docs/two-loop-fleet.md.
Roadmap: docs/sdk-round2.md — full plugin-SDK seam
parity: live events (v1.7), watch tripwires + the goal ladder (v1.8), wipe-scoped
knowledge (v1.9), background exploration campaigns (v2.0), the A2A Syndicate (v2.1).
From a git URL (ADR 0027) — review the manifest, then enable:
python -m server plugin install https://github.com/protoLabsAI/spacetraders-plugin
# review the printed manifest + capabilities, then enable it:
# plugins: { enabled: [spacetraders] } in your configOr drop this directory into your protoAgent's plugins/. No core edits.
Needs protoAgent ≥ 0.40.0 (the graph.sdk engine helpers — supervise,
Knobs, telemetry; the manifest's min_protoagent_version gates older hosts).
Pure Python over httpx (a core dep) — no extra pip install.
- Get a SpaceTraders account token at https://spacetraders.io.
- In the console: System → Settings → SpaceTraders — paste the agent token (or the account token + call sign to register a new agent).
- (Optional) seed the durable lessons so the agent recalls them:
PYTHONPATH=. python plugins/spacetraders/seed_kb.py - Tell the agent: "grow the treasury" — it runs the growth engine in the background and supervises. Watch it on the Fleet dashboard.
One-command fresh start / post-wipe recovery — register → seed → kick the engine:
PYTHONPATH=. python plugins/spacetraders/fresh_start.py <CALLSIGN> [FACTION]The background growth engine (st_autopilot_start, one shared rate budget) runs the
fleet by role, all guarded against loss:
- probes SCOUT markets (free) → build the price map trade needs;
- one cargo ship works CONTRACTS — the capital base (contracts are capped at one
active per agent, so they seed, they don't scale). When no workable contract exists
(un-sourceable good, DRIFT-only delivery, none on offer), the lead falls back to
trade for the rest of the window on the best route no other hauler holds — and a lead
that strikes out
contract_strikeoutswindows running (nothing fulfilled) is demoted to trade for acontract_cooldown-window cooldown before re-pinning contracts, so the fleet's best hauler never idles on a dead contract slot; - ships with a mining laser MINE the nearest asteroid and sell the ore — the engine classifies each ship by its mounts, so a mining drone digs instead of being mis-cast; the sell stop is validated both ways: assignment prefers a market that actually buys ore (imports or exchange), and at the dock only goods the market trades are offered — every refused good gets a buyer-existence scan covering every marketplace page in the system (cached per window; a possibly-truncated scan — a cut-short page walk or a single unreadable market — counts as unproven, never as "no buyer"), one side-trip to an in-system buyer recovers what it can, and jettison fires only on a scan-proven no-buyer good (a known buyer or an unproven scan holds the good in the hold instead — "reroute budget spent" never destroys sellable cargo); the stop only moves to the side-trip market when it also covers everything the assigned market was buying (a minor tail can't hijack the primary ore's buyer), a listed-but-glutted good rides along for the next run instead of being destroyed, a failure on the side-trip itself (unreachable buyer, one 429 at the dock) ends the window with the haul intact rather than jettisoning it at its own buyer, an unreachable sell market likewise keeps the haul, and three straight sell stops that neither earn nor shrink the hold strike the window out — progress is measured on the ship's own sale receipts, never the fleet-shared credits balance, so other ships earning concurrently can't mask a dead market (a market that lists the ore but never absorbs it can't replay rejected sells until the deadline);
- every other cargo ship runs the best profitable TRADE route — the scaling lever,
each independent + spread-guarded, re-evaluated as markets saturate, with built-in
saturation damping (≈ one
tradeVolume/visit, glutted importers skipped, haulers diversified across the top routes) so it never crashes its own routes; - profit is reinvested into haulers once capital is comfortable — through the
availability-aware capital-deployment ladder below, and surplus beyond
gate_bufferfunds the system's jump-gate construction.
It learns: each discovered route is remembered in the knowledge store and recalled before re-scanning, so every window — and every fresh start — is smarter than the last.
Above the engine, the OODA strategist (manage-the-fleet skill) steers it between
windows: Observe (st_report) → Orient (recall lessons, compare cr/hr vs the goal)
→ Decide → Act (st_strategy / st_tune / st_assign) → Learn (memory_ingest).
A scheduler tick is the loop's clock; a spacetraders:credits goal is its terminus.
The universe resets weekly (Saturday mornings) — durable lessons + learned routes survive (they're the agent's memory); the in-game agent/ships/token don't. After a wipe, just run
fresh_start.pyagain — it re-registers, re-seeds, and the engine recalls what it learned last cycle. Nothing per-reset is hard-coded.
The trade engine routes the supply chain, not arbitrary buy-low/sell-high. It buys a
good where a market EXPORTS it (supply HIGH/ABUNDANT → cheap, and it refills
every cycle) and sells where another market IMPORTS it (supply SCARCE → dear, and
that demand refills too). Random arbitrage on any good saturates fast — every trade moves
the price, capped by tradeVolume, so a 50% spread dies in two trades while a 10%
export→import route refills forever. So best_route ranks by margin × tradeVolume
(per-cycle throughput), not raw spread: a 10% route moving 60 units beats a 50% one capped
at 5. Cross-market EXCHANGE spreads are only a fallback when no export→import pair exists.
Time-aware lane scoring (v2.7): the fleet's real budget is ship-time, so when the
engine knows a route's endpoint coordinates (the window-open marketplace scan carries
them) the score becomes net profit per second: net = margin × volume − fuel over
travel + stop overhead, with a first-order CRUISE model (fuel ≈ distance;
travel ≈ distance × 25 / nominal hauler speed 30; 15 s fixed overhead per stop — the
constants are documented in analysis.py). A fat lane 400 units out no longer starves
credits/hr while a thinner lane 10 units away compounds six times as often. Every route
in a time-aware ranking scores in the same unit: one with an unmapped endpoint is scored
at a pessimistic nominal distance (400 units — an unproven endpoint can't claim to be
near), not its raw margin × volume — raw is credits per visit while mapped routes
score credits per second, and mixing the units would let any unknown-distance lane
structurally outrank every mapped one. Callers that pass no coordinates at all keep the
time-blind margin × volume ranking, so nothing regresses where the whole ranking is
coordinate-free. The engine's window-open marketplace scan is paginated (the API caps
a page at 20), so markets past page 1 get coordinates too instead of ranking as
unknown-distance forever.
Three engine upgrades from the operator's external deep-research synthesis:
- Contract ride-along — contracts pay for the trip, so an empty corner of the hold
is margin left on the table. After the contract units are aboard (never before — the
contract's own load is never squeezed) and before the buy market is departed, spare
room carries a good the current market exports that the delivery stop imports
(or a market a trivial, coordinate-proven hop from it), picked at the best margin at
or above
min_marginand bounded by every existing buy guard (max_spend_frac,reserve_flooras a hard per-chunk floor, never above the confirmed resale). It's sold right after the delivery lands. Strictly opportunistic: any ride-along failure — no pick, a dead price store, a refused buy, an unreachable sink — degrades to the plain contract flow; it can never block, delay, or fail the contract leg. - Proactive route rotation (
route_rotate_cycles, default 4,0= off) — repeated deliveries walk a sink up the supply tiers, so waiting for the margin to collapse (the strike hysteresis) means the last windows on a lane are the worst ones. The plan now counts each hauler's consecutive profitable windows on one route and, at the threshold, prefers swapping it onto the best unused fresh route — same no-steal rules as any assignment (never a route another hauler holds or carries in), counter reset on the swap. With no alternative the hauler keeps its lane — rotation never idles a profitable ship, it just fires the first window an alternative appears. A vacated lane rests for the remainder of the window — no later hauler may pick it up until the next reconcile — so the saturated sink actually gets its breather instead of rotation degenerating into a synchronized fleet-wide reshuffle (haulers assigned together hit the threshold together; the staggered pickup also desyncs their counters). - Survey-fed mining — when a hull on the rock carries a
MOUNT_SURVEYOR_*(the command frigate does), it charts the asteroid once per fill cycle and every miner on that rock extracts against the best cached survey (largest deposit signature), typically a 2–3× value lift over blind extraction. Surveys expire and exhaust server-side: a survey-shaped rejection drops the dead chart and the same swing falls back to a plain extract — survey trouble never costs a window. No free surveyor hull (it may be on contract duty) ⇒ plain extraction, exactly as before.
Alongside these, the extract-409 retry: a rejected extract/siphon that is the
cooldown-skew shape ([4000]/HTTP 409 — the server's cooldown clock and ours disagree
by a hair; five windows died to it in one live night) waits out the server-stated
remainder parsed from the rejection itself (our own cooldown read saying "ready" is
exactly what the skew means, so re-reading it alone could burn the retry instantly),
re-reads the ship's cooldown as a backstop, and retries once. A second consecutive
rejection — or any other rejection class (wrong waypoint, no mount) — stays terminal, so
the bail-on-reject guard that stops budget-burning extract spam is not weakened. The v2.7 research round also
corrected the reinvest price book to observed yard prices (probe ≈ 28k, light hauler
≈ 393k) so affordability gates stop delaying compounding past the point the treasury can
actually pay.
Surplus credits are deployed, not hoarded — and a watch trips if they ever sit idle:
- The reinvest ladder runs once per window, rungs in priority order, each gated on
affordability (
reserve_flooris the hard cash floor; the*_bufferknobs are comfort thresholds): probe (while the price map is undermap_target), heavy freighter (once overheavy_bufferwith no long-range hull — range unlocks far contracts/routes), light hauler (overbuy_buffer— the scaling default). The ladder is availability-aware: a rung whose hull no in-system yard sells falls through to the next rung the same window (yard stock is scanned once per window, never cached across windows — yards restock). Before v2.5 that wedge was live: at ≥1.5M credits the heavy-freighter rung matched, no local yard sold one, and reinvest bought nothing. Fall-through needs proof, though — unknown ≠ unsold: if any per-yard read flaked (exhausted-retry 429s are the live norm), a rung absent from the incomplete scan holds the pass and retries next window instead of buying the next rung down (one flake must never convert into an irreversible wrong-hull purchase while the fleet is deliberately banking for the higher rung). - The gate-supply rung: when the home system's jump gate is under construction and
live credits (read after the window's reinvest buy — the window-open snapshot goes
stale the moment a hull is bought) clear
gate_buffer(default 1M), one spare hauler per window (never the contract lead, never one whose hold is full of trade cargo) buys the site's missing materials at in-system markets and delivers them (construction_suppliedon the bus). Sources come from the cached price map (the probes' recorded market visits), memoized per window: a source that shows no live price (a stale row — the good left its listings) is dropped with a log and the next candidate tried, and only when every cached seller dies does the good get its one live market sweep per window — gate supply never re-scans every market per trip, and a stale cache can never produce a false "no in-system source" verdict while a real seller exists. Spend is floored twice — never belowmax(reserve_floor, buy_buffer/2)and never more thanmax_spend_fracof the treasury — and the floors hold under fleet concurrency (one shared treasury, every job spending it at once): the budget re-derives from live credits after the travel leg lands, and the floor rides into the buy loop as a hard per-chunk check, so a concurrent trade buy mid-flight or mid-chunks can't drag credits through the guarantee — gate spend can't starve trading. Deliberate deployment steps thest-drawdownhigh-water mark down, so it never reads as a treasury crash — but under the liquid-treasury metric timing matters: a hull purchase reconciles at buy (credits → a hull the metric excludes), while gate materials reconcile in TWO parts — because a gate export is valued as cargo at its (lower) in-system sell price S, not the purchasePrice P the fleet paid. The buy drops the realizable treasury byunits·(P − S)at buy (the spread the export loses becoming cargo the metric marks to S — often 0 when no in-system market buys it), and the residualunits·Sdrops at delivery when construction consumes it for no credits back. A delivery-only step left the buy→deliver window — engine down, capital parked in in-flight cargo the metric already marks to S — unreconciled, so it false-tripped on the exact incident it exists for. Both parts read S from the durable 1-day price window (the drawdown verifier's own window), so a gate source visited only for gate runs (routinely >1h stale) still supplies the basis; P is the live purchase price. The manualst_supply_constructionpath — which sees only the delivery and can't assume the buy was reconciled — reconciles the full cost basis at once, conservatively (over-stepping self-heals via the mark's upward-only ratchet; under-stepping would false-trip). The ordinary buy→sell trade cycle, by contrast, is treasury-neutral end-to-end and needs no reconciling. A material with no workable source is logged once per window (price-fail path included) and skipped, never wedged on. Paid-for cargo is never dumped: materials a cut window left in the hold re-draft their hauler to gate duty next window regardless of the treasury (carrier-first dispatch) and are delivered first, and a drafted hauler's unsold trade tail rides along untouched (the v2.4.x sell-guard doctrine) — shopping fills only the free room, a full hold hands the window back to the ship's route. Two hardening rules keep those guarantees honest: provenance is stamped at buy time — the shopping leg records what it buys per ship (epoch-scoped, on disk), delivery consumes the stamp, and only stamped units are ever drafted or delivered: cargo symbols can't tell gate leftovers from trade capital (gap goods are ordinary market goods, so a FAB_MATS route can rank), and the plan route can't vouch for a plan-less hauler's dynamic trade buys or a swapped route's unsold tail — an unstamped gap-good tail rides untouched to its own sell leg, and the ship's own plan-route good is additionally never shopped on that hull nor delivered even when stamped (its route sells the hold at its confirmed sink instead of donating it at zero revenue); and flake-proof re-entry — the last confirmed gate sighting (waypoint + material list) persists on disk (cleared on a universe reset or when the gate finishes), so one exhausted-retry 429 on the gate scan — or a host restart plus a flaky read — can't skip the carrier check and hand a loaded hauler to a trade loop that would fire-sell the materials. A stale sighting only protects loaded cargo; it never arms new spending. - When it holds cash (all deliberate): under the buffers, at
max_ships, when no in-system yard sells any affordable rung, or when the gate needs only unsourceable goods. If that hold persists — credits aboveidle_capital_floorfor the whole lookback with no ship purchase and no gate delivery — thest-idle-capitaltripwire wakes the agent to retune (max_ships,gate_buffer) or to say explicitly why holding is right. Banking toward the gate never trips it: while the gate is under construction the idle floor rises togate_buffer— saving the engine is about to spend is deployment in progress, not idle capital. The manual tools count as deployment too:st_buy_shipandst_supply_constructionstamp the same ground truth the engine does (and step the drawdown mark by the spend), so following the watch's own playbook by hand acknowledges the trip instead of re-firing it with a false "no deployment" story.
The engine publishes to the protoAgent event bus (ADR 0039) under the spacetraders.
namespace — fire-and-forget, never control flow. The Fleet dashboard subscribes over the
iframe event bridge for live refresh — with a since high-water mark (protoAgent #1640),
so a reopened dashboard replays what it missed and the 60s poll survives only as a
fallback for pre-#1640 hosts (it's disarmed the first time an event carries seq). The
console rail dot lights for free when the view is hidden, and other plugins (a Discord
ops feed, a portfolio watcher) can subscribe spacetraders.# without importing this plugin.
Payload contracts (until manifest-typed event schemas land — protoAgent #1636):
| Topic | Payload | Fired |
|---|---|---|
spacetraders.engine_started |
{window_minutes} |
autopilot begins |
spacetraders.engine_stopped |
{reason} |
operator/agent stop, or the goal hook winds it down |
spacetraders.window_closed |
{minutes, credits_start, credits_end, gained, per_hour, ships} |
every engine window (ship count, not detail — bus payloads stay light) |
spacetraders.trade_executed |
{ship, good, buy_at, sell_at, units} |
a buy→haul→sell round trip completes |
spacetraders.ship_purchased |
{ship, type, yard} |
reinvestment buys a hull |
spacetraders.construction_supplied |
{ship, good, units, site} |
the gate-supply rung delivers materials to the construction site |
spacetraders.reset_recovered |
{status} |
the watchdog re-registered after a universe wipe |
This plugin is a demonstration of the substrate's capabilities — an autonomous, research-driven, self-improving agent that goes from a fresh start to a growing treasury — not a min-maxed, leaderboard-optimal bot. That's deliberate. The engine plays a sound, loss-guarded baseline and stops there, leaving the interesting decisions — and the headroom — to you and your strategies.
What's left as room to explore (and how the pieces invite it):
- Multiple goals, not one number. It runs a
spacetraders:creditstarget and aspacetraders:fleet_sizetarget as parallel monitor goals — because optimizing one metric alone creates blind spots (a pure credits goal under-invests in ships). Add your own goals (reputation, a jump gate, a system to dominate) with a plugin verifier. - A
strategistsubagent that researches and decides. It audits, reads the meta (web_search+ the knowledge store), and acts within bounded authority (self-heal, tune, steady fleet growth). Widen its mandate, sharpen its strategy, or replace it — it's where your edge goes. - Tunable engine knobs (
st_tune) and a deliberately conservative posture (declines slow far-hauls, modest reserves, simple route math). Crank the aggression, expand into the outer system, specialize ships — the levers are exposed on purpose. - It learns + remembers (knowledge store), so whatever strategy you layer on compounds across windows and survives the wipe.
In short: it gets the fleet going and growing on its own, and gets out of your way so the optimization, specialization, and clever plays are yours to add.