THE SHADOW TRADER

Other Systems › The Shadow Trader

A tab of the Premium Shop, open all week (#770, D 2026-09-07 — LIVE on release since 2026-09-08 (0.71).; weekend-only until then). Every week the trader takes every currency except Wraithgold and hands back one currency — a different one each week — at a rate that is computed from how long each currency takes to earn, never authored by hand. Status: designed and BUILT 2026-08-25 (D, report #475) — rate model, quote and trade doors, the weekend tab. ON DEV; release at D's word.

The rules (D, 2026-08-25)

RuleWhat it means
All week (#770)The tab is there every day. The weekly cadence is the rotation alone: what he pays in turns over at Monday 00:00 UTC (the same clock as the daily reset), and the tab says what next week pays in. The weekend-only window (Friday noon to Monday noon JST, #628) ended with 0.71 on 2026-09-08; traderNextOpenMs() answers 0 for good.
One currency a weekEach week has one target: the currency he pays out. Week 0 (the week of Monday 2026-08-24) is Ichor, then Scrap, Dust, Essence, Ink, and round again. He takes every other currency — Scrap, Ichor, Dust, Essence, Ink — and never Wraithgold, in or out.
Rates from acquisition timeThe price of one unit of the week's currency, paid in another, is the ratio of how fast the two are earned, times the trader's cut. Earned means: what a battle yields in expectation, through the same drop and dismantle tables the game runs on. See the model below.
Self-adjustingThere is no rate table anywhere. The rate is derived at request time from drop_chances_by_floor.csv, drop_grade_weights.csv, drop_rates.csv, encounter_weights.csv, floors.csv, items.csv, dismantle.csv, breeding.csv and the Ichor-per-kill formula. Raise or lower any of them and the rates follow on the next request — a balance change can never leave the trader out of step.
One rate per account: its own best floorT5 (D, 2026-10-18, Tower rulings form: “Rebalance the trader, rebalance the income, but no cap on trader”; Bug Run 0.89-4 Q14). The Worker reads the account’s progress ratchet (its best floor, the number only credited battles can raise; GET /api/_internal/ratchet on the player object) and computes the rates at that floor — time parity plus the 25% cut at the floor the account can actually farm. A boss floor prices at the wild floor below it. No ratchet, no quote (503), never a fallback to 119. There is no weekly cap (D). Why: priced at 119 for everyone, a floor-25 climber bought Scrap with Ichor at 0.10× its farming cost at 25 (0.28× at 75, 0.98× at 105; tools/sim_econ.mjs); priced at the own floor every pair is 1.25× farming in every band, so the 40% cut the proposal carried is not needed and the cut stays 25%. Superseded rule — D, 2026-08-25: “The shadow trader cannot have a dynamic per account rate. It needs to be the same for everyone, and based off endgame playtime.” Every rate is computed at one reference floor, TRADER_REFERENCE_FLOOR (the endgame), never at the account's own floor. The two candidate floors are tabled below — 99 and 119, the wild floors; 100 and 120 are boss floors with no wild encounters, and the model prices a boss floor at the wild floor below it, so a table headed “floor 100” was floor 99's numbers (D caught the label 2026-08-25). D picked 119 (“119 as a base for the shadow trader”, 2026-08-25): TRADER_REFERENCE_FLOOR = 119.
The cutTRADER_PREMIUM = 0.25: you pay time-parity × 1.25, i.e. you get back 80% of what the same time would have earned you directly. One constant, in src/trader.js; D's to tune.
Whole numbers, no remainderA trade is quoted as pay P of A, receive G of B with both whole. P is exactly what G costs (rounded up to the unit); a request too small to buy one unit is refused, never partially taken.
Server-authoritativeThe client sends lines [{from, amount}] and an idem key, nothing else. The Worker prices every line off the tables; the player's object replays on the key, enforces a 30-second per-account cooldown, validates every line against the stored wallet (a short line refuses with what you hold and what it needs), then writes the exchange into the stored save itself (version bumped) and answers the new balances and version — the client only adopts. The client also saves before asking, so income still only in memory is never overwritten by the adopted wallet. The door sits in the wallet rate class on top of the cooldown.
The tab (D's UX)A slider per other currency, from 0 to the balance at the moment the shop opened. Above each, the rate reads 1 : x at rest and y : x as the slider moves (y paid, x received). ACQUIRING <total> <currency> at the very top. CANCEL / CONFIRM at the bottom; CONFIRM opens a confirmation window listing PAY y A → GET x B per line and the total; CONFIRM TRADE sends one request with every line. After a trade both buttons count down 30 s. Errors are plain sentences. The whole tab fits the panel without a scrollbar (#958, D 2026-09-17) — and because the stage is fixed at 1920×1080, that is a property of the stage geometry, true at every viewport rather than at a lucky one. Beautifying the tab is a backlog item.

The model — what one battle yields at floor F

Battles per hour cancel out of every ratio, so “acquisition time” reduces to expected units per battle. One battle at floor F, as the worker runs it:

Then, for the week's target B and any other currency A: rate(A→B) = perBattle(A) ÷ perBattle(B) × (1 + premium), in units of A per one B.

The reference floor — 99 and 119, tabled for D (the current codex, #772 Ichor curve)

Per-battle yields at each candidate floor, the time parity of each currency in Ichor (its weight: how many Ichor the same play time earns instead of one unit of it), and the full parity matrix. The trader's cut (×1.25) goes on top of any matrix cell. D picked 119; the constant is 119. Numbers are acquisitionPerBattle in src/trader.js run on the current codex. The 2026-08-25 simulation (tools/sim_trader.mjs, 300,000 battles per floor, within 1% of the model) predates #772 and still carries the old Ichor ladder; its Ichor column is not re-run.

CurrencyFloor 99: per battleper hourweight (Ichor = 1)Floor 119: per battleper hourweight (Ichor = 1)
Scrap4.835264.2830.23,2870.915
Ichor20.62,249127.63,0071
Dust1.3114315.73.433748.04
Essence1.4716014.13.794147.27
Ink1.3114315.73.433748.04

Per hour at 109 battles/h (dfmc-client tools/sim_econ.mjs BATTLES_PER_HOUR). Every cell is acquisitionPerBattle (src/trader.js) run under node on the current codex by tools/codex_tables.py.

Parity matrix at floor 99 — units of the row currency for ONE unit of the column currency:

ScrapIchorDustEssenceInk
Scrap—0.2343.673.293.67
Ichor4.28—15.714.115.7
Dust0.2720.0637—0.8961.00
Essence0.3040.07111.12—1.12
Ink0.2720.06371.000.896—

Parity matrix at floor 119 (the reference floor):

ScrapIchorDustEssenceInk
Scrap—1.098.797.958.79
Ichor0.915—8.047.278.04
Dust0.1140.124—0.9041.00
Essence0.1260.1381.11—1.11
Ink0.1140.1241.000.904—

What moves between the two: the Apex band’s drop grades push Scrap up 6.2× per battle (floor 99 gear dismantles at the 91–100 band’s weights; the Apex band starts at Rare), Ichor 1.3× (Apex spawn mix and egg dismantles; the level term caps at 100), the enhancement currencies 2.6×. So Scrap is worth 4.3 Ichor of time at 99 and 0.9 at 119; Dust, Essence and Ink sit at 16 / 14 / 16 Ichor at 99 and 8 / 7 / 8 at 119; Dust and Ink are always 1:1 and Essence is ~10% cheaper (tags dismantle to Essence as well as Scrap).

Worked numbers (premium 0.25)

FloorScrap / battleIchor / battleDust / battleEssence / battleInk / battle
10.5269.870.08800.1170.0880
30 (boss floor: farms 29)1.0712.80.2640.2940.264
60 (boss floor: farms 59)2.6315.20.5280.5880.528
90 (boss floor: farms 89)5.0819.70.9801.090.980
100 (boss floor: farms 99)4.8320.61.311.471.31
10117.527.11.501.801.50
120 (boss floor: farms 119)30.227.63.433.793.43

An Ichor week at the reference floor 119: 1,000 Scrap buys 731 Ichor; 10 Dust buys 64 Ichor (time parity would be 8.04 Ichor per Dust; 6.43 after the cut). A Dust week at 119: one Dust costs about 10.0 Ichor, or 11 Scrap.

A note the model surfaced (not changed): drop_rates.csv says each lane lands “exactly 10% per battle (3 kills)”, but the authored per-kill chances, mix-weighted on the encounter weights, come to ~6.7% per kill — about 19% per battle per lane, roughly twice the stated figure. The trader's ratios are unaffected (every drop-fed currency scales together), but the sentence and the table disagree; D's call which is right.

The build

PieceWhereState
Rate modelsrc/trader.js — acquisitionPerBattle, traderRates, traderQuote, the rotation, the weekend rule. Pure; the worker and the gate both import it.built, gated (tools/probe_trader_model.mjs 10/10: every floor yields every currency, boss floors step down, five weeks name five currencies, weekend rule, rate = parity × premium, integer quotes, doubling an authored yield doubles its rate, halving the drop chances halves every drop-fed yield)
Quote doorGET /api/shop/trader — open?, week, target, floor (this account’s best floor as the ratchet records it, T5), per-battle yields at that floor, the rates, next opening. The trade door prices at the same floor. The panel head reads “RATES FOLLOW YOUR BEST FLOOR (N)”.built
Trade doorPOST /api/shop/trade {idem, realm, lines:[{from, amount}]} — open every day since #770 (weekend-gated before), every line priced in the Worker, validated against the stored wallet in the player's object, the exchange written into the stored save, idempotent on idem, 30 s cooldown, wallet rate classbuilt
Shop tabSHADOW TRADER beside PREMIUM, all week (#770), the UX above — every currency it names carries its icon from the shared currency set (equipment header, reward window, bag)built; beautifying it is a backlog item (D)
Gatetools/probe_trader_e2e.py on the full stack, 23/23: closed Monday, open Saturday at 119, a two-line trade with the exchange in the stored save before any client save, replay, cooldown, insufficient, self-sale refused, a fresh client boots onto the traded wallet, its save lands with no wallet flag, then the tab driven through the real DOM including the flush-first rulegreen

Changelog

2026-11-03 — codex review (dfmc-client lane codexreview-a; D 2026-11-03 “review the codex, clean it up, update whatever needs updating”). WIKI ONLY. The rules table no longer says the weekend window is still live on release (it ended with 0.71); rotation (Ichor, Scrap, Dust, Essence, Ink), the 0.25 cut, the 30-second cooldown, the all-week door and the own-best-floor pricing (T5) re-checked against src/trader.js / src/worker.js on dev 0.93 — unchanged.

2026-10-23 — C17 part 3 (dfmc-client lane c17-gen-2): every table and figure on this page is generated by running acquisitionPerBattle under node. WIKI ONLY; no game code changed. The tables were pasted on 2026-09-30 and the codex has moved since: floor 99 Scrap per battle 9.68 → 4.83, floor 119 Scrap 60.3 → 30.2 and Ichor 42.2 → 27.6, so at 119 Scrap is now worth 0.9 Ichor of time (was 0.7). Per hour is at the sim’s 109 battles/h (tools/sim_econ.mjs), not “one battle every 15 s”. The worked example now reads 1,000 Scrap → 731 Ichor (was 559). The worked-numbers rows on floors 30 / 60 / 90 / 100 / 120 are boss floors and say which wild floor they price.

2026-10-22 — the trader prices at the account’s own best floor; no cap (T5, D 2026-10-18, Tower rulings form; Bug Run 0.89-4 board Q14). LIVE on release since 2026-10-23 (0.89). D: “Rebalance the trader, rebalance the income, but no cap on trader.” The 2026-08-25 rule priced everyone at floor 119, so the trader was cheaper than farming for every climber below the Apex band (min price/farm ratio by band at 109 battles/h: 1-49 0.10, 50-89 0.28, 90-109 0.98, 110-119 1.25). Now GET /api/shop/trader and POST /api/shop/trade read the account’s progress ratchet through one internal door on its player object and price at that floor (a boss floor at the wild floor below); every (from, target) pair is 1.25× farming at every band, so the proposal’s 40% cut is not taken and TRADER_PREMIUM stays 0.25. traderRates refuses without a floor; TRADER_REFERENCE_FLOOR is the tabled example only. No weekly cap was built. The income half (REC2) is on admin-backend — Dismantle yield: Epic+ egg Ichor ×0.4, Gear/Tag Scrap ×0.75, tier-10 ×0.5; with it the per-floor yields on this page shift (119: Ichor 42.2 → 27.6 per battle, Scrap 60.3 → 30.2) and the rates follow on the next request. Lands with the wipe; no migration. Gates (dfmc-client): tools/probe_econ_bands.mjs 10/12 → 12/12 (in gates.sh), tools/probe_t5_trader_band.py on the full stack, red before (both accounts quoted 119); probe_trader_model.mjs and probe_trader_e2e.py re-aimed from “floor 119 for everyone”.

2026-10-01 — the Shadow Trader lives (SHOP-FX, D, Bug Run 0.89 board Q7). LIVE on release since 2026-10-23 (0.89). PRESENTATION ONLY. D: the painting “should also breathe, his eyes should shine brighter every few seconds, the coin in his hand should have wraithfire gently burning around it.” The painting breathes (scale 1.006 × 1.013 from his feet, 5.2 s); a baked eye-shine layer (white core, cyan halo, a thin glint) flares over his eyes every 4.6 s; a 16-frame baked wraithfire strip burns round the raised coin (steps, 1.6 s, its opacity breathing over 3.1 s), clear of the coin’s face and his fingers. All of it is pre-baked (tools/make_shop_fx_art.py → img/shop/fx/, dfmc-client) and moves only transform and opacity, the #1052 portal rule; the painting’s CSS drop-shadow is a baked static shadow now. The conversion sliders wear the theme’s gold (accent-color) instead of the browser’s blue. Rates, ceilings, the cooldown and the trade call are unchanged. Gate tools/probe_shopfx.py (dfmc-client) 5/24 → 24/24.

2026-10-01 — the tab is reskinned and its cards fill the frame (SHOP-UI, D, Bug Run 0.89 board Q7). LIVE on release since 2026-10-23 (0.89). PRESENTATION ONLY. D, after the #1050 premium-shop rework: the tab buttons, CANCEL and CONFIRM read out of theme; the conversion cards should be “bigger vertically with bigger fonts” and “resize equally to fill the square”. CANCEL and CONFIRM (both the slider step and the confirmation step) are the arena’s hammered-bronze plate (img/pvp/queue.webp; CANCEL quenched, an unavailable CONFIRM faded, both burn their runes on press); each pay card is the catalogue row’s plate and takes an equal share of the frame’s height, its type 23/21/16 → 30/28/20 px and icons 22 → 30. Rates, ceilings, the cooldown and the trade call are unchanged. Gate tools/probe_shopui.py (dfmc-client) 6/24 → 24/24.

2026-09-30 — review F387: the simulator is on the #772 curve too. TOOLS ONLY. tools/sim_trader.mjs, the Monte Carlo that checks this model through the worker's own pieces, still paid Ichor per kill on the old /20 level term and 1/1.5/2.5/4/7 ladder, so it could only disagree with src/trader.js. It now uses round(2 × (1 + min(100, F)/100) × mult) with the ladder read from src/tables.json, as the worker does. Floor 99, 20,000 battles: Ichor 21.06 simulated vs 21.07 modelled (0.9993). No rate changed.

2026-09-30 — review drift fixes: F118, F119. WIKI ONLY. The model, both yield tables, both parity matrices and the worked numbers are restated on the #772 Ichor curve (level/100, ladder 1/1.1/1.2/1.35/1.5) that src/trader.js has priced with since 2026-09-10; they were the old /20 and 1/1.5/2.5/4/7 numbers (floor-99 Ichor 108.2 per battle, now 21.1). The rules table still said the reference constant held 99; it is 119 (D, 2026-08-25). DESCRIPTIVE ONLY.

2026-09-17 — the tab fits the window; no scrollbar (#958, D). ON DEV. D: “shadow trader window should not have a scroll, make the shop window bigger and resize items inside of it so it never needs to be scrolled”. The tab laid out at 1162 stage px inside a panel offering 926 — over by 236, and since the stage is a fixed 1920×1080 box scaled by fitScale, that overflow was present at every viewport, not only the 1423×687 it was reported from. Split between the box and its contents: the shared shop panel goes to 94% height and min(1320px,94%) wide with tighter padding (see Premium Shop), and each pay row comes down from 135 to ~115 stage px — padding 14/18 → 10/16, inner gaps 8 → 6, and one step off each type size with its currency icon following (title 26 → 23, rate label 24 → 21, the y of N footer 18 → 16); the ACQUIRING line 44 → 34, the CANCEL / CONFIRM buttons and the tab strip likewise. Geometry only — rates, the weekly target, the cooldown, wallet math and the confirm flow are untouched. Leaves 66 stage px of slack on the tab, 332 on its confirm step. Gate tools/probe_958.py 5/7 red → 7/7, measured in stage units and carrying an anti-cheat pair so a fit bought by shrinking the controls to nothing would fail; tools/probe_trader_ui.py still 6/6.

2026-09-07 — open all week, next week's resource named, currency icons throughout (#770, D). LIVE on release since 2026-09-08 (0.71). The weekend gate is retired — the trader is there every day and the Monday rotation is the only cadence; the quote carries next week's resource and the tab says it; every currency in the tab (pay rows, header, ACQUIRING, the confirm window) draws its icon from the shared currency set. Gates: tools/probe_trader_window.mjs (all 168 hours open, rotation turns at the boundary only), tools/probe_trader_e2e.py 23/23 full stack, tools/probe_trader_ui.py 6/6.

2026-08-29 — the trader now keeps the hours he was meant to (#628). LIVE on release since 2026-09-01 (0.65). He ran Saturday and Sunday by UTC, so on a Friday he simply was not there. The window is now Friday noon to Monday noon JST — three days rather than two. The countdown to his return moved with it; it had been counting to Saturday, which would have named the wrong day for anyone waiting. One rule decides, on the server, and the shop tab appears only when his answer says he is here.