Spansh has no official API documentation. Every shape below was observed live and is replayed by the
tests from tests/data/spansh/ (trimmed real replies, re-recorded with
tools/smoke_spansh.py --record). If Spansh changes one of these, the smoke script is the first thing to
run. Code: EDMultitool/edmultitool/sources/spansh.py; all calls go through sources/http.py.
Common to every call:
User-Agent: EDMultitool/<version> (EDMC plugin; +https://github.com/Martin8O/ED-Multitool),Accept: application/json.- Our pace: at most one request per second (polling included), 20 s timeout. GETs are retried on 429/5xx/network failure, job submits (POST) only on 429/503 — after a timeout or 502 Spansh may already have the job, and a retry would start a duplicate. Waits: Retry-After if given, else 2 / 4 / 8 s; a Retry-After over 60 s means we give up at once instead of knocking early.
- Errors come back as JSON
{"error": "<message>"}with a 4xx status; we show the message. - id64 arrives as a string in route results (
"id64": "10477373803") and as a number in the autocomplete reply. Both are parsed to an exact int. The numericidfield in bodies is the same value as a float-unsafe number — never read it. - No CORS: only our Python side calls Spansh, never the web page.
GET /systems/field_values/system_names?q=<text> → 200
{"min_max": [{"id64": 525823134027, "name": "Col 285 Sector VE-Z d15", "x": -235.125, "y": 66.90625, "z": -280.65625}, …]}- Up to 20 rows, prefix matches. We skip texts shorter than 2 characters. Cached 24 h.
Submit, form-encoded (application/x-www-form-urlencoded):
| field | riches POST /riches/route |
bio POST /exobiology/route |
our value |
|---|---|---|---|
from |
✓ | ✓ | system name |
to |
✓ | ✓ | system name |
range |
✓ | ✓ | laden jump range, ly, 2 decimals |
radius |
✓ | ✓ | ly from the from→to line |
max_results |
✓ | ✓ | stops wanted (Spansh may return fewer) |
max_distance |
✓ | ✓ | max distance to arrival, ls |
min_value |
✓ | ✓ | riches: min scan (or mapping) value per body; bio: min value per species (see below) |
loop |
✓ | ✓ | 0 / 1 |
avoid_thargoids |
✓ | ✓ | 0 / 1 |
use_mapping_value |
✓ | — (Spansh ignores it; not sent) | 1 = rank by DSS value |
- Submit reply:
202 {"job": "<UUID>", "status": "queued"}. - Unknown system:
400 {"error": "Could not find starting system"}. max_resultspicks the best N, then orders them along the route (probed live 2026-10-12, Sol → Colonia, riches min 1 M and bio, 10 vs 30): the 30-stop answer holds all 10 of the 10-stop answer plus 20 more, but not as a prefix — the extra stops sit in tour order among them. Swapping a done stop relies on this: the replacement for a stop that is already yours comes from the same search asked for 3× the stops (≤ 100), sent only when a stop is spent.- Body
id64= the system'sid64+ the journal'sBodyID<< 55 (bodyidis the same number again); that is how journalSAAScanComplete/ScanOrganicbodies are matched to Spansh bodies.
Poll: GET /results/<job>
- still running →
202with{"job", "state": "started", "status": "queued", "parameters": {…}, …}; - done →
200 {"status": "ok", "state": "completed", "job", "parameters", "result": [ … ]}; often already on the first poll (1–2 s for a Sol→Colonia tour); - unknown / expired job →
404 {"error": "Unable to find route"}. - We stop polling after 300 s. Completed results are cached by the exact submit form for 6 h.
result rows (in tour order; the first row is from and the last is to, both with no bodies):
{"id64": "180665307059", "name": "Boelts KT-Q d5-5", "jumps": 497, "x": …, "y": …, "z": …, "bodies": [ … ]}jumps= jumps from the previous row at the given range.
Riches body:
{"id64": "396316947873910707", "name": "Boelts KT-Q d5-5 A 5", "type": "Planet",
"subtype": "High metal content world", "distance_to_arrival": 1177.85, "is_terraformable": true,
"estimated_scan_value": 191807, "estimated_mapping_value": 696898, "landmark_value": null}Bio body — same, without is_terraformable, plus species:
{"landmark_value": 34841800, "landmarks": [{"type": "Frutexa", "subtype": "Frutexa Acus", "count": 16, "value": 7774700}, …]}value= base sale value of the species (no first-logged bonus);count= sightings Spansh knows of.- Bio
min_valueis a per-species filter (verified 2026-10-12, Sol→Colonia at 22.38 ly): with 5,000,000 every returnedlandmarks[]entry was ≥ 5,000,000 and cheaper species were simply left out of the list; with 20,000,000 no system came back at all (the dearest species on that line is 19,010,800), although bodies worth 34.8 M in total exist there. So ED Multitool sends the species minimum asmin_valueand applies its per-body minimum itself. landmark_valueis the body's total over all its species, unfiltered — that is why it "did not match the sum" of a filtered list. We sum the returnedlandmarks[].valueourselves and never read it.- Riches
min_valuefilters per body: every returned body has its ranked value (mapping whenuse_mapping_value=1, else scan) ≥min_value.
Checked by hand on 2026-10-12 against spansh.co.uk's own results pages for the recorded jobs: riches 7 systems / 11 bodies / scan 3,354,293 / mapping 12,187,252 / 1,114 jumps — identical; bio per-body species values and sums identical (e.g. Qa'wakana 12 f: 9 species, 34,841,800 Cr).
A tour job's from must be a system Spansh has a record of: from an undiscovered one it answers 400
{"error": "Could not find starting system"}. When that start is the system you are in, the search starts
instead from the nearest system Spansh knows (next section) and says so.
POST /systems/search with no filter at all, nearest first, one row:
{"filters": {}, "sort": [{"distance": {"direction": "asc"}}],
"reference_coords": {"x": -75.0, "y": -150.0, "z": -340.0}, "size": 1, "page": 0}→ {"count": 10000, "results": [{"id64", "name", "x", "y", "z", "distance", …}]} (Merope, 3.65 ly; from its
own coordinates: Merope at 0.0 ly). Recorded reply: tests/data/spansh/systems_nearest_merope.json
(trimmed). Cached like the other searches.
Both route-only choices — "Normal" and "With neutron boosts" — use Spansh's galaxy plotter, so both show
every jump with fuel (the same table either way). Shape read off spansh.co.uk's own page
code (form-encoded like the tours, same /results/<job> polling), then each parameter we vary probed live at
two values. Recorded replies: tools/smoke_spansh.py --plots --record (Sol → Maia, both choices).
POST /generic/route (spansh.co.uk/exact-plotter) — wants system ids, not names (we look names up with
the autocomplete, exact name match, any letter case; a name two systems share is refused). The autocomplete
puts the exact name first, even when 20+ names start with it (checked 2026-10-12: "Sol",
"Col 285 Sector KZ-C b27-1", "Synuefe EN-H d11-11", the last two with a full list of 20), so far a real
system was always among the 20 rows (observed, not documented by Spansh). Ship constants as Spansh's own
journal-Loadout import computes them:
| field | our value |
|---|---|
source, destination |
id64 |
optimal_mass, max_fuel_per_jump |
FSD after engineering (journal modifiers) |
fuel_multiplier, fuel_power |
FSD constants, e.g. 0.013, 2.45 |
base_mass |
Loadout UnladenMass + reservoir fuel |
tank_size, internal_tank_size |
main tank, reservoir |
range_boost |
Guardian FSD booster, ly |
cargo |
t carried the whole way |
use_supercharge |
0 = "Normal", 1 = "With neutron boosts" |
supercharge_multiplier / injection_multiplier |
4 (6 for the SCO Mk II, as Spansh maps …_overchargebooster_mkii) / 2 |
is_supercharged, use_injections, use_injections_when_required, exclude_secondary, reserve_size |
0 |
refuel_every_scoopable |
0 |
algorithm / max_time |
optimistic / 60 (Spansh's defaults) |
Probed at two values (Sol → Maia, Mandalay SCO 5A): cargo 8 → 20 jumps, longest 22.49 ly; 100 → 23 jumps,
longest 19.46 ly. use_supercharge 0 → no neutron jump; 1 → 18 jumps incl. one 90 ly boost.
refuel_every_scoopable 1 → must_refuel on 14 rows (every scoopable star); 0 → 3 rows (only where needed).
range_boost 0 → 20 jumps, longest 22.49 ly; 10.5 → 13 jumps, longest 32.66 ly — a flat bonus in ly per
jump, the same figure the journal-based range adds. The other ship constants were sent at one value each
(this ship's); every one of the 20 jumps lies within ED Multitool's own range formula for the fuel on board at
that moment (tightest: 22.25 of 22.27 ly), so Spansh reads them in the units we send.
Result: result.jumps[], one row per jump, first = the start (distance 0):
{"name": "Tharwangu", "id64": 13865093965185, "x": …, "y": …, "z": …, "distance": 18.56,
"distance_to_destination": 290.73, "fuel_in_tank": 32, "fuel_used": 3.13, "must_refuel": true,
"is_scoopable": true, "has_neutron": false, "must_inject": 0}has_neutron: true= supercharge at this star: the jump of the NEXT row is the boosted one (Sol → Colonia with neutrons: all 183 marks sit right before the 183 jumps longer than a normal jump, and nowhere else).is_scoopableis right for the stars on the way (checked against Spansh's own system data: G, M, M = scoopable; L brown dwarf, neutron star = not), but the start and destination always sayfalse— Maia (a B star) and Brestla (a red dwarf) included. ED Multitool treats both ends as "not known".- The route starts with full tanks;
fuel_in_tankis after arriving (and after refuelling). - A long plot takes a while: Sol → Colonia took ~64 s (normal, 1,071 jumps) and ~63 s (neutrons, 784 jumps); the normal reply is ~440 KB.
- Start = destination is not refused: Sol → Sol comes back as a two-row route of 0 ly. ED Multitool refuses it before asking.
Checked by hand on 2026-10-12 against spansh.co.uk's own results pages for the very form ED Multitool sends (same job shape, Spansh's page reading it): normal Sol → Maia — 20 jumps, every name / distance / refuel mark identical; with neutrons Trianguli Sector EL-Y c7 → Brestla — 6 jumps, supercharge at Jackson's Lighthouse, refuel at Col 285 Sector ZT-X b15-1, all rows identical; with neutrons Sol → Colonia — 785 rows, 784 jumps, 183 neutron and 133 refuel marks, every row identical.
Neutron plotter POST /route (spansh.co.uk/plotter) — used briefly, not used since 2026-10-12 (it lists
only the supercharge waypoints, so a trip without neutrons showed as start → destination). Kept for the
record: fields from, to (names), range, efficiency (default 60; 100 made the job fail with HTTP 500),
supercharge_multiplier (4 / 6); result result.system_jumps[] with system, id64, distance_jumped,
jumps (from the previous row; their sum is the trip's jump count, result.total_jumps is the number of legs)
and neutron_star. Its Sol → Colonia at 22.09 ly matched spansh.co.uk row for row (268 rows, 700 jumps).
- Prime planets: the riches job has no body-type filter, so with
planets= prime we sendmax_results= 100 and filter subtypes (Earth-like / Water / Ammonia world, oris_terraformable) on our side. Probed live 2026-10-12, Sol → Colonia, min 1M mapping: 10 → 12 rows, 100 → 102 rows, both fine.
The "Gear from the forum" card turns the place names of a forum post into systems and stations. Recorded
replies: tools/smoke_spansh.py --gear --record (tests/data/spansh/gear_*.json, trimmed).
- Pieces that may be systems go through the autocomplete above (any letter case; exact name only). A piece that reads like a station ("… Terminal", "… Port", "… Town") is never asked for as a system.
POST /systems/search, JSON body (Content-Type: application/json):{"filters": {"name": {"value": ["Keian Gu", …]}}, "sort": [], "size": <2 × names>, "page": 0}→200{"count", "from", "reference", "results": [{"id64", "name", "x", "y", "z", "stations": [ … ], "bodies": …}]}. The name filter is exact and letter case matters ("shivis" finds nothing, "Shivis" does) — so we send the autocomplete's own spelling. A list of names is an OR; one call for all the systems of a post. Eachstations[]row:name,type("Ocellus Starport", "Outpost", "Planetary Outpost", settlements…),distance_to_arrival(ls),has_large_pad,medium_pads,small_pads,is_planetary(plus market and services, never read). Odyssey settlements are in the list.- The Missions tab uses the same system search to place mission destinations: the journal names a mission's system only by name (no id64), in the game's own spelling, which is Spansh's; all open missions' systems go in one call (≤ 20 names), cached 24 h. A name with no row = "Spansh does not know" on the page. Rows are matched back to the asked names ignoring letter case only as a safeguard (the filter itself is exact).
POST /stations/search, same body shape,size50 →results[]with the station fields above plussystem_name,system_id64(a number),system_x/y/z. Used only for station names no found system holds: one call per name (≤ 4 per post) with the post's spelling and each word capitalised, so a name's hits are all of them. Many stations share a name ("Hahn Terminal": 7).- Optional
"reference_system": "<name>"addsdistance(ly) to every row — used only to check our own distances by hand (2026-10-12, from Trianguli Sector RT-Q a5-0: Tatamo 183.60, Maricoriang 229.38, Kuukese 189.07 ly — identical to ED Multitool's). - Replies are large (a station row carries its whole market: ~36 KB each); we keep only the fields above, cached 24 h by query.
- A search changes nothing at Spansh, so a JSON POST is retried like a GET (one
502was seen live and the same query answered at once on retry); job submits stay un-retried on 502 / timeouts.
POST /trade/route (spansh.co.uk/trade), form-encoded, a job like the tours: 202 {"job"} → GET /results/{job} until it stops answering 202. One hop only (max_hops=1): Spansh picks what to buy at the
start station and the station that buys it for the most profit.
| field | our value |
|---|---|
system, station |
names; both required (system alone → 400 {"error": "system and station are required"}) |
max_hops |
1 |
max_hop_distance |
ly, straight from the start system to the one you sell in |
starting_capital / max_cargo |
Cr to spend / t to fill (the page's fields, or your balance / the ship's hold) |
max_system_distance |
ls from the star, both stations |
max_price_age |
seconds (1 day = 86400) |
requires_large_pad |
1 when the ship needs L, else 0 (Spansh tells only large pads from the rest) |
allow_planetary / allow_player_owned |
planetary ports and settlements / fleet carriers: 1 or 0 |
allow_prohibited, unique, permit |
0 |
The result echoes parameters (only the truthy ones, with starting_station_id instead of the names) and
result = a list of hops, [] when nothing pays within the limits. A hop: source / destination =
{system, system_id64 (a number), station, market_id, distance_to_arrival (ls), market_updated_at
(unix seconds), x, y, z}; distance (ly); commodities = [{name, amount (t), profit (Cr/t),
total_profit, source_commodity / destination_commodity = {buy_price, sell_price, supply,
demand}}]; total_profit, cumulative_profit. No pad sizes, station types, factions or docking access.
Probed live (Sol, Abraham Lincoln; tools/smoke_spansh.py --trade): 400 t / 50 M Cr → Biowaste 400 t,
profit = sell − buy per t and total = profit × t; 20,000 Cr → the load shrinks to 183 t (what 20,000 Cr buys,
no second commodity); max_hop_distance 1 → result: []; an unknown station → 400 {"error": "Could not find station"} at the submit. The same 400 came for a real station Spansh's station search knows (Jiushao
Vista, Cave Sector HR-W d1-31) even with a 30-day price age: the trade router keeps its own list of
markets.
The station choice (before a trade): POST /systems/search {"filters": {"name": {"value": [name]}}, "sort": [], "size": 4, "page": 0} — the exact name, letter case included ("sol" finds nothing). Each system
row carries stations = [{name, type, has_market, distance_to_arrival, has_large_pad,
medium_pads, small_pads, …}]; we list those with has_market: true, nearest to the star first. Some rows
carry only name, distance_to_arrival and has_large_pad (4 of 67 in Sol): not listed. Fleet carriers did
not appear in Sol's list. Size 4 keeps its cache apart from the gear lookups' (size 2), whose rows were cached
without has_market.
The "Commodity" card. Recorded reply: tools/smoke_spansh.py --market --record
(tests/data/spansh/market_sol_tritium.json, trimmed). Every filter below was checked live at two values.
POST /stations/search, JSON body:→{"filters": { "market": [{"name": "Tritium", "demand": {"value": [10, 999999999], "comparison": "<=>"}}], "market_updated_at": {"value": ["2026-10-09T16:00:00Z", "2026-10-13T16:00:00Z"], "comparison": "<=>"}, "type": {"value": ["Asteroid base", "Coriolis Starport", …every type but "Drake-Class Carrier"]}, "has_large_pad": {"value": true}}, "sort": [{"distance": {"direction": "asc"}}], "reference_coords": {"x": 0, "y": 0, "z": 0}, "size": 25, "page": 0}{"count", "results": [station rows]}, nearest first;distance(ly) fromreference_coords.market[].nameis the display name exactly asGET /stations/field_values/marketlists it (the bundled list,tools/make_commodities.py).supply= stock (you buy),demand(you sell); the range applies to that commodity only (Tritium near Sol: ≥ 10 t → 5,188 stations, ≥ 5,000 t → 1,597, every row ≥ 5,000).market_updated_atfilters the station's market time;typeis an OR of station types. A type"comparison": "!="is ignored (carriers still came back), so carriers are left out by listing every other type (GET /stations/field_values/type). Only a large pad has a filter; medium is checked by us.- Each row's
market[]is the WHOLE market (~36 KB a station): we keep only the commodity asked (buy_price,sell_price,supply,demand) pluscontrolling_minor_faction,carrier_docking_access("All","Friends","Squadron Friends", … or missing),market_updated_at, pads,system_x/y/z. - A carrier's
controlling_minor_factionis"FleetCarrier". A Powerplay Stronghold Carrier is a row named"Stronghold Carrier"of type"Planetary Outpost"whosecontrolling_minor_factionis the power ("Aisling Duval") — only pilots pledged to that power may dock. - Cached 10 minutes (markets change): the
market_updated_atwindow moves in 10-minute steps, so the same search again within them is the same request; the exact age is cut on our side. More pages (page1, 2) are asked only when our own filters leave fewer than 10 rows; a later page failing keeps what was found.
The "Sell cargo" card, one search per commodity carried. Recorded reply: tools/smoke_spansh.py --sell --record (tests/data/spansh/sell_sol_painite.json, trimmed). Checked live: the order on every page, the
distance at 20 and 50 ly, demand ≥ the tonnes carried, large pads.
- The commodity-market body above, with
demandfrom the tonnes you carry, plus"distance": {"min": "0", "max": "50"}(ly fromreference_coords; strings, as Spansh's own page sends) and"sort": [{"market_sell_price": [{"name": "Painite", "direction": "desc"}]}]. → the best-paying stations first;countis the number within the distance (Painite ≥ 30 t near Sol: 554 within 50 ly, 105 within 20). - Each row still carries its whole market: we keep every commodity of the hold (not only the one searched), so the one station that pays most for the whole load is found among all the rows returned. A commodity at demand 0 there is not an offer.
- Cached 10 minutes like the market search; the cache key includes the commodities kept.
Nearest station: the station search with no market condition (verified 2026-10-13, services 2026-10-15)
The "Nearest service" card's only source. Recorded replies: tools/smoke_spansh.py --stations --record
(tests/data/spansh/stations_sol_large.json, stations_sol_medium.json, stations_sol_services.json,
stations_sol_trader_raw.json, trimmed). Each filter checked live at two values (large pad / none, settlements
in / out, carriers in / out, services, kinds).
POST /stations/searchwith{"filters": {"type": {"value": [...]}, "has_large_pad": {"value": true}}, "sort": [{"distance": {"direction": "asc"}}], "reference_coords": {x, y, z}, "size": 25, "page": n}. Types = the bundledSTATION_TYPES(no carriers), minus"Settlement"/"Surface Settlement"when settlements are left out, plus"Drake-Class Carrier"when carriers are wanted. Medium pads are checked by us.- Order within a system is arbitrary (only the system's distance is sorted): ED Multitool sorts by
(system distance,
distance_to_arrival) and asks the next page while a page still ends in the last system shown (Sol has more stations than one page). - A row: the station fields of the market search plus
updated_at; a megaship can havemedium_pads/small_padsnull (no pad known: never shown). Odyssey settlements often reporthas_large_pad: truewith 0 medium / small pads. - Cross-checked 2026-10-13 from Bitjala (large pad, no settlements): the 10 shown = a direct query sorted the same way, 10/10, Rajgonta 6.47 ly.
- Services:
"services": [{"name": "Black Market"}, {"name": …}]= stations with every one;"services": {"value": [...]}= any one of them (with or without a"comparison"), so the list-of-names form is the one used. Names as in a row'sservices[].name("Material Trader", "Technology Broker", "Interstellar Factors Contact", "Universal Cartographics", "Vista Genomics", "Restock", …); an unknown name gives 0 results. Recorded:stations_sol_services.json(Black Market + Interstellar Factors, 25 of 25 have both). Cross-checked from 9 Tauri (medium pad): the 10 shown all have both in a direct query by name. - A trader's or broker's kind = the station's primary economy (the game's rule):
"primary_economy": {"value": ["Extraction", "Refinery"]}(raw),["Industrial"](manufactured / human broker),["High Tech", "Military"](encoded),["High Tech"](Guardian broker). A value list is "any of"; checked live near Sol (raw traders 373, every row Extraction or Refinery; Guardian brokers 887, every row High Tech). A row carriesprimary_economyin Spansh's spelling ("High Tech"with a space). Two kinds that share no economy (a raw trader with a human broker) are answered without asking. Cross-checked from Maia (raw trader, medium pad): the 10 shown = a direct query sorted the same way, 10/10, Hinz Hub 196 ly.
The "Powerplay" card. Recorded replies: tools/smoke_spansh.py --powerplay --record
(tests/data/spansh/power_sol_lyr_{held,others,acquire}.json, trimmed). Each filter was checked live at two values.
POST /systems/search, JSON body:→{"filters": {"controlling_power": {"value": ["Li Yong-Rui"]}, "power_state": {"value": ["Exploited", "Fortified", "Stronghold"]}}, "sort": [{"distance": {"direction": "asc"}}], "reference_coords": {"x": 0, "y": 0, "z": 0}, "size": 10, "page": 0}{"count", "results": [system rows]}, nearest first.countis capped at 10 000.- Power names:
GET /systems/field_values/controlling_power(and/power) — the 12 powers, spelled as the journal spells them ("A. Lavigny-Duval"). States:GET /systems/field_values/power_state→Exploited,Fortified,Stronghold,Unoccupied(nothing else). - "Held":
controlling_power(from Sol: 2,185 systems; withpower_stateFortified 429, every row Fortified). "Present":power(the row'spower[]holds it) — includes systems another power holds (Sol: Jerome Archer) and Unoccupied ones (7,995 for Li Yong-Rui). The page asks it two ways, never mixed with the power's own: "others hold" =power+controlling_power= the 11 other powers + the three controlled states (a value list is "any of"; from Sol 3,206 for Li Yong-Rui, Sol first, held by Jerome Archer); "can acquire" =powerpower_stateUnoccupied (from Sol 8,013, Stein 2051 first). Recorded 2026-10-14.
- Row fields we keep:
id64,name,x/y/z,controlling_power(null when Unoccupied),power[],power_state,power_state_control_progress(a fraction: 1.44 = 144 %, past the state's band),power_state_reinforcement/power_state_undermining(merits this cycle),updated_at. An Unoccupied row leavescontrolling_power, the progress and both merit fields out altogether (not null). A row also carries bodies, stations and factions: trimmed before caching. - Cached 15 minutes.
- How many systems a power holds (the Commander tab): the same search, one per state
(
power_state=Stronghold/Fortified/Exploited),reference_coords0,0,0 and"size": 1: onlycountis read. Li Yong-Rui on 2026-10-13: 241 + 429 + 1,515 = 2,185, the same as the "held" search. Then every one of the 12 powers held in any controlled state (all three states in one filter, size 1): 12 more searches, cached 15 minutes like the rest. 2026-10-13: Aisling Duval 3,038 (a direct query: 3,038) … Denton Patreus 720.
The "This system" card (on every arrival). Recorded replies: tools/smoke_spansh.py --arrival --record
(tests/data/spansh/bodies_{boelts,qawakana,plaa_eurk,unknown}.json, trimmed; landmarks without coordinates).
POST /bodies/search, JSON body:→{"filters": {"system_id64": {"value": 642232231025}}, "size": 100, "page": 0}{"count", "results": [body rows]}; a system Spansh does not know →count0,results[]. A further page is asked only while a full page came back andcountis not reached (no real system needs it).- Row fields we read:
body_id(the journal's BodyID),name,type(Star/Planet),subtype(EDSM-style names:High metal content world,Earth-like world,K (Yellow-Orange) Star…),distance_to_arrival,estimated_scan_value,estimated_mapping_value(also given for stars, where it means nothing — not read),terraforming_state(Terraformable/Not terraformable/Terraformed; there is nois_terraformablehere, unlike the tour jobs),is_landable,signals([{"name": "Biological", "count": 6}, …]),genuses([{"name": "Bacterial"}]: codex keys, shown as the game's genus names),landmarks(every sighting:{"type": "Frutexa", "subtype": "Frutexa Acus", "value": 7774700, "variant", "latitude", "longitude"}, one entry per sighting, plus geysers and settlements withvalue0). Species = landmarks with a value, one persubtype. - Values:
estimated_*follow one formula exactly (all 89 planets of the three recorded systems, to the credit): scan = ⌊max(500, k + k·0.56591828·mass^0.2)⌋ with mass in Earth masses; mapping = ⌊scan·3.3333333333⌋ + ⌊max(scan·0.3, 555)⌋. k: Earth-like 181,126; water world 64,831 (+116,295 terraformable); ammonia world 96,932; metal-rich 21,790; high metal content and class II gas giant 9,654 (+100,677 terraformable HMC); class I gas giant 1,656; anything else 300 (+93,328 terraformable). Stars: ⌊k + solar masses·k/66.25⌋, k 1,200 (22,628 neutron stars and black holes, 14,057 white dwarfs). Our own FSS scans are valued with it (core/arrival.py), so a body Spansh does not know yet gets the same number. - Cached 1 hour (other explorers' scans reach Spansh within that).
The commodity card's backup when Spansh fails. Recorded reply:
tests/data/ardent/market_sol_tritium.json (first 30 rows). Same client, pace and User-Agent as Spansh.
GET /system/address/{id64}/commodity/name/{symbol}/nearby/imports(where you can sell) or/exports(where you can buy);/system/name/{name}/…takes the system's name instead (used only when the id64 is not known), paramsmaxDistance(ly, default 100, max 500),minVolume(demand / stock ≥),maxDaysAgo(whole days; default 90),sort=distance,fleetCarriers=falseto leave carriers out —truemeans carriers ONLY, so with carriers wanted the parameter is left out.{symbol}is the game's commodity symbol in lower case (lowtemperaturediamond,opalfor Void Opal), never the display name (low temperature diamonds→[]). The pairs are bundled (catalog/commodities.py); a commodity without one has no backup search.- Reply: a list (up to 1,000 rows) of
{stationName, stationType, maxLandingPadSize (1/2/3), systemAddress, systemName, systemX/Y/Z, distanceToArrival, buyPrice, sellPrice, stock, demand, updatedAt, distance}. The current system itself is left out; no station faction; Stronghold Carriers come asstationType: "StrongholdCarrier"without their power (so never known to be yours), and carriers without their docking access (shown as open).demand: 0rows pass anyminVolume(Ardent reads 0 as "no limit"); we leave them out, as Spansh's filter does.
The update check. Public, no token, no sign-in; the same polite client, pace and User-Agent.
GET /repos/{owner}/{repo}/releases/latest→ the newest published release (drafts and pre-releases are never returned). Fields used:tag_name(v0.31.0),html_url(the release page),name,draft,prerelease. Shape as seen on a public repository, kept intests/data/github/latest_release.jsonwith our own tag. A404({"message": "Not Found"}) means no release is published yet, or the repository cannot be seen: "nothing newer", and the check says nothing.- Unauthenticated limit: 60 calls an hour per address, answered with
403/429; the client asks about once a day (reply cached 24 h), so the limit is never near. A failed ask is tried again an hour later. - A tag counts only when it reads as
[v]MAJOR.MINOR[.PATCH][-prerelease]; a link counts only when it beginshttps://github.com/.