From 04a4af0299d0d54c83f4552843cdc5591908cae7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 03:15:59 -0500 Subject: [PATCH 01/15] codex/economy/flip-candidates.yaml: GE flipping entry (epic #1 P0 backlog) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the epic's uncovered P0 economy backlog item (flip candidates). Verified source chain 2026-08-27: prices.runescape.wiki mapping + latest, all 15 shortlist limits cross-checked against Module:Exchange (agree); GE tax verified at 2% since 2025-05-29 (1% at 2021 intro), 5M/item cap, sub-50gp effectively untaxed; Merchanting + Grand Exchange/Buying limits pages cited for mechanics. Net model: 0.98*sell - buy at band edges. Gate: python3 tools/validate.py exit 0 (2 entries, 0 errors, 1 pre-existing warning on the golden entry's researched flag — none added by this entry). --- codex/economy/flip-candidates.yaml | 248 +++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 codex/economy/flip-candidates.yaml diff --git a/codex/economy/flip-candidates.yaml b/codex/economy/flip-candidates.yaml new file mode 100644 index 0000000..c32b0d1 --- /dev/null +++ b/codex/economy/flip-candidates.yaml @@ -0,0 +1,248 @@ +id: flip-candidates +title: GE Flipping (margin trading) +domain: economy +tags: [flipping, merchanting, ge, money-making, hybrid] +status: verified +version: 1 +updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date + +summary: > + Buy-and-sell Grand Exchange margin flipping: place resting buy offers at + the low edge of an item's price band and resting sell offers at the high + edge, harvesting the spread on high-volume items. There are no skill or + quest requirements — capital breadth and slot discipline are the whole game. + +accounts: + membership: hybrid # flipping works on F2P and members accounts alike + min_level: 3 # post-Tutorial Island baseline; no skill requirement + requires_quest: [] + requires_other: + - "Post-Tutorial Island account with Grand Exchange access (no skill or quest requirement)" + +best_practice: + equipment: + - item: cash stack (flip capital) + why: "Gross margin per item is a few gp, so profit scales with quantity and the 4h + buy-limit window caps per-item volume — capital breadth (more candidates/slots + covered) matters more than any gear. Bulk flipping at scale needs ~20M+ cash." + source: "OSRS wiki — Merchanting (Bulk quantity flipping)" + techniques: + - name: flip the spread, not the trend + why: "Flipping profit is the buy/sell spread between pending offers (the impatience + premium), NOT price movement; it is safer when the item has high trade volume." + source: "OSRS wiki — Merchanting (Flipping)" + - name: prefer dynamic-price items, avoid static-price items + why: "Static-price items have no spread to harvest; only dynamic-price items carry + an exploitable margin." + source: "OSRS wiki — Merchanting (flipping guidance: 'strongly recommended to not + flip static-price items')" + - name: quote at the band edges, never instant-buy/sell + why: "Instant matching pays the standing offer's price (the market's impatience + price); a resting offer fills when a matching offer appears at or better than your + price. The 2% convenience fee makes sub-2%-spread staples unprofitable at band + edges outright, so the edges are where the margin lives." + source: "OSRS wiki — Merchanting (How the Grand Exchange works); OSRS wiki — Grand Exchange + (2% tax since 2025-05-29, capped 5M/item)" + - name: pace quotes inside the 4h buy-limit window, watch connected limits + why: "The buying rate is restricted to a set quantity every 4 hours per player, and + some items share a connected limit with related items — treat those as one bucket + when filling slots." + source: "OSRS wiki — Grand Exchange (Buy limits; Grand Exchange/Buying limits)" + +economy: + # Model (documented, reproducible from the snapshot): + # per slot: buy `limit` units at the low edge, sell at the high edge. + # net_pu = 0.98 * sell - buy (2% convenience fee on the sale; the 5M/item cap and + # the sub-50gp rounding exemption are not hit at these prices) + # 4h/slot = net_pu * limit (buy-limit window caps per-4h volume) + # Snapshot 2026-08-27: prices.runescape.wiki mapping + latest; limits cross-checked + # against Module:Exchange/ (all agree). Top 8 by 4h/slot below in profit_drivers. + item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members) + item_id: 44 + cost_per_cycle: 223 # buy @ low edge per unit (snapshot 2026-08-27) + income_per_cycle: 244 # sell @ high edge per unit, gross (snapshot 2026-08-27) + net_per_cycle: 16.12 # 0.98 * 244 - 223 (2% fee applied to the sale) + gp_per_hour_low: 69000 # 50% of the top-8 modeled upper bound — partial fills/drift + gp_per_hour_high: 138000 # top-8 modeled upper bound: sum(4h/slot) / 4 (8 slots, full fills) + xp_per_hour: 0 # flipping awards no XP + supplies: + - item: Rune arrowtips # canonical wiki names per action-grammar conventions + qty_per_cycle: 1 + source: ge + ge_limit: 10000 # mapping + Module:Exchange agree + - item: Iron bar + qty_per_cycle: 1 + source: ge + ge_limit: 10000 + - item: Cosmic rune + qty_per_cycle: 1 + source: ge + ge_limit: 18000 + profit_drivers: + - "Spread per unit x trade volume; volume is the safety factor (high-volume items fill faster and drift less)" + - "Slot breadth: cover as many net-positive candidates as capital allows (standard 8 slots)" + - "Snapshot 2026-08-27 top 8 by modeled 4h/slot: Rune arrowtips 161,200 (buy 223 / sell 244 / limit 10000); Iron bar 88,600 (145/157/10000); Cosmic rune 61,920 (122/128/18000); Magic logs 61,680 (772/793/12000); Silver ore 57,460 (73/79/13000); Mud rune 53,640 (96/101/18000); Raw swordfish 41,100 (206/213/15000); Raw lobster 27,000 (106/110/15000)" + - "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells" + +state_machine: + initial: idle + terminal: [complete, failed] + states: + idle: + goal: be logged in at a bank with a GE booth, capital >= floor + next: [scan, complete, failed] + scan: + goal: read the current high/low band for every candidate + next: [buy, idle] + buy: + goal: place one resting buy offer per slot at or below the low edge + next: [wait_fill, collect, idle] + wait_fill: + goal: hold offers; re-quote any approaching the 4h window lapse + next: [sell, buy, collect] + sell: + goal: place one sell offer per filled slot at or above the high edge + next: [wait_fill, collect] + collect: + goal: collect proceeds, deposit coins, log the cycle + next: [scan, complete, idle] + complete: + goal: session GP target reached + failed: + goal: hard failure — stop, log ERROR sentinel, alert + transitions: + - from: idle + when: "capital below floor OR session target reached" + to: complete + - from: scan + when: "no candidate net-positive at band edges on 3 consecutive scans" + to: idle + - from: buy + when: "all slots quoted" + to: wait_fill + - from: wait_fill + when: "all pending buys filled" + to: sell + - from: wait_fill + when: "4h window lapse with unfilled offers" + to: collect + - from: sell + when: "sells placed" + to: wait_fill + - from: collect + when: "session GP target reached" + to: complete + - from: collect + when: "rotation done and window open" + to: scan + +actions: + - id: a1 + step: reach-ge + verb: move_to + target: "bank with Grand Exchange booth" + params: + location: "Grand Exchange (Varrock) area bank" + method: [teleport, walk] + on_fail: retry + anti_ban: + delay: [400, 1600] + + - id: a2 + step: open-ge + verb: ge_open + target: grand-exchange + params: + booth: "GE counter or bank booth" + on_fail: retry + anti_ban: + delay: [800, 1800] + + - id: a3 + step: scan-bands + verb: ge_check_price + target: "current candidates" + params: + items: "top-N candidates from the entry table" + band: "high and low edge per item" + on_fail: skip + anti_ban: + delay: [1200, 2800] + + - id: a4 + step: place-buys + verb: ge_buy + target: grand-exchange + params: + item: "per candidate slot" + qty: "min(remaining 4h limit, slot capacity)" + price: "low edge (rounded down)" + max_price: "low edge — never instant" + on_fail: skip + anti_ban: + delay: [900, 2700] + + - id: a5 + step: hold-fills + verb: sleep + params: + delay: [6000, 12000] + until: "ge_offers_done changes or 4h window lapse" + on_fail: retry + anti_ban: + delay: [300, 900] + + - id: a6 + step: place-sells + verb: ge_sell + target: grand-exchange + params: + item: "per filled slot" + qty: "filled quantity" + price: "high edge (rounded up)" + on_fail: skip + anti_ban: + delay: [900, 2700] + + - id: a7 + step: collect-and-log + verb: ge_collect + target: grand-exchange + params: + deposit: "coins to bank" + log: "RESULT item, qty, net gp per slot" + on_fail: alert + anti_ban: + delay: [600, 1400] + +run_control: + iteration: "one full buy -> fill -> sell -> collect rotation across all slots" + max_iterations: null + break: + every: [6, 10] # rotations + for: [60, 240] # seconds of idle AFK + stop_conditions: + - "session GP target reached" + - "capital under floor" + - "no net-positive candidates on 3 consecutive scans" + - "3 consecutive aborted offers (client issue — do not loop)" + +risk: + ban_level: low + notes: + - "Flipping is standard gameplay; the detectable signal is quote churn — keep per-offer pacing humanised, never instant-buy/sell loops." + - "The 2% fee (since 2025-05-29) and 5M/item cap make sub-2%-spread staples unprofitable at band edges; re-scan before every rotation." + - "Thin/volatile items: a resting offer can sit for hours — cap quote lifetime to the 4h window and re-quote rather than chase." + +sources: + - "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h; connected limits; verified 2026-08-27)" + - "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics)" + - "OSRS wiki — Merchanting (flipping = spread exploitation; high trade volume = safety; bulk flipping needs ~20M+ cash; avoid static-price items; offer matching at or better than your price)" + - "prices.runescape.wiki/api/v1/osrs/mapping (item ids, members flags, limits — limits cross-checked against Module:Exchange/, all agree)" + - "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)" +verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing) + +related: + - low-alch-f2p + - f2p-alch-catalog + - mule-topup From 587da06ef009e9e3d364cb822fd3dc0a98a48a3c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 03:19:52 -0500 Subject: [PATCH 02/15] codex: members low-alchemy entry (loss-minimizing) Live-verified 2026-08-27 against prices.runescape.wiki mapping/latest (members catalog: lowalch != null, members=true, 3232 tradeable items) plus OSRS wiki spell infoboxes and Module:Exchange limits. - Same state machine + actions as low-alch-f2p (golden template) - Top-10 least-loss members candidates in economy.profit_drivers - Handoff note verified from LA page: switch to HLA at 55 (hard level gate + strictly more coins and xp per cast); Superheat Item (43) is a smithing production spell, not the magic-XP handoff - best_practice: staff of fire, noted items, Bryophyta's staff + Tome of Fire combo, self-crafted natures via Abyss; every line sourced - accounts: membership members, min_level 21 Closes #2 --- codex/economy/low-alch-members.yaml | 198 ++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 codex/economy/low-alch-members.yaml diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml new file mode 100644 index 0000000..473d2a4 --- /dev/null +++ b/codex/economy/low-alch-members.yaml @@ -0,0 +1,198 @@ +id: low-alch-members +title: Low-Level Alchemy (Members) +domain: economy +tags: [alching, magic-training, members, loss-training] +status: verified +version: 1 +updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date + +summary: > + Magic training from level 21 by low-alching members items with the lowest + loss per cast (high-limit, high-volume candidates first). Same state + machine and actions as low-alch-f2p; the catalog is the members item pool + (mapping lowalch != null, members=true, live GE prices 2026-08-27). At + current prices the method burns a small amount of GP per cast for 31 XP — + run it for XP, not GP. Handoff: switch to High Level Alchemy at level 55 + (hard level gate + strictly more coins AND XP per cast; see sources). + +accounts: + membership: members + min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified) + requires_quest: [] + requires_other: + - "Staff of Fire equipped (removes 3 fire runes from the cost)" + - "A bank with GE access within reach" + +best_practice: + equipment: + - item: staff of fire + why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved)" + source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)" + techniques: + - name: cast on noted (stacked) items + why: "one inventory slot, minimal mouse travel between spell icon and item, no item un-noting" + source: "OSRS wiki — Low Level Alchemy (Usage: casting alchemy spells on noted items)" + - name: Bryophyta's staff + Tome of Fire (members-only, 50 Magic) + why: "charged Bryophyta's staff holds up to 1,000 nature runes (0 rune inventory slots) with a 1/15 chance per cast to not consume a nature rune; charged Tome of Fire supplies infinite fire runes in the shield slot and non-combat spells (alchemy) do not consume charges — the wiki documents this exact pair for alchemy profit; payback takes thousands of casts, so adopt at 50+ Magic when nature prices are high" + source: "OSRS wiki — Bryophyta's staff (passive effect; 'can be used along with a tome of fire to increase profits with High Level Alchemy'); OSRS wiki — Tome of Fire (level 50 Magic, non-combat spells do not consume charges)" + - name: self-craft nature runes as a cost cap + why: "members with 44 Runecraft can craft natures from pure essence, capping the per-cast rune cost at essence+time when GE nature prices spike; use the Abyss (no talisman needed) from the Mage of Zamorak" + source: "OSRS wiki — Nature rune (level 44 Runecraft, pure essence, 9 xp); OSRS wiki — Abyss (runic altar access)" + +economy: + # Verified 2026-08-27 against prices.runescape.wiki (live 'latest') + + # OSRS wiki (spell infoboxes, Module:Exchange limits, item pages). + item: Rune arrow # example anchor; rank by least-loss candidate live + item_id: 892 + cost_per_cycle: 188 # arrow buy 44 + nature rune 144 (staff of fire) + income_per_cycle: 160 # lowalch of rune arrow (40% of value 400) + net_per_cycle: -28 + gp_per_hour_low: -60000 # negative: this burns GP (31 xp/cast is the product) + gp_per_hour_high: -40000 + xp_per_hour: 55800 # 31 xp/cast (wiki) @ ~1800 casts/hr (3-tick, wiki) + supplies: + - item: rune arrow + qty_per_cycle: 1 + source: ge + ge_limit: 11000 # mapping + Module:Exchange/Rune arrow agree (wiki-verified) + - item: nature rune + qty_per_cycle: 1 + source: ge + ge_limit: 18000 + profit_drivers: + - "No profit to chase — this is loss-minimizing training: XP is the product. Pick the least-loss candidate live each run." + - "Top 10 losses 2026-08-27 (rank by (lowalch - buy), members catalog, mapping 'latest' low as buy basis):" + - "1. Mystic mist staff (id 20733, lowalch 17000, buy 1000, spread +16000, GE limit 8)" + - "2. Trailblazer top (t1) (id 25031, lowalch 6000, buy 555, spread +5445, GE limit 5)" + - "3. Twisted trousers (t2) (id 24401, lowalch 4000, buy 100, spread +3900, GE limit 5)" + - "4. Dual sai (id 23206, lowalch 2400, buy 1500, spread +900, GE limit 8)" + - "5. Adamant hasta(p) (id 11407, lowalch 832, buy 1, spread +831, GE limit 125)" + - "6. Adamant spear(p++) (id 5726, lowalch 832, buy 10, spread +822, GE limit 125)" + - "7. Bagged flower (id 8451, lowalch 2000, buy 1250, spread +750, GE limit 10000)" + - "8. Adamant spear(p) (id 1259, lowalch 832, buy 200, spread +632, GE limit 125)" + - "9. Frying pan (id 7441, lowalch 664, buy 101, spread +563, GE limit 50)" + - "10. Maple blackjack(d) (id 6420, lowalch 640, buy 100, spread +540, GE limit 125)" + - "Caveat: thin items (limits 5-8) cannot sustain a run and their 'low' fills can be stale — a bot cannot reliably buy at those prices. The sustainable loss-minimizers are the liquid high-limit set: rune arrow (892, -28/cast), rune javelin tips (19580, -7/cast), opal amulet (u) (21099, -199/cast), jade ring (21084, -63/cast)." + +state_machine: + initial: idle + terminal: [complete, failed] + states: + idle: + goal: reach the bank + have supplies in inventory + next: [casting, bank_supplies] + casting: + goal: cast Low Level Alchemy on the current candidate item + next: [bank_supplies, idle, complete, failed] + bank_supplies: + goal: bank the produced coins, withdraw fresh candidate + runes + next: [casting, idle] + complete: + goal: reached the run's XP/level target + failed: + goal: hard failure — stop, log ERROR sentinel, alert + transitions: + - from: casting + when: "candidate item count < buffer OR nature rune count < buffer" + to: bank_supplies + - from: idle + when: "magic level >= target (default 55, hand off to HLA at 55)" + to: complete + +# --- handoff note (wiki-verified 2026-08-27, do not copy from other entries) --- +# Low Level Alchemy: level 21, 31 xp/cast, 1 nature + 3 fire (spellbook: Normal). +# Superheat Item: level 43, 53 xp/cast, 1 nature + 4 fire — a SMITHING +# production spell (ore -> bar), NOT the magic-XP handoff; as pure Magic +# training it is only a soft side option (e.g. training Smithing alongside). +# High Level Alchemy: level 55, 65 xp/cast, 1 nature + 5 fire. +# Handoff: the LA page itself states "Players with level 55 Magic are +# generally recommended to use High Level Alchemy instead, as it provides +# more coins and experience per cast than Low Level Alchemy". So the switch +# is level 55: a HARD level gate (HLA uncastable below 55) plus strict +# dominance afterwards (with a fire staff both cost 1 nature/cast, and HLA +# pays 60% of value + 65 xp vs LA's 40% + 31 xp) — after 55, LA is strictly +# worse for Magic XP and is never the right call except when deliberately +# burning GP at minimum XP (never recommended). + +actions: + - id: a1 + step: reach-bank + verb: move_to + target: bank + params: + location: "nearest bank with GE" + method: [teleport, walk] + on_fail: retry + anti_ban: + delay: [400, 1600] + + - id: a2 + step: buy-supplies + verb: ge_buy + target: grand-exchange + params: + item: "current candidate item" + qty: "fill to buffer" + max_price: "live low + 5%" + on_fail: retry + anti_ban: + delay: [1200, 3000] + + - id: a3 + step: cast-alch + verb: use_item_on + target: "candidate item in inventory" + params: + spell: "Low Level Alchemy" + item: "least-loss live candidate in inventory" + on: "itself" + on_fail: retry + anti_ban: + delay: [1600, 3400] # 3-tick cast = ~5.4s game; stay under it, humanised + + - id: a4 + step: bank-loop + verb: bank_inventory + target: bank + params: + deposit: [all coins, all products] + withdraw: "candidate item + nature runes up to buffer" + on_fail: alert + +run_control: + iteration: "one cast" + max_iterations: null + break: + every: [8, 14] # bank cycles (breaks land on bank visits) + for: [45, 120] # seconds of idle AFK + stop_conditions: + - "magic level >= target (55 -> hand off to HLA)" + - "gp under floor" + - "3 consecutive failed casts (client issue — do not loop)" + +risk: + ban_level: low + notes: + - "High cast cadence is the main detection vector; keep anti_ban delays." + - "GE buy bursts: pace offers, respect the 4h limit window; thin items (limit < 100) cannot sustain a long run." + - "Thin-item 'latest low' fills can be stale — confirm fill price with a live GE offer before committing a run; the liquid high-limit set is the dependable loss-minimizer." + +sources: + - "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, 1 nature + 3 fire; spellbook Normal; infobox 2026-08-27)" + - "OSRS wiki — High Level Alchemy (level 55, 65 xp/cast, 1 nature + 5 fire; 60% of item value; infobox 2026-08-27)" + - "OSRS wiki — Superheat Item (level 43, 53 xp/cast, 1 nature + 4 fire; smithing production spell; infobox 2026-08-27)" + - "OSRS wiki — Low Level Alchemy (handoff quote: 'Players with level 55 Magic are generally recommended to use High Level Alchemy instead, as it provides more coins and experience per cast than Low Level Alchemy')" + - "prices.runescape.wiki mapping 2026-08-27 (members/lowalch/highalch/limit catalog; lowalch == highalch*2//3 asserted for cited items)" + - "prices.runescape.wiki latest 2026-08-27 (low = per-repo buy basis; rune arrow buy 44, nature rune buy 144)" + - "OSRS wiki — Module:Exchange/Rune arrow, Nature rune, Staff of fire + top-10 candidates (GE limits agree with mapping; verified 2026-08-27)" + - "OSRS wiki — Rune arrow / Bagged flower / Mystic mist staff etc. item pages (members = Yes for every cited catalog item; verified 2026-08-27)" + - "OSRS wiki — Nature rune (members craft at level 44 Runecraft from pure essence, 9 xp; Nature Altar south-east of Tai Bwo Wannai)" + - "OSRS wiki — Abyss (access to runic altars without talismans via the Mage of Zamorak)" + - "OSRS wiki — Bryophyta's staff (level 30 Attack + Magic; up to 1,000 nature charges; 1/15 chance to not consume a charge on any nature-rune spell; alchemy combo with Tome of Fire)" + - "OSRS wiki — Tome of Fire (members, level 50 Magic to wield; charged tome is an infinite fire-rune source and non-combat spells do not consume charges)" +verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing) + +related: + - low-alch-f2p + - high-alch-p2p + - mule-topup From 9651691e9f6e2a1138656ee1fae9518fb6a36119 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 03:22:50 -0500 Subject: [PATCH 03/15] codex: F2P alch catalog dataset (machine-readable) Generated 2026-08-27 from prices.runescape.wiki mapping + latest: filter lowalch != null, members=false, GE limit >= 150 -> 303 items, sorted by buy price asc. Columns: item, id, buy (latest.low), sell (latest.high), limit, lowalch_price, value, net_per_cycle (= lowalch_price - buy). Source of truth for least-loss ranking referenced by low-alch-f2p / low-alch-members (related: f2p-alch-catalog). Validator already skips underscore-prefixed dataset files (verified: exit 0, file not counted as an entry). Closes #6 --- codex/economy/_f2p-alch-catalog.yaml | 2435 ++++++++++++++++++++++++++ 1 file changed, 2435 insertions(+) create mode 100644 codex/economy/_f2p-alch-catalog.yaml diff --git a/codex/economy/_f2p-alch-catalog.yaml b/codex/economy/_f2p-alch-catalog.yaml new file mode 100644 index 0000000..8c89f69 --- /dev/null +++ b/codex/economy/_f2p-alch-catalog.yaml @@ -0,0 +1,2435 @@ +# Dataset: F2P alch catalog (machine-readable) +# Generated 2026-08-27 from prices.runescape.wiki mapping + latest. Source of truth +# for "rank by cheapest/least-loss" in low-alch-f2p and low-alch-members. +# Validator intentionally skips files starting with "_". +fetched: '2026-08-27' +source: https://prices.runescape.wiki/api/v1/osrs/mapping + /api/v1/osrs/latest (live, at implement time) +filter: lowalch != null, members=false, ge limit >= 150 +sorted_by: buy (latest.low) ascending +note: buy = latest.low, sell = latest.high (per repo data-sources.md convention). net_per_cycle = lowalch_price - buy; nature/fire runes NOT included (staff of fire reduces rune cost to 1 nature/cast). +entries: 303 +items: +- item: Pure essence + id: 7936 + buy: 1 + sell: 2 + limit: 30000 + lowalch_price: 1 + value: 4 + net_per_cycle: 0 +- item: Bronze bolts + id: 877 + buy: 2 + sell: 3 + limit: 7000 + lowalch_price: 1 + value: 0 + net_per_cycle: -1 +- item: Feather + id: 314 + buy: 2 + sell: 3 + limit: 30000 + lowalch_price: 1 + value: 2 + net_per_cycle: -1 +- item: Mind rune + id: 558 + buy: 2 + sell: 3 + limit: 18000 + lowalch_price: 1 + value: 3 + net_per_cycle: -1 +- item: Vial + id: 229 + buy: 2 + sell: 3 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -1 +- item: Jug + id: 1935 + buy: 3 + sell: 3 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -2 +- item: Jug of wine + id: 1993 + buy: 3 + sell: 4 + limit: 6000 + lowalch_price: 1 + value: 1 + net_per_cycle: -2 +- item: Team-3 cape + id: 4319 + buy: 3 + sell: 200 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: 17 +- item: Body rune + id: 559 + buy: 4 + sell: 5 + limit: 18000 + lowalch_price: 1 + value: 3 + net_per_cycle: -3 +- item: Bronze arrow + id: 882 + buy: 4 + sell: 4 + limit: 7000 + lowalch_price: 1 + value: 1 + net_per_cycle: -3 +- item: Eye of newt + id: 221 + buy: 4 + sell: 4 + limit: 13000 + lowalch_price: 1 + value: 3 + net_per_cycle: -3 +- item: Fire rune + id: 554 + buy: 4 + sell: 5 + limit: 50000 + lowalch_price: 1 + value: 4 + net_per_cycle: -3 +- item: Vial of water + id: 227 + buy: 4 + sell: 5 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -3 +- item: Air rune + id: 556 + buy: 5 + sell: 6 + limit: 50000 + lowalch_price: 1 + value: 4 + net_per_cycle: -4 +- item: Earth rune + id: 557 + buy: 5 + sell: 6 + limit: 50000 + lowalch_price: 1 + value: 4 + net_per_cycle: -4 +- item: Iron arrow + id: 884 + buy: 5 + sell: 4 + limit: 7000 + lowalch_price: 1 + value: 3 + net_per_cycle: -4 +- item: Team-13 cape + id: 4339 + buy: 5 + sell: 159 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: 15 +- item: Team-23 cape + id: 4359 + buy: 5 + sell: 229 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: 15 +- item: Team-47 cape + id: 4407 + buy: 5 + sell: 266 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: 15 +- item: Water rune + id: 555 + buy: 5 + sell: 6 + limit: 50000 + lowalch_price: 1 + value: 4 + net_per_cycle: -4 +- item: Fishing bait + id: 313 + buy: 6 + sell: 6 + limit: 8000 + lowalch_price: 1 + value: 3 + net_per_cycle: -5 +- item: Mithril arrow + id: 888 + buy: 6 + sell: 7 + limit: 7000 + lowalch_price: 12 + value: 32 + net_per_cycle: 6 +- item: Thread + id: 1734 + buy: 6 + sell: 7 + limit: 18000 + lowalch_price: 1 + value: 1 + net_per_cycle: -5 +- item: Grain + id: 1947 + buy: 7 + sell: 8 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -6 +- item: Bucket + id: 1925 + buy: 8 + sell: 8 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -7 +- item: Steel arrow + id: 886 + buy: 8 + sell: 8 + limit: 7000 + lowalch_price: 4 + value: 12 + net_per_cycle: -4 +- item: Black skirt + id: 1015 + buy: 10 + sell: 10 + limit: 150 + lowalch_price: 1 + value: 2 + net_per_cycle: -9 +- item: Copper ore + id: 436 + buy: 10 + sell: 11 + limit: 13000 + lowalch_price: 1 + value: 3 + net_per_cycle: -9 +- item: Maple logs + id: 1517 + buy: 10 + sell: 11 + limit: 15000 + lowalch_price: 32 + value: 80 + net_per_cycle: 22 +- item: Team-15 cape + id: 4343 + buy: 10 + sell: 177 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: 10 +- item: Unfired pot + id: 1787 + buy: 10 + sell: 102 + limit: 10000 + lowalch_price: 1 + value: 1 + net_per_cycle: -9 +- item: Pot + id: 1931 + buy: 11 + sell: 18 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -10 +- item: Priest gown (top) + id: 426 + buy: 11 + sell: 25 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -9 +- item: Rune essence + id: 1436 + buy: 11 + sell: 12 + limit: 20000 + lowalch_price: 1 + value: 4 + net_per_cycle: -10 +- item: Sardine + id: 325 + buy: 12 + sell: 13 + limit: 6000 + lowalch_price: 4 + value: 10 + net_per_cycle: -8 +- item: Silk + id: 950 + buy: 12 + sell: 14 + limit: 18000 + lowalch_price: 12 + value: 30 + net_per_cycle: 0 +- item: Bowl + id: 1923 + buy: 13 + sell: 21 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -12 +- item: Raw sardine + id: 327 + buy: 13 + sell: 15 + limit: 15000 + lowalch_price: 4 + value: 10 + net_per_cycle: -9 +- item: Tin ore + id: 438 + buy: 13 + sell: 14 + limit: 13000 + lowalch_price: 1 + value: 3 + net_per_cycle: -12 +- item: Jug of water + id: 1937 + buy: 14 + sell: 15 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -13 +- item: Trout + id: 333 + buy: 17 + sell: 18 + limit: 6000 + lowalch_price: 4 + value: 10 + net_per_cycle: -13 +- item: Raw anchovies + id: 321 + buy: 18 + sell: 19 + limit: 13000 + lowalch_price: 6 + value: 15 + net_per_cycle: -12 +- item: Beer glass + id: 1919 + buy: 20 + sell: 24 + limit: 10000 + lowalch_price: 1 + value: 2 + net_per_cycle: -19 +- item: Shrimps + id: 315 + buy: 20 + sell: 20 + limit: 6000 + lowalch_price: 2 + value: 5 + net_per_cycle: -18 +- item: Willow logs + id: 1519 + buy: 20 + sell: 21 + limit: 15000 + lowalch_price: 16 + value: 40 + net_per_cycle: -4 +- item: Longbow + id: 839 + buy: 21 + sell: 31 + limit: 18000 + lowalch_price: 32 + value: 80 + net_per_cycle: 11 +- item: Oak shortbow + id: 843 + buy: 21 + sell: 30 + limit: 18000 + lowalch_price: 40 + value: 100 + net_per_cycle: 19 +- item: Logs + id: 1511 + buy: 22 + sell: 23 + limit: 15000 + lowalch_price: 1 + value: 4 + net_per_cycle: -21 +- item: Onion + id: 1957 + buy: 22 + sell: 22 + limit: 13000 + lowalch_price: 1 + value: 3 + net_per_cycle: -21 +- item: Tomato + id: 1982 + buy: 22 + sell: 22 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -21 +- item: Wool + id: 1737 + buy: 22 + sell: 22 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -21 +- item: Blue skirt + id: 1011 + buy: 25 + sell: 38 + limit: 150 + lowalch_price: 1 + value: 2 + net_per_cycle: -24 +- item: Cabbage + id: 1965 + buy: 25 + sell: 26 + limit: 6000 + lowalch_price: 1 + value: 1 + net_per_cycle: -24 +- item: Chocolate bar + id: 1973 + buy: 25 + sell: 28 + limit: 13000 + lowalch_price: 4 + value: 10 + net_per_cycle: -21 +- item: Cooking apple + id: 1955 + buy: 25 + sell: 26 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -24 +- item: Bucket of water + id: 1929 + buy: 26 + sell: 28 + limit: 13000 + lowalch_price: 2 + value: 6 + net_per_cycle: -24 +- item: Garlic + id: 1550 + buy: 26 + sell: 30 + limit: 11000 + lowalch_price: 1 + value: 3 + net_per_cycle: -25 +- item: Raw chicken + id: 2138 + buy: 26 + sell: 27 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -25 +- item: Raw salmon + id: 331 + buy: 26 + sell: 29 + limit: 15000 + lowalch_price: 12 + value: 30 + net_per_cycle: -14 +- item: Potato + id: 1942 + buy: 27 + sell: 27 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -26 +- item: Rope + id: 954 + buy: 27 + sell: 34 + limit: 250 + lowalch_price: 7 + value: 18 + net_per_cycle: -20 +- item: Salmon + id: 329 + buy: 27 + sell: 29 + limit: 6000 + lowalch_price: 12 + value: 30 + net_per_cycle: -15 +- item: Adamant arrow + id: 890 + buy: 28 + sell: 26 + limit: 11000 + lowalch_price: 32 + value: 80 + net_per_cycle: 4 +- item: Egg + id: 1944 + buy: 28 + sell: 28 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -27 +- item: Raw shrimps + id: 317 + buy: 28 + sell: 29 + limit: 15000 + lowalch_price: 2 + value: 5 + net_per_cycle: -26 +- item: Bowl of water + id: 1921 + buy: 30 + sell: 42 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -29 +- item: Strength potion(1) + id: 119 + buy: 30 + sell: 25 + limit: 2000 + lowalch_price: 4 + value: 11 + net_per_cycle: -26 +- item: Team-28 cape + id: 4369 + buy: 30 + sell: 101 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -10 +- item: Team-34 cape + id: 4381 + buy: 30 + sell: 812 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -10 +- item: Team-39 cape + id: 4391 + buy: 30 + sell: 150 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -10 +- item: Team-43 cape + id: 4399 + buy: 30 + sell: 229 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -10 +- item: Meat pie + id: 2327 + buy: 31 + sell: 33 + limit: 10000 + lowalch_price: 6 + value: 15 + net_per_cycle: -25 +- item: Priest gown (bottom) + id: 428 + buy: 31 + sell: 34 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -29 +- item: Herring + id: 347 + buy: 34 + sell: 14 + limit: 6000 + lowalch_price: 4 + value: 10 + net_per_cycle: -30 +- item: Bucket of milk + id: 1927 + buy: 35 + sell: 35 + limit: 13000 + lowalch_price: 2 + value: 6 + net_per_cycle: -33 +- item: Steel nails + id: 1539 + buy: 35 + sell: 35 + limit: 13000 + lowalch_price: 1 + value: 3 + net_per_cycle: -34 +- item: Anchovies + id: 319 + buy: 37 + sell: 37 + limit: 15000 + lowalch_price: 6 + value: 15 + net_per_cycle: -31 +- item: Pie dish + id: 2313 + buy: 37 + sell: 39 + limit: 500 + lowalch_price: 1 + value: 3 + net_per_cycle: -36 +- item: White apron + id: 1005 + buy: 37 + sell: 49 + limit: 150 + lowalch_price: 1 + value: 2 + net_per_cycle: -36 +- item: Bones + id: 526 + buy: 38 + sell: 40 + limit: 3000 + lowalch_price: 1 + value: 1 + net_per_cycle: -37 +- item: Bear fur + id: 948 + buy: 39 + sell: 44 + limit: 18000 + lowalch_price: 4 + value: 10 + net_per_cycle: -35 +- item: Ashes + id: 592 + buy: 40 + sell: 41 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -39 +- item: Fiendish ashes + id: 25766 + buy: 40 + sell: 40 + limit: 3000 + lowalch_price: 1 + value: 1 + net_per_cycle: -39 +- item: Oak logs + id: 1521 + buy: 40 + sell: 42 + limit: 15000 + lowalch_price: 8 + value: 20 + net_per_cycle: -32 +- item: Oak longbow + id: 845 + buy: 40 + sell: 44 + limit: 18000 + lowalch_price: 64 + value: 160 + net_per_cycle: 24 +- item: Raw rat meat + id: 2134 + buy: 40 + sell: 50 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -39 +- item: Raw trout + id: 335 + buy: 40 + sell: 48 + limit: 15000 + lowalch_price: 4 + value: 10 + net_per_cycle: -36 +- item: Unblessed symbol + id: 1716 + buy: 40 + sell: 38 + limit: 10000 + lowalch_price: 80 + value: 200 + net_per_cycle: 40 +- item: Unstrung symbol + id: 1714 + buy: 40 + sell: 40 + limit: 10000 + lowalch_price: 80 + value: 200 + net_per_cycle: 40 +- item: Raw beef + id: 2132 + buy: 42 + sell: 44 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -41 +- item: Raw tuna + id: 359 + buy: 43 + sell: 44 + limit: 15000 + lowalch_price: 16 + value: 40 + net_per_cycle: -27 +- item: Strength potion(2) + id: 117 + buy: 43 + sell: 43 + limit: 2000 + lowalch_price: 5 + value: 13 + net_per_cycle: -38 +- item: Tuna + id: 361 + buy: 45 + sell: 45 + limit: 6000 + lowalch_price: 16 + value: 40 + net_per_cycle: -29 +- item: Energy potion(1) + id: 3014 + buy: 46 + sell: 46 + limit: 2000 + lowalch_price: 14 + value: 36 + net_per_cycle: -32 +- item: Pike + id: 351 + buy: 46 + sell: 71 + limit: 6000 + lowalch_price: 8 + value: 20 + net_per_cycle: -38 +- item: Chocolate dust + id: 1975 + buy: 49 + sell: 54 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -48 +- item: Blue cape + id: 1021 + buy: 50 + sell: 109 + limit: 150 + lowalch_price: 12 + value: 32 + net_per_cycle: -38 +- item: Red cape + id: 1007 + buy: 50 + sell: 50 + limit: 150 + lowalch_price: 1 + value: 2 + net_per_cycle: -49 +- item: Shortbow + id: 841 + buy: 50 + sell: 52 + limit: 18000 + lowalch_price: 20 + value: 50 + net_per_cycle: -30 +- item: Team-20 cape + id: 4353 + buy: 50 + sell: 194 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -30 +- item: Team-36 cape + id: 4385 + buy: 50 + sell: 57 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -30 +- item: Banana + id: 1963 + buy: 51 + sell: 51 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -50 +- item: Redberries + id: 1951 + buy: 54 + sell: 56 + limit: 13000 + lowalch_price: 1 + value: 3 + net_per_cycle: -53 +- item: Cheese + id: 1985 + buy: 55 + sell: 55 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -54 +- item: Team-7 cape + id: 4327 + buy: 55 + sell: 245 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -35 +- item: Ball of wool + id: 1759 + buy: 57 + sell: 57 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -56 +- item: Bronze bar + id: 2349 + buy: 60 + sell: 64 + limit: 10000 + lowalch_price: 3 + value: 8 + net_per_cycle: -57 +- item: Bread + id: 2309 + buy: 64 + sell: 65 + limit: 6000 + lowalch_price: 4 + value: 12 + net_per_cycle: -60 +- item: Raw pike + id: 349 + buy: 66 + sell: 71 + limit: 15000 + lowalch_price: 8 + value: 20 + net_per_cycle: -58 +- item: Team-6 cape + id: 4325 + buy: 66 + sell: 66 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -46 +- item: Apple pie + id: 2323 + buy: 67 + sell: 75 + limit: 10000 + lowalch_price: 12 + value: 30 + net_per_cycle: -55 +- item: Cooked chicken + id: 2140 + buy: 67 + sell: 69 + limit: 6000 + lowalch_price: 1 + value: 4 + net_per_cycle: -66 +- item: Cooked t-bone steak + id: 33109 + buy: 68 + sell: 79 + limit: 6000 + lowalch_price: 4 + value: 10 + net_per_cycle: -64 +- item: Kebab + id: 1971 + buy: 68 + sell: 70 + limit: 6000 + lowalch_price: 1 + value: 3 + net_per_cycle: -67 +- item: Beer + id: 1917 + buy: 70 + sell: 78 + limit: 2000 + lowalch_price: 1 + value: 2 + net_per_cycle: -69 +- item: Clay + id: 434 + buy: 70 + sell: 64 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -69 +- item: Cooked meat + id: 2142 + buy: 70 + sell: 71 + limit: 6000 + lowalch_price: 1 + value: 4 + net_per_cycle: -69 +- item: Grapes + id: 1987 + buy: 70 + sell: 67 + limit: 20000 + lowalch_price: 1 + value: 1 + net_per_cycle: -69 +- item: Iron ore + id: 440 + buy: 70 + sell: 70 + limit: 13000 + lowalch_price: 6 + value: 17 + net_per_cycle: -64 +- item: Willow shortbow + id: 849 + buy: 70 + sell: 100 + limit: 18000 + lowalch_price: 80 + value: 200 + net_per_cycle: 10 +- item: Strength potion(3) + id: 115 + buy: 72 + sell: 72 + limit: 2000 + lowalch_price: 5 + value: 13 + net_per_cycle: -67 +- item: Silver ore + id: 442 + buy: 73 + sell: 79 + limit: 13000 + lowalch_price: 30 + value: 75 + net_per_cycle: -43 +- item: Team-16 cape + id: 4345 + buy: 75 + sell: 46 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -55 +- item: Team-44 cape + id: 4401 + buy: 75 + sell: 134 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -55 +- item: Yellow cape + id: 1023 + buy: 75 + sell: 93 + limit: 150 + lowalch_price: 12 + value: 32 + net_per_cycle: -63 +- item: Pot of flour + id: 1933 + buy: 80 + sell: 73 + limit: 13000 + lowalch_price: 4 + value: 10 + net_per_cycle: -76 +- item: Team-46 cape + id: 4405 + buy: 81 + sell: 78 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -61 +- item: Energy potion(2) + id: 3012 + buy: 84 + sell: 86 + limit: 2000 + lowalch_price: 28 + value: 72 + net_per_cycle: -56 +- item: Gold bar + id: 2357 + buy: 86 + sell: 89 + limit: 10000 + lowalch_price: 120 + value: 300 + net_per_cycle: 34 +- item: Unfired pie dish + id: 1789 + buy: 87 + sell: 101 + limit: 10000 + lowalch_price: 1 + value: 3 + net_per_cycle: -86 +- item: Pink skirt + id: 1013 + buy: 90 + sell: 86 + limit: 150 + lowalch_price: 1 + value: 2 + net_per_cycle: -89 +- item: Lobster + id: 379 + buy: 95 + sell: 97 + limit: 6000 + lowalch_price: 28 + value: 70 + net_per_cycle: -67 +- item: Woad leaf + id: 1793 + buy: 95 + sell: 95 + limit: 11000 + lowalch_price: 1 + value: 1 + net_per_cycle: -94 +- item: Black toy horsey + id: 2524 + buy: 100 + sell: 130 + limit: 150 + lowalch_price: 40 + value: 100 + net_per_cycle: -60 +- item: Cadava berries + id: 753 + buy: 100 + sell: 100 + limit: 600 + lowalch_price: 1 + value: 1 + net_per_cycle: -99 +- item: Team-2 cape + id: 4317 + buy: 100 + sell: 9999 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -80 +- item: Team-45 cape + id: 4403 + buy: 100 + sell: 744 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -80 +- item: Team-50 cape + id: 4413 + buy: 100 + sell: 138 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -80 +- item: Team-10 cape + id: 4333 + buy: 101 + sell: 689 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -81 +- item: Willow longbow + id: 847 + buy: 101 + sell: 110 + limit: 18000 + lowalch_price: 128 + value: 320 + net_per_cycle: 27 +- item: Pizza base + id: 2283 + buy: 105 + sell: 144 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -104 +- item: Tiara + id: 5525 + buy: 105 + sell: 150 + limit: 10000 + lowalch_price: 40 + value: 100 + net_per_cycle: -65 +- item: Chaos rune + id: 562 + buy: 106 + sell: 107 + limit: 18000 + lowalch_price: 36 + value: 90 + net_per_cycle: -70 +- item: Strength potion(4) + id: 113 + buy: 106 + sell: 110 + limit: 2000 + lowalch_price: 5 + value: 14 + net_per_cycle: -101 +- item: Raw lobster + id: 377 + buy: 110 + sell: 110 + limit: 15000 + lowalch_price: 28 + value: 70 + net_per_cycle: -82 +- item: Dwarven stout + id: 1913 + buy: 111 + sell: 100 + limit: 2000 + lowalch_price: 1 + value: 2 + net_per_cycle: -110 +- item: Yew logs + id: 1515 + buy: 111 + sell: 112 + limit: 12000 + lowalch_price: 64 + value: 160 + net_per_cycle: -47 +- item: Silver bar + id: 2355 + buy: 113 + sell: 117 + limit: 10000 + lowalch_price: 60 + value: 150 + net_per_cycle: -53 +- item: Raw herring + id: 345 + buy: 116 + sell: 116 + limit: 15000 + lowalch_price: 4 + value: 10 + net_per_cycle: -112 +- item: Soft clay + id: 1761 + buy: 116 + sell: 116 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -115 +- item: Wizard's mind bomb + id: 1907 + buy: 116 + sell: 400 + limit: 2000 + lowalch_price: 1 + value: 2 + net_per_cycle: -115 +- item: Team-9 cape + id: 4331 + buy: 121 + sell: 121 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -101 +- item: Law rune + id: 563 + buy: 124 + sell: 125 + limit: 18000 + lowalch_price: 96 + value: 240 + net_per_cycle: -28 +- item: Bread dough + id: 2307 + buy: 125 + sell: 135 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -124 +- item: Cosmic rune + id: 564 + buy: 126 + sell: 128 + limit: 18000 + lowalch_price: 20 + value: 50 + net_per_cycle: -106 +- item: Energy potion(3) + id: 3010 + buy: 126 + sell: 126 + limit: 2000 + lowalch_price: 44 + value: 110 + net_per_cycle: -82 +- item: Stew + id: 2003 + buy: 127 + sell: 129 + limit: 10000 + lowalch_price: 8 + value: 20 + net_per_cycle: -119 +- item: Raw bear meat + id: 2136 + buy: 130 + sell: 170 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -129 +- item: Vile ashes + id: 25769 + buy: 130 + sell: 131 + limit: 3000 + lowalch_price: 1 + value: 1 + net_per_cycle: -129 +- item: Pastry dough + id: 1953 + buy: 132 + sell: 137 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -131 +- item: Cowhide + id: 1739 + buy: 137 + sell: 137 + limit: 13000 + lowalch_price: 1 + value: 2 + net_per_cycle: -136 +- item: Gold amulet + id: 1692 + buy: 141 + sell: 144 + limit: 18000 + lowalch_price: 140 + value: 350 + net_per_cycle: -1 +- item: Plank + id: 960 + buy: 141 + sell: 146 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -140 +- item: Unfired bowl + id: 1791 + buy: 141 + sell: 141 + limit: 10000 + lowalch_price: 1 + value: 2 + net_per_cycle: -140 +- item: Nature rune + id: 561 + buy: 145 + sell: 145 + limit: 18000 + lowalch_price: 72 + value: 180 + net_per_cycle: -73 +- item: Gold necklace + id: 1654 + buy: 148 + sell: 156 + limit: 18000 + lowalch_price: 180 + value: 450 + net_per_cycle: 32 +- item: Antipoison(1) + id: 179 + buy: 150 + sell: 173 + limit: 2000 + lowalch_price: 57 + value: 144 + net_per_cycle: -93 +- item: Coal + id: 453 + buy: 150 + sell: 153 + limit: 13000 + lowalch_price: 18 + value: 45 + net_per_cycle: -132 +- item: Dust + id: 30328 + buy: 150 + sell: 2499 + limit: 250 + lowalch_price: 1 + value: 2 + net_per_cycle: -149 +- item: Yellow dye + id: 1765 + buy: 151 + sell: 332 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -149 +- item: Gold amulet (u) + id: 1673 + buy: 152 + sell: 160 + limit: 18000 + lowalch_price: 140 + value: 350 + net_per_cycle: -12 +- item: Iron bar + id: 2351 + buy: 153 + sell: 157 + limit: 10000 + lowalch_price: 11 + value: 28 + net_per_cycle: -142 +- item: Air talisman + id: 1438 + buy: 154 + sell: 165 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -153 +- item: Chocolate cake + id: 1897 + buy: 155 + sell: 160 + limit: 6000 + lowalch_price: 28 + value: 70 + net_per_cycle: -127 +- item: Gold ore + id: 444 + buy: 155 + sell: 155 + limit: 30000 + lowalch_price: 60 + value: 150 + net_per_cycle: -95 +- item: Cake + id: 1891 + buy: 158 + sell: 161 + limit: 6000 + lowalch_price: 20 + value: 50 + net_per_cycle: -138 +- item: Gold ring + id: 1635 + buy: 160 + sell: 162 + limit: 18000 + lowalch_price: 140 + value: 350 + net_per_cycle: -20 +- item: Mithril ore + id: 447 + buy: 161 + sell: 158 + limit: 13000 + lowalch_price: 64 + value: 162 + net_per_cycle: -97 +- item: Charcoal + id: 973 + buy: 162 + sell: 169 + limit: 13000 + lowalch_price: 18 + value: 45 + net_per_cycle: -144 +- item: Hard leather + id: 1743 + buy: 170 + sell: 182 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -169 +- item: Leather + id: 1741 + buy: 171 + sell: 193 + limit: 13000 + lowalch_price: 1 + value: 1 + net_per_cycle: -170 +- item: Asgarnian ale + id: 1905 + buy: 173 + sell: 245 + limit: 2000 + lowalch_price: 1 + value: 2 + net_per_cycle: -172 +- item: Red spiders' eggs + id: 223 + buy: 175 + sell: 180 + limit: 13000 + lowalch_price: 2 + value: 7 + net_per_cycle: -173 +- item: Maple shortbow + id: 853 + buy: 176 + sell: 160 + limit: 18000 + lowalch_price: 160 + value: 400 + net_per_cycle: -16 +- item: Death rune + id: 560 + buy: 190 + sell: 195 + limit: 25000 + lowalch_price: 72 + value: 180 + net_per_cycle: -118 +- item: Energy potion(4) + id: 3008 + buy: 191 + sell: 194 + limit: 2000 + lowalch_price: 58 + value: 146 + net_per_cycle: -133 +- item: Team-40 cape + id: 4393 + buy: 191 + sell: 48 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -171 +- item: Swordfish + id: 373 + buy: 199 + sell: 202 + limit: 6000 + lowalch_price: 32 + value: 80 + net_per_cycle: -167 +- item: Team-41 cape + id: 4395 + buy: 200 + sell: 741 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -180 +- item: Raw swordfish + id: 371 + buy: 206 + sell: 215 + limit: 15000 + lowalch_price: 32 + value: 80 + net_per_cycle: -174 +- item: Green dye + id: 1771 + buy: 209 + sell: 300 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -207 +- item: Monkey bones + id: 3183 + buy: 217 + sell: 331 + limit: 3000 + lowalch_price: 1 + value: 1 + net_per_cycle: -216 +- item: Raw t-bone steak + id: 33106 + buy: 217 + sell: 229 + limit: 13000 + lowalch_price: 4 + value: 10 + net_per_cycle: -213 +- item: Limpwurt root + id: 225 + buy: 220 + sell: 223 + limit: 13000 + lowalch_price: 2 + value: 7 + net_per_cycle: -218 +- item: Brown toy horsey + id: 2520 + buy: 222 + sell: 528 + limit: 150 + lowalch_price: 40 + value: 100 + net_per_cycle: -182 +- item: Team-29 cape + id: 4371 + buy: 224 + sell: 99 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -204 +- item: Big bones + id: 532 + buy: 232 + sell: 244 + limit: 3000 + lowalch_price: 1 + value: 1 + net_per_cycle: -231 +- item: Uncut sapphire + id: 1623 + buy: 234 + sell: 234 + limit: 10000 + lowalch_price: 10 + value: 25 + net_per_cycle: -224 +- item: Sapphire + id: 1607 + buy: 239 + sell: 257 + limit: 13000 + lowalch_price: 100 + value: 250 + net_per_cycle: -139 +- item: Orange cape + id: 1031 + buy: 251 + sell: 528 + limit: 150 + lowalch_price: 12 + value: 32 + net_per_cycle: -239 +- item: Grey gloves + id: 2902 + buy: 254 + sell: 1365 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -54 +- item: Orange dye + id: 1769 + buy: 260 + sell: 261 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -258 +- item: Pie shell + id: 2315 + buy: 260 + sell: 260 + limit: 13000 + lowalch_price: 1 + value: 4 + net_per_cycle: -259 +- item: Uncooked apple pie + id: 2317 + buy: 264 + sell: 350 + limit: 13000 + lowalch_price: 6 + value: 16 + net_per_cycle: -258 +- item: Water talisman + id: 1444 + buy: 264 + sell: 270 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -263 +- item: Maple longbow + id: 851 + buy: 266 + sell: 271 + limit: 18000 + lowalch_price: 256 + value: 640 + net_per_cycle: -10 +- item: Antipoison(2) + id: 177 + buy: 268 + sell: 500 + limit: 2000 + lowalch_price: 86 + value: 216 + net_per_cycle: -182 +- item: Fur + id: 6814 + buy: 278 + sell: 340 + limit: 18000 + lowalch_price: 4 + value: 10 + net_per_cycle: -274 +- item: Meat pizza + id: 2293 + buy: 278 + sell: 293 + limit: 10000 + lowalch_price: 20 + value: 50 + net_per_cycle: -258 +- item: Team-30 cape + id: 4373 + buy: 295 + sell: 350 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -275 +- item: Team-25 cape + id: 4363 + buy: 300 + sell: 600 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -280 +- item: Brass necklace + id: 1009 + buy: 302 + sell: 302 + limit: 150 + lowalch_price: 12 + value: 30 + net_per_cycle: -290 +- item: Team-49 cape + id: 4411 + buy: 309 + sell: 1 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -289 +- item: Team-26 cape + id: 4365 + buy: 310 + sell: 54 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -290 +- item: Antipoison(3) + id: 175 + buy: 315 + sell: 461 + limit: 2000 + lowalch_price: 115 + value: 288 + net_per_cycle: -200 +- item: Grey toy horsey + id: 2526 + buy: 318 + sell: 140 + limit: 150 + lowalch_price: 40 + value: 100 + net_per_cycle: -278 +- item: White toy horsey + id: 2522 + buy: 325 + sell: 3400 + limit: 150 + lowalch_price: 40 + value: 100 + net_per_cycle: -285 +- item: Uncooked meat pie + id: 2319 + buy: 334 + sell: 349 + limit: 13000 + lowalch_price: 3 + value: 8 + net_per_cycle: -331 +- item: Chef's hat + id: 1949 + buy: 353 + sell: 339 + limit: 150 + lowalch_price: 1 + value: 2 + net_per_cycle: -352 +- item: Triangle sandwich + id: 6962 + buy: 355 + sell: 351 + limit: 6000 + lowalch_price: 10 + value: 25 + net_per_cycle: -345 +- item: Red dye + id: 1763 + buy: 367 + sell: 500 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -365 +- item: Sapphire amulet (u) + id: 1675 + buy: 375 + sell: 403 + limit: 10000 + lowalch_price: 360 + value: 900 + net_per_cycle: -15 +- item: Uncooked berry pie + id: 2321 + buy: 386 + sell: 420 + limit: 13000 + lowalch_price: 2 + value: 6 + net_per_cycle: -384 +- item: Plain pizza + id: 2289 + buy: 393 + sell: 439 + limit: 10000 + lowalch_price: 16 + value: 40 + net_per_cycle: -377 +- item: Team-33 cape + id: 4379 + buy: 397 + sell: 397 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -377 +- item: Team-19 cape + id: 4351 + buy: 402 + sell: 446 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -382 +- item: Emerald + id: 1605 + buy: 407 + sell: 420 + limit: 13000 + lowalch_price: 200 + value: 500 + net_per_cycle: -207 +- item: Sapphire ring + id: 1637 + buy: 409 + sell: 423 + limit: 10000 + lowalch_price: 360 + value: 900 + net_per_cycle: -49 +- item: Black cape + id: 1019 + buy: 413 + sell: 435 + limit: 150 + lowalch_price: 2 + value: 7 + net_per_cycle: -411 +- item: Burnt bones + id: 528 + buy: 418 + sell: 574 + limit: 3000 + lowalch_price: 1 + value: 1 + net_per_cycle: -417 +- item: Antipoison(4) + id: 2446 + buy: 440 + sell: 482 + limit: 2000 + lowalch_price: 144 + value: 360 + net_per_cycle: -296 +- item: Sapphire necklace + id: 1656 + buy: 442 + sell: 449 + limit: 18000 + lowalch_price: 420 + value: 1050 + net_per_cycle: -22 +- item: Team-8 cape + id: 4329 + buy: 448 + sell: 423 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -428 +- item: Team-42 cape + id: 4397 + buy: 463 + sell: 398 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -443 +- item: Uncut emerald + id: 1621 + buy: 470 + sell: 465 + limit: 10000 + lowalch_price: 20 + value: 50 + net_per_cycle: -450 +- item: Sapphire amulet + id: 1694 + buy: 477 + sell: 519 + limit: 10000 + lowalch_price: 360 + value: 900 + net_per_cycle: -117 +- item: Purple dye + id: 1773 + buy: 479 + sell: 693 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -477 +- item: Blue dye + id: 1767 + buy: 500 + sell: 596 + limit: 150 + lowalch_price: 2 + value: 5 + net_per_cycle: -498 +- item: Teal gloves + id: 2932 + buy: 500 + sell: 508 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -300 +- item: Purple cape + id: 1029 + buy: 530 + sell: 576 + limit: 150 + lowalch_price: 12 + value: 32 + net_per_cycle: -518 +- item: Anchovy pizza + id: 2297 + buy: 541 + sell: 553 + limit: 10000 + lowalch_price: 24 + value: 60 + net_per_cycle: -517 +- item: Team-22 cape + id: 4357 + buy: 568 + sell: 99000 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -548 +- item: Steel bar + id: 2353 + buy: 570 + sell: 570 + limit: 10000 + lowalch_price: 40 + value: 100 + net_per_cycle: -530 +- item: Adamantite ore + id: 449 + buy: 573 + sell: 585 + limit: 4500 + lowalch_price: 160 + value: 400 + net_per_cycle: -413 +- item: Team-32 cape + id: 4377 + buy: 580 + sell: 580 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -560 +- item: Emerald amulet (u) + id: 1677 + buy: 591 + sell: 602 + limit: 10000 + lowalch_price: 510 + value: 1275 + net_per_cycle: -81 +- item: Team-38 cape + id: 4389 + buy: 603 + sell: 4000 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -583 +- item: Team-12 cape + id: 4337 + buy: 607 + sell: 2500 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -587 +- item: Earth talisman + id: 1440 + buy: 608 + sell: 635 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -607 +- item: Bone fragments + id: 25139 + buy: 627 + sell: 629 + limit: 500 + lowalch_price: 1 + value: 1 + net_per_cycle: -626 +- item: Emerald ring + id: 1639 + buy: 640 + sell: 649 + limit: 10000 + lowalch_price: 510 + value: 1275 + net_per_cycle: -130 +- item: Team-17 cape + id: 4347 + buy: 644 + sell: 1114 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -624 +- item: Emerald necklace + id: 1658 + buy: 655 + sell: 675 + limit: 18000 + lowalch_price: 570 + value: 1425 + net_per_cycle: -85 +- item: Team-31 cape + id: 4375 + buy: 671 + sell: 50 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -651 +- item: Green cape + id: 1027 + buy: 684 + sell: 652 + limit: 150 + lowalch_price: 12 + value: 32 + net_per_cycle: -672 +- item: Emerald amulet + id: 1696 + buy: 690 + sell: 719 + limit: 10000 + lowalch_price: 510 + value: 1275 + net_per_cycle: -180 +- item: Team-18 cape + id: 4349 + buy: 698 + sell: 542 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -678 +- item: Team-27 cape + id: 4367 + buy: 738 + sell: 545 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -718 +- item: Castle wars bracelet(3) + id: 11079 + buy: 750 + sell: 779 + limit: 10000 + lowalch_price: 670 + value: 1675 + net_per_cycle: -80 +- item: Purple gloves + id: 2942 + buy: 750 + sell: 900 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -550 +- item: Team-1 cape + id: 4315 + buy: 755 + sell: 792 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -735 +- item: Ruby + id: 1603 + buy: 819 + sell: 840 + limit: 13000 + lowalch_price: 400 + value: 1000 + net_per_cycle: -419 +- item: Yellow gloves + id: 2922 + buy: 869 + sell: 748 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -669 +- item: Team-11 cape + id: 4335 + buy: 892 + sell: 1445 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -872 +- item: Yellow bead + id: 1472 + buy: 900 + sell: 1050 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -899 +- item: Mithril bar + id: 2359 + buy: 956 + sell: 973 + limit: 10000 + lowalch_price: 120 + value: 300 + net_per_cycle: -836 +- item: Purple boots + id: 2934 + buy: 966 + sell: 1333 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -766 +- item: Teal boots + id: 2924 + buy: 980 + sell: 1289 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -780 +- item: Ring of forging + id: 2568 + buy: 1000 + sell: 1032 + limit: 10000 + lowalch_price: 810 + value: 2025 + net_per_cycle: -190 +- item: Wine of Zamorak + id: 245 + buy: 1009 + sell: 1037 + limit: 11000 + lowalch_price: 1 + value: 1 + net_per_cycle: -1008 +- item: Team-5 cape + id: 4323 + buy: 1032 + sell: 44000 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -1012 +- item: Red gloves + id: 2912 + buy: 1039 + sell: 1039 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -839 +- item: Ruby ring + id: 1641 + buy: 1040 + sell: 1052 + limit: 10000 + lowalch_price: 810 + value: 2025 + net_per_cycle: -230 +- item: Red boots + id: 2904 + buy: 1063 + sell: 1321 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -863 +- item: Uncut ruby + id: 1619 + buy: 1074 + sell: 1083 + limit: 10000 + lowalch_price: 40 + value: 100 + net_per_cycle: -1034 +- item: Ruby amulet (u) + id: 1679 + buy: 1098 + sell: 1126 + limit: 10000 + lowalch_price: 810 + value: 2025 + net_per_cycle: -288 +- item: Team-48 cape + id: 4409 + buy: 1100 + sell: 1500 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -1080 +- item: Team-21 cape + id: 4355 + buy: 1108 + sell: 2300 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -1088 +- item: Black bead + id: 1474 + buy: 1111 + sell: 1150 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -1110 +- item: Ruby amulet + id: 1698 + buy: 1111 + sell: 1140 + limit: 10000 + lowalch_price: 810 + value: 2025 + net_per_cycle: -301 +- item: Ruby necklace + id: 1660 + buy: 1127 + sell: 1127 + limit: 18000 + lowalch_price: 870 + value: 2175 + net_per_cycle: -257 +- item: Grey boots + id: 2894 + buy: 1129 + sell: 1865 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -929 +- item: White bead + id: 1476 + buy: 1180 + sell: 1180 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -1179 +- item: Mind talisman + id: 1448 + buy: 1189 + sell: 1348 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -1188 +- item: Team-35 cape + id: 4383 + buy: 1210 + sell: 2108 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -1190 +- item: Team-4 cape + id: 4321 + buy: 1359 + sell: 951 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -1339 +- item: Body talisman + id: 1446 + buy: 1654 + sell: 1660 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -1653 +- item: Diamond + id: 1601 + buy: 1683 + sell: 1699 + limit: 11000 + lowalch_price: 800 + value: 2000 + net_per_cycle: -883 +- item: Adamantite bar + id: 2361 + buy: 1835 + sell: 1864 + limit: 10000 + lowalch_price: 256 + value: 640 + net_per_cycle: -1579 +- item: Red bead + id: 1470 + buy: 1905 + sell: 1905 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -1904 +- item: Diamond amulet (u) + id: 1681 + buy: 1913 + sell: 1915 + limit: 10000 + lowalch_price: 1410 + value: 3525 + net_per_cycle: -503 +- item: Diamond ring + id: 1643 + buy: 1938 + sell: 1957 + limit: 10000 + lowalch_price: 1410 + value: 3525 + net_per_cycle: -528 +- item: Diamond necklace + id: 1662 + buy: 1990 + sell: 1996 + limit: 18000 + lowalch_price: 1470 + value: 3675 + net_per_cycle: -520 +- item: Team-37 cape + id: 4387 + buy: 2000 + sell: 900 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -1980 +- item: Diamond amulet + id: 1700 + buy: 2090 + sell: 2124 + limit: 10000 + lowalch_price: 1410 + value: 3525 + net_per_cycle: -680 +- item: Fire talisman + id: 1442 + buy: 2311 + sell: 2330 + limit: 11000 + lowalch_price: 1 + value: 4 + net_per_cycle: -2310 +- item: Uncut diamond + id: 1617 + buy: 2450 + sell: 2483 + limit: 10000 + lowalch_price: 80 + value: 200 + net_per_cycle: -2370 +- item: Yellow boots + id: 2914 + buy: 2471 + sell: 4800 + limit: 150 + lowalch_price: 200 + value: 500 + net_per_cycle: -2271 +- item: Team-14 cape + id: 4341 + buy: 3074 + sell: 546 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -3054 +- item: Muddy key + id: 991 + buy: 6234 + sell: 6616 + limit: 11000 + lowalch_price: 32 + value: 80 + net_per_cycle: -6202 +- item: Runite ore + id: 451 + buy: 10265 + sell: 10265 + limit: 4500 + lowalch_price: 1280 + value: 3200 + net_per_cycle: -8985 +- item: Runite bar + id: 2363 + buy: 12230 + sell: 12236 + limit: 10000 + lowalch_price: 2000 + value: 5000 + net_per_cycle: -10230 +- item: Team-24 cape + id: 4361 + buy: 60000 + sell: 632 + limit: 150 + lowalch_price: 20 + value: 50 + net_per_cycle: -59980 From 864f2c9e1c32daea6a5cd5df79f6881279b2f083 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 05:27:09 -0500 Subject: [PATCH 04/15] fix(flip-candidates): floor GE tax to whole coins, pin full sell-cycle, qualify F2P path Governor-review bounce (PR #7) fixes on issue-1: 1. GE 2% convenience fee rounds DOWN per item: net = sell - floor(0.02*sell) - buy. Anchor net 16.12 -> 17 (244-4-223); top-8 members table recomputed with floored tax -> 598,000/4h = 149,500 gp/h (was 138,000); gp_per_hour_low/high updated. Validator now rejects fractional-tax arithmetic for GE-margin entries. 2. State machine split wait_fill into wait_buy_fill / wait_sell_fill so the full buy -> buy-fill -> sell -> sell-fill -> collect path is explicit; sell cycle can no longer loop back into sell without collecting. 3. Economy qualified as the members path (8 slots); F2P path added (3 slots, F2P-tradeable rows only): Iron bar + Cosmic rune + Silver ore = 227,000/4h = 56,750 gp/h. Members/F2P flags cross-checked against wiki item pages. --- codex/economy/flip-candidates.yaml | 77 ++++++++++++++++++++---------- tools/validate.py | 24 ++++++++++ 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/codex/economy/flip-candidates.yaml b/codex/economy/flip-candidates.yaml index c32b0d1..22ab173 100644 --- a/codex/economy/flip-candidates.yaml +++ b/codex/economy/flip-candidates.yaml @@ -3,21 +3,25 @@ title: GE Flipping (margin trading) domain: economy tags: [flipping, merchanting, ge, money-making, hybrid] status: verified -version: 1 +version: 2 updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > Buy-and-sell Grand Exchange margin flipping: place resting buy offers at the low edge of an item's price band and resting sell offers at the high edge, harvesting the spread on high-volume items. There are no skill or - quest requirements — capital breadth and slot discipline are the whole game. + quest requirements — capital breadth and slot discipline are the whole + game. Members accounts run 8 GE slots over the full candidate set; + F2P accounts get 3 slots and F2P-tradeable rows only (separate F2P path + in profit_drivers). accounts: - membership: hybrid # flipping works on F2P and members accounts alike + membership: hybrid # members: 8 slots/full set; F2P: 3 slots/F2P rows (see economy) min_level: 3 # post-Tutorial Island baseline; no skill requirement requires_quest: [] requires_other: - "Post-Tutorial Island account with Grand Exchange access (no skill or quest requirement)" + - "Members account: 8 GE slots, full candidate set (the modeled path below). F2P account: 3 GE slots and F2P-tradeable rows only — run the F2P path in profit_drivers." best_practice: equipment: @@ -48,22 +52,35 @@ best_practice: some items share a connected limit with related items — treat those as one bucket when filling slots." source: "OSRS wiki — Grand Exchange (Buy limits; Grand Exchange/Buying limits)" + - name: run the path that matches the account's slot set + why: "Members get eight GE slots and may trade any row; F2P accounts get three slots + and may only purchase free-to-play rows. The modeled economics below assume the + members set — a hybrid account must swap to the 3-slot F2P path (Iron bar / + Cosmic rune / Silver ore) instead of chasing members-only rows it cannot buy." + source: "OSRS wiki — Grand Exchange (Members get eight Grand Exchange slots ... free players are limited to three. Additionally, free players may only purchase free-to-play items, and not members' items)" economy: # Model (documented, reproducible from the snapshot): - # per slot: buy `limit` units at the low edge, sell at the high edge. - # net_pu = 0.98 * sell - buy (2% convenience fee on the sale; the 5M/item cap and - # the sub-50gp rounding exemption are not hit at these prices) - # 4h/slot = net_pu * limit (buy-limit window caps per-4h volume) - # Snapshot 2026-08-27: prices.runescape.wiki mapping + latest; limits cross-checked - # against Module:Exchange/ (all agree). Top 8 by 4h/slot below in profit_drivers. + # Members path (8 GE slots — the modeled economics below): + # per slot: buy `limit` units at the low edge, sell at the high edge. + # tax_pu = floor(0.02 * sell) # 2% convenience fee, rounded DOWN to + # # whole coins per item; the 5M/item cap + # # and the sub-50gp rounding exemption + # # are not hit at these prices + # net_pu = sell - tax_pu - buy # integer per-item net (floored tax) + # 4h/slot = net_pu * limit # buy-limit window caps per-4h volume + # F2P path (3 GE slots, F2P-tradeable rows only): same arithmetic, top-3 + # F2P rows — see profit_drivers for the table and rate. + # Snapshots 2026-08-27: prices.runescape.wiki mapping + latest; limits and + # members flags cross-checked against the wiki item pages (all agree). Top 8 + # by 4h/slot below in profit_drivers. item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members) item_id: 44 cost_per_cycle: 223 # buy @ low edge per unit (snapshot 2026-08-27) income_per_cycle: 244 # sell @ high edge per unit, gross (snapshot 2026-08-27) - net_per_cycle: 16.12 # 0.98 * 244 - 223 (2% fee applied to the sale) - gp_per_hour_low: 69000 # 50% of the top-8 modeled upper bound — partial fills/drift - gp_per_hour_high: 138000 # top-8 modeled upper bound: sum(4h/slot) / 4 (8 slots, full fills) + net_per_cycle: 17 # 244 - floor(244*0.02) - 223 = 244 - 4 - 223 (floored tax) + gp_per_hour_low: 74750 # 50% of the top-8 modeled upper bound — partial fills/drift + gp_per_hour_high: 149500 # top-8 modeled upper bound: sum(4h/slot) / 4 (8 slots, full fills) xp_per_hour: 0 # flipping awards no XP supplies: - item: Rune arrowtips # canonical wiki names per action-grammar conventions @@ -80,9 +97,10 @@ economy: ge_limit: 18000 profit_drivers: - "Spread per unit x trade volume; volume is the safety factor (high-volume items fill faster and drift less)" - - "Slot breadth: cover as many net-positive candidates as capital allows (standard 8 slots)" - - "Snapshot 2026-08-27 top 8 by modeled 4h/slot: Rune arrowtips 161,200 (buy 223 / sell 244 / limit 10000); Iron bar 88,600 (145/157/10000); Cosmic rune 61,920 (122/128/18000); Magic logs 61,680 (772/793/12000); Silver ore 57,460 (73/79/13000); Mud rune 53,640 (96/101/18000); Raw swordfish 41,100 (206/213/15000); Raw lobster 27,000 (106/110/15000)" - - "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells" + - "Slot breadth: cover as many net-positive candidates as capital allows (members 8 slots / F2P 3 slots)" + - "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h on 8 slots" + - "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h (alternates: Raw swordfish 45,000, Raw lobster 30,000)" + - "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges for members; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells" state_machine: initial: idle @@ -96,13 +114,16 @@ state_machine: next: [buy, idle] buy: goal: place one resting buy offer per slot at or below the low edge - next: [wait_fill, collect, idle] - wait_fill: - goal: hold offers; re-quote any approaching the 4h window lapse + next: [wait_buy_fill, collect, idle] + wait_buy_fill: + goal: hold BUY offers; re-quote any approaching the 4h window lapse next: [sell, buy, collect] sell: goal: place one sell offer per filled slot at or above the high edge - next: [wait_fill, collect] + next: [wait_sell_fill, collect] + wait_sell_fill: + goal: hold SELL offers; re-quote any approaching the 4h window lapse + next: [collect, sell, idle] collect: goal: collect proceeds, deposit coins, log the cycle next: [scan, complete, idle] @@ -119,16 +140,22 @@ state_machine: to: idle - from: buy when: "all slots quoted" - to: wait_fill - - from: wait_fill + to: wait_buy_fill + - from: wait_buy_fill when: "all pending buys filled" to: sell - - from: wait_fill - when: "4h window lapse with unfilled offers" + - from: wait_buy_fill + when: "4h window lapse with unfilled BUY offers" to: collect - from: sell when: "sells placed" - to: wait_fill + to: wait_sell_fill + - from: wait_sell_fill + when: "all pending sells filled" + to: collect + - from: wait_sell_fill + when: "4h window lapse with unfilled SELL offers" + to: sell - from: collect when: "session GP target reached" to: complete @@ -216,7 +243,7 @@ actions: delay: [600, 1400] run_control: - iteration: "one full buy -> fill -> sell -> collect rotation across all slots" + iteration: "one full buy -> buy-fill -> sell -> sell-fill -> collect rotation across all slots" max_iterations: null break: every: [6, 10] # rotations diff --git a/tools/validate.py b/tools/validate.py index 7667c3d..5dde203 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -11,6 +11,10 @@ Runs: => WARNING (entry should be re-verified, not dropped) - `best_practice` lines whose source starts with "researched" => WARNING (citation TODO — file a follow-up issue) + - GE-margin entries (tags include flipping/merchanting): the 2% GE + convenience fee is rounded DOWN to whole coins per item, so + net_per_cycle MUST equal income - floor(0.02*income) - cost. + Fractional-tax arithmetic (0.98*sell - buy) is an error. 3. Global id uniqueness across the whole codex tree. Exit codes: 0 = clean (warnings ok), 1 = validation errors. @@ -19,6 +23,7 @@ Usage: python3 tools/validate.py [--stale-days 30] """ import datetime import json +import math import re import sys from pathlib import Path @@ -107,6 +112,25 @@ def main(): if len(a_steps) != len(set(a_steps)): errors.append(f"{rel}: duplicate action step names") + # GE-margin floor-tax arithmetic. The wiki: "The 2% tax rounds down + # to the nearest whole number, so items sold for below 50 coins have + # a tax of less than one coin" — per-item net MUST use the floored + # tax, never 0.98*sell - buy. Scoped to GE-margin tags so alchemy + # (income is alch value, not a GE sale) is unaffected. + tags = {str(t).lower() for t in data.get("tags", [])} + eco = data.get("economy") or {} + if tags & {"flipping", "merchanting", "ge"} and all( + k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle") + ): + sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"] + tax = math.floor(sell * 0.02) + expected = sell - tax - buy + if eco["net_per_cycle"] != expected: + errors.append( + f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model " + f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); " + f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid") + # 3. staleness / research TODOs if data.get("status") == "verified": try: From f6675ba543f0a91d4323fddda1838f1c3a71de0c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 12:34:08 -0500 Subject: [PATCH 05/15] fix(validate): scope GE floor-tax gate to margin tags, add regression selftest Governor-review bounce (PR #7, attempt 2) fix on issue-1: - The floor-tax rule previously triggered on any entry carrying the generic 'ge' tag (tags & {flipping, merchanting, ge}). A training, processing, or procurement entry may legitimately carry 'ge' while its income/cost model is unrelated to flip tax, so it could fail the global gate unless its economics accidentally satisfied the flip equation. - Rule now gates ONLY the unambiguous margin tags (flipping and/or merchanting); the bare 'ge' tag no longer triggers it. - Rule extracted into ge_floor_tax_error() and a --selftest mode added with three regression fixtures, run on every gate invocation: * fractional-tax flipping entry -> must FAIL * non-margin entry with only 'ge' + 'processing' (economics deliberately violate the flip equation) -> must PASS * correctly-floored flipping entry -> must PASS - Demo (old scope): ge+processing fixture with net 2000 vs floored 1900 errored under the old tag set; low-alch-f2p's -112 vs -115 model would also false-positive if it carried 'ge'. Tests: python3 tools/validate.py -> 2 entries, 0 errors, 1 pre-existing warning (researched flag on low-alch-f2p); --selftest -> 3 PASS, 0 errors; git diff --check clean. --- tools/validate.py | 113 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 23 deletions(-) diff --git a/tools/validate.py b/tools/validate.py index 5dde203..5947f6a 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -11,15 +11,23 @@ Runs: => WARNING (entry should be re-verified, not dropped) - `best_practice` lines whose source starts with "researched" => WARNING (citation TODO — file a follow-up issue) - - GE-margin entries (tags include flipping/merchanting): the 2% GE - convenience fee is rounded DOWN to whole coins per item, so - net_per_cycle MUST equal income - floor(0.02*income) - cost. - Fractional-tax arithmetic (0.98*sell - buy) is an error. + - GE-margin entries (tags include flipping and/or merchanting — + the unambiguous margin signals): the 2% GE convenience fee is + rounded DOWN to whole coins per item, so net_per_cycle MUST + equal income - floor(0.02*income) - cost. Fractional-tax + arithmetic (0.98*sell - buy) is an error. + NOTE: the generic `ge` tag alone does NOT gate this rule — a + training/processing/procurement entry may legitimately carry + `ge` while its income/cost model has nothing to do with flip tax. + - Regression fixtures (--selftest): a fractional-tax flipping + entry must FAIL; a non-margin entry carrying only `ge` plus a + non-flip tag must PASS. 3. Global id uniqueness across the whole codex tree. Exit codes: 0 = clean (warnings ok), 1 = validation errors. -Usage: python3 tools/validate.py [--stale-days 30] +Usage: python3 tools/validate.py [--stale-days 30] [--selftest] + --selftest: run the GE floor-tax regression fixtures only (no tree scan) """ import datetime import json @@ -46,12 +54,82 @@ def load_entries(): return sorted(out) +MARGIN_TAGS = {"flipping", "merchanting"} + + +def ge_floor_tax_error(rel, data): + """GE floor-tax gate for a single entry. + + Applies ONLY to the unambiguous margin tags (flipping/merchanting). + The generic `ge` tag is not proof that income_per_cycle is a taxed + GE resale: a training/processing/procurement entry may carry `ge` + while modeling unrelated income/cost, so it must not be gated by + this rule. Returns an error message, or None when the entry is not + gated or its floored-tax arithmetic is correct. + + Wiki: "The 2% tax rounds down to the nearest whole number, so items + sold for below 50 coins have a tax of less than one coin" — per-item + net MUST use the floored tax, never 0.98*sell - buy. + """ + tags = {str(t).lower() for t in data.get("tags", [])} + if not tags & MARGIN_TAGS: + return None + eco = data.get("economy") or {} + if not all(k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle")): + return None + sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"] + tax = math.floor(sell * 0.02) + expected = sell - tax - buy + if eco["net_per_cycle"] != expected: + return ( + f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model " + f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); " + f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid") + return None + + +def run_selftest(verbose=False): + """Regression fixtures for the GE floor-tax gate; returns error strings.""" + fixtures = [ + # name, tags, economy, expect_error + ("fractional-tax flipping entry", ["flipping"], + {"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 16.12}, True), + # 5000 - floor(5000*0.02) - 3000 = 1900 != 2000: this fixture's + # economics deliberately do NOT satisfy the flip equation, so the + # old `ge`-tag scoping would false-positive on it (2000 != 1900). + ("non-margin entry with only 'ge' + 'processing'", ["ge", "processing"], + {"income_per_cycle": 5000, "cost_per_cycle": 3000, "net_per_cycle": 2000}, False), + ("correctly-floored flipping entry", ["flipping", "merchanting"], + {"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17}, False), + ] + errors = [] + for name, tags, eco, expect_error in fixtures: + rel = f"selftest/{name}" + got_error = ge_floor_tax_error(rel, {"tags": tags, "economy": eco}) is not None + passed = got_error == expect_error + if verbose: + print(f"SELFTEST {'PASS' if passed else 'FAIL'} {name}") + if not passed: + errors.append( + f"selftest: '{name}' expected " + f"{'an error' if expect_error else 'no error'} but got " + f"{'an error' if got_error else 'no error'}") + return errors + + def main(): global STALE_DAYS if "--stale-days" in sys.argv: STALE_DAYS = int(sys.argv[sys.argv.index("--stale-days") + 1]) errors, warnings = [], [] + if "--selftest" in sys.argv: + errors += run_selftest(verbose=True) + print(f"RESULT: selftest, {len(errors)} errors, 0 warnings") + return 1 if errors else 0 + + # The fix itself is gated: regression fixtures run on every invocation. + errors += run_selftest(verbose=False) entries = load_entries() if not entries: print("ERROR: no codex entries found under codex/") @@ -112,24 +190,13 @@ def main(): if len(a_steps) != len(set(a_steps)): errors.append(f"{rel}: duplicate action step names") - # GE-margin floor-tax arithmetic. The wiki: "The 2% tax rounds down - # to the nearest whole number, so items sold for below 50 coins have - # a tax of less than one coin" — per-item net MUST use the floored - # tax, never 0.98*sell - buy. Scoped to GE-margin tags so alchemy - # (income is alch value, not a GE sale) is unaffected. - tags = {str(t).lower() for t in data.get("tags", [])} - eco = data.get("economy") or {} - if tags & {"flipping", "merchanting", "ge"} and all( - k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle") - ): - sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"] - tax = math.floor(sell * 0.02) - expected = sell - tax - buy - if eco["net_per_cycle"] != expected: - errors.append( - f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model " - f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); " - f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid") + # GE-margin floor-tax arithmetic (see ge_floor_tax_error): scoped to + # the unambiguous margin tags flipping/merchanting. The generic `ge` + # tag alone must not gate this rule — alchemy/processing/procurement + # entries can carry `ge` with income unrelated to a taxed GE resale. + err = ge_floor_tax_error(rel, data) + if err: + errors.append(err) # 3. staleness / research TODOs if data.get("status") == "verified": From 31a23a02149b2a6e61598002bd746e833280ee48 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 12:42:02 -0500 Subject: [PATCH 06/15] codex: fix low-alch-members entry per governor review (bounce fix attempt 1) - Pacing: 3-tick cast is 1.8s (0.6s/tick), not 5.4s; xp_per_hour and gp_per_hour now derive from the mandatory anti_ban.delay [1600, 3400]ms pause (2.5s midpoint -> 1,440 casts/hr -> 44,640 XP/hr), arithmetic pinned. - Anchor: model the actual least-loss liquid candidate, rune javelin tips (id 19580, -3/cast live, limit 10,000, lowalch==highalch*2//3 asserted); top-10 margin list re-fetched live; sign errors in old caveat fixed (opal amulet (u) stale-low artifact disqualified, jade ring now marginal-profit and flagged as flip candidate, not the anchor). - Bryophyta's staff + Tome: document the 30 Attack equip gate; accounts below it keep the ordinary Staff of Fire path (default, no Attack req). - version 1 -> 2. Validator: 0 errors (1 pre-existing F2P WARN). --- codex/economy/low-alch-members.yaml | 84 +++++++++++++++++------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml index 473d2a4..3a0b8dd 100644 --- a/codex/economy/low-alch-members.yaml +++ b/codex/economy/low-alch-members.yaml @@ -3,7 +3,7 @@ title: Low-Level Alchemy (Members) domain: economy tags: [alching, magic-training, members, loss-training] status: verified -version: 1 +version: 2 # 2: bounce-fix 2026-08-27 — pacing-pinned economics, least-loss anchor, 30 Attack gate updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > @@ -12,7 +12,8 @@ summary: > machine and actions as low-alch-f2p; the catalog is the members item pool (mapping lowalch != null, members=true, live GE prices 2026-08-27). At current prices the method burns a small amount of GP per cast for 31 XP — - run it for XP, not GP. Handoff: switch to High Level Alchemy at level 55 + run it for XP, not GP. Least-loss liquid anchor at last check: rune + javelin tips (-3/cast). Handoff: switch to High Level Alchemy at level 55 (hard level gate + strictly more coins AND XP per cast; see sources). accounts: @@ -20,8 +21,9 @@ accounts: min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified) requires_quest: [] requires_other: - - "Staff of Fire equipped (removes 3 fire runes from the cost)" + - "Staff of Fire equipped (removes 3 fire runes from the cost; no Attack requirement — the default path)" - "A bank with GE access within reach" + - "30 Attack + 50 Magic ONLY if adopting the optional Bryophyta's staff + Tome of Fire technique — below that the Staff of Fire path applies" best_practice: equipment: @@ -32,8 +34,8 @@ best_practice: - name: cast on noted (stacked) items why: "one inventory slot, minimal mouse travel between spell icon and item, no item un-noting" source: "OSRS wiki — Low Level Alchemy (Usage: casting alchemy spells on noted items)" - - name: Bryophyta's staff + Tome of Fire (members-only, 50 Magic) - why: "charged Bryophyta's staff holds up to 1,000 nature runes (0 rune inventory slots) with a 1/15 chance per cast to not consume a nature rune; charged Tome of Fire supplies infinite fire runes in the shield slot and non-combat spells (alchemy) do not consume charges — the wiki documents this exact pair for alchemy profit; payback takes thousands of casts, so adopt at 50+ Magic when nature prices are high" + - name: Bryophyta's staff + Tome of Fire (members-only, 50 Magic, 30 Attack) + why: "charged Bryophyta's staff holds up to 1,000 nature runes (0 rune inventory slots) with a 1/15 chance per cast to not consume a nature rune; charged Tome of Fire supplies infinite fire runes in the shield slot and non-combat spells (alchemy) do not consume charges — the wiki documents this exact pair for alchemy profit; payback takes thousands of casts, so adopt at 50+ Magic when nature prices are high. EQUIP GATE: the staff requires 30 Attack + 30 Magic and the tome requires 50 Magic — accounts below 30 Attack (e.g. 1-Attack mules) cannot equip it and keep the ordinary Staff of Fire path (equipment section above) instead" source: "OSRS wiki — Bryophyta's staff (passive effect; 'can be used along with a tome of fire to increase profits with High Level Alchemy'); OSRS wiki — Tome of Fire (level 50 Magic, non-combat spells do not consume charges)" - name: self-craft nature runes as a cost cap why: "members with 44 Runecraft can craft natures from pure essence, capping the per-cast rune cost at essence+time when GE nature prices spike; use the Abyss (no talisman needed) from the Mage of Zamorak" @@ -42,37 +44,49 @@ best_practice: economy: # Verified 2026-08-27 against prices.runescape.wiki (live 'latest') + # OSRS wiki (spell infoboxes, Module:Exchange limits, item pages). - item: Rune arrow # example anchor; rank by least-loss candidate live - item_id: 892 - cost_per_cycle: 188 # arrow buy 44 + nature rune 144 (staff of fire) - income_per_cycle: 160 # lowalch of rune arrow (40% of value 400) - net_per_cycle: -28 - gp_per_hour_low: -60000 # negative: this burns GP (31 xp/cast is the product) - gp_per_hour_high: -40000 - xp_per_hour: 55800 # 31 xp/cast (wiki) @ ~1800 casts/hr (3-tick, wiki) + # Re-verified at bounce fix 2026-08-27 with a fresh live fetch; the + # modeled anchor is now the ACTUAL least-loss liquid candidate. + item: Rune javelin tips # least-loss liquid anchor 2026-08-27 (see profit_drivers) + item_id: 19580 + cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire) + income_per_cycle: 540 # lowalch of rune javelin tips (40% of value 1350) + net_per_cycle: -3 + gp_per_hour_low: -6750 # -3/cast @ max cadence: 3600/1.6s delay = 2250 casts/hr + gp_per_hour_high: -3176 # -3/cast @ min cadence: 3600/3.4s delay = 1058.8 casts/hr + xp_per_hour: 44640 # 31 xp/cast (wiki) @ 1,440 casts/hr — pinned below + # Pacing pin: actions[].anti_ban.delay [1600, 3400] ms is the MANDATORY + # post-cast pause and the cadence constraint. A game tick is 0.6s, so a + # 3-tick LA cast is 1.8s — shorter than the 2.5s midpoint pause, so the + # pause, not the cast, sets the rate: 3600/2.5 = 1,440 casts/hr. + # xp_per_hour = 1440 x 31 = 44,640/hr + # gp_per_hour (midpoint) = 1440 x -3 = -4,320/hr; the low/high range + # above is the delay-bound spread (1,600ms -> 2,250 casts; 3,400ms -> + # 1,058.8 casts). supplies: - - item: rune arrow + - item: rune javelin tips qty_per_cycle: 1 source: ge - ge_limit: 11000 # mapping + Module:Exchange/Rune arrow agree (wiki-verified) + ge_limit: 10000 # mapping + Module:Exchange/Rune javelin tips agree (re-verified 2026-08-27) - item: nature rune qty_per_cycle: 1 source: ge ge_limit: 18000 profit_drivers: - - "No profit to chase — this is loss-minimizing training: XP is the product. Pick the least-loss candidate live each run." - - "Top 10 losses 2026-08-27 (rank by (lowalch - buy), members catalog, mapping 'latest' low as buy basis):" - - "1. Mystic mist staff (id 20733, lowalch 17000, buy 1000, spread +16000, GE limit 8)" - - "2. Trailblazer top (t1) (id 25031, lowalch 6000, buy 555, spread +5445, GE limit 5)" - - "3. Twisted trousers (t2) (id 24401, lowalch 4000, buy 100, spread +3900, GE limit 5)" - - "4. Dual sai (id 23206, lowalch 2400, buy 1500, spread +900, GE limit 8)" - - "5. Adamant hasta(p) (id 11407, lowalch 832, buy 1, spread +831, GE limit 125)" - - "6. Adamant spear(p++) (id 5726, lowalch 832, buy 10, spread +822, GE limit 125)" - - "7. Bagged flower (id 8451, lowalch 2000, buy 1250, spread +750, GE limit 10000)" - - "8. Adamant spear(p) (id 1259, lowalch 832, buy 200, spread +632, GE limit 125)" - - "9. Frying pan (id 7441, lowalch 664, buy 101, spread +563, GE limit 50)" - - "10. Maple blackjack(d) (id 6420, lowalch 640, buy 100, spread +540, GE limit 125)" - - "Caveat: thin items (limits 5-8) cannot sustain a run and their 'low' fills can be stale — a bot cannot reliably buy at those prices. The sustainable loss-minimizers are the liquid high-limit set: rune arrow (892, -28/cast), rune javelin tips (19580, -7/cast), opal amulet (u) (21099, -199/cast), jade ring (21084, -63/cast)." + - "No guaranteed profit to chase — this is loss-minimizing training: XP is the product. Pick the least-loss (least-negative net) candidate live each run." + - "Least-loss liquid anchors 2026-08-27 (net = lowalch - low buy - nature 140): rune javelin tips (19580, -3/cast, limit 10,000) is the modeled anchor; rune arrow (892, -25/cast, limit 11,000) is the runner-up." + - "Jade ring (21084) is currently MARGINAL-PROFIT (+67/cast, low/high 303/326) — flips sign as prices move, so it is not the loss anchor. Opal amulet (u) (21099) shows a 20x low/high spread (17/365) = stale-low artifact, disqualified per data-sources.md thin-fill trap." + - "Alch-margin candidates by (lowalch - buy) 2026-08-27 (thin limits cannot sustain a run; 'low' fills on thin items can be stale):" + - "1. Trailblazer top (t1) (id 25031, lowalch 6000, buy 555, margin +5445, GE limit 5)" + - "2. Twisted trousers (t2) (id 24401, lowalch 4000, buy 100, margin +3900, GE limit 5)" + - "3. Adamant hasta(p) (id 11407, lowalch 832, buy 1, margin +831, GE limit 125)" + - "4. Adamant spear(p++) (id 5726, lowalch 832, buy 10, margin +822, GE limit 125)" + - "5. Bagged flower (id 8451, lowalch 2000, buy 1250, margin +750, GE limit 10000 — deep limit but thin market: a 10k fill at 1,250 is unverifiable, do not model a run on it)" + - "6. Dual sai (id 23206, lowalch 2400, buy 1750, margin +650, GE limit 8)" + - "7. Frying pan (id 7441, lowalch 664, buy 101, margin +563, GE limit 50)" + - "8. Maple blackjack(d) (id 6420, lowalch 640, buy 100, margin +540, GE limit 125)" + - "9. Adamant spear(p) (id 1259, lowalch 832, buy 450, margin +382, GE limit 125)" + - "10. Mystic mist staff (id 20733, lowalch 17000, buy 75727, margin -58727, GE limit 8) — buy spiked ABOVE alch value 2026-08-27; a true loss trap, do not buy" + - "Caveat: thin items (limits 5-125) cannot sustain a run and their 'low' fills can be stale — a bot cannot reliably buy at those prices. The dependable loss-minimizers are the liquid high-limit pair modeled above: rune javelin tips (-3/cast) and rune arrow (-25/cast)." state_machine: initial: idle @@ -148,7 +162,7 @@ actions: on: "itself" on_fail: retry anti_ban: - delay: [1600, 3400] # 3-tick cast = ~5.4s game; stay under it, humanised + delay: [1600, 3400] # MANDATORY post-cast pause (cadence policy): 3-tick cast = 1.8s (0.6s/tick); midpoint 2.5s -> ~1,440 casts/hr (pinned in economy) - id: a4 step: bank-loop @@ -173,19 +187,19 @@ run_control: risk: ban_level: low notes: - - "High cast cadence is the main detection vector; keep anti_ban delays." + - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: ~1,440 casts/hr at the 2.5s midpoint pause)." - "GE buy bursts: pace offers, respect the 4h limit window; thin items (limit < 100) cannot sustain a long run." - - "Thin-item 'latest low' fills can be stale — confirm fill price with a live GE offer before committing a run; the liquid high-limit set is the dependable loss-minimizer." + - "Thin-item 'latest low' fills can be stale — confirm fill price with a live GE offer before committing a run; the liquid high-limit pair (rune javelin tips / rune arrow) is the dependable loss-minimizer." sources: - "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, 1 nature + 3 fire; spellbook Normal; infobox 2026-08-27)" - "OSRS wiki — High Level Alchemy (level 55, 65 xp/cast, 1 nature + 5 fire; 60% of item value; infobox 2026-08-27)" - "OSRS wiki — Superheat Item (level 43, 53 xp/cast, 1 nature + 4 fire; smithing production spell; infobox 2026-08-27)" - "OSRS wiki — Low Level Alchemy (handoff quote: 'Players with level 55 Magic are generally recommended to use High Level Alchemy instead, as it provides more coins and experience per cast than Low Level Alchemy')" - - "prices.runescape.wiki mapping 2026-08-27 (members/lowalch/highalch/limit catalog; lowalch == highalch*2//3 asserted for cited items)" - - "prices.runescape.wiki latest 2026-08-27 (low = per-repo buy basis; rune arrow buy 44, nature rune buy 144)" - - "OSRS wiki — Module:Exchange/Rune arrow, Nature rune, Staff of fire + top-10 candidates (GE limits agree with mapping; verified 2026-08-27)" - - "OSRS wiki — Rune arrow / Bagged flower / Mystic mist staff etc. item pages (members = Yes for every cited catalog item; verified 2026-08-27)" + - "prices.runescape.wiki mapping 2026-08-27 (members/lowalch/highalch/limit catalog; rune javelin tips id 19580: lowalch 540, highalch 810, limit 10000, members=true; lowalch == highalch*2//3 asserted for every cited item)" + - "prices.runescape.wiki latest 2026-08-27, re-fetched at bounce fix (low = per-repo buy basis: rune javelin tips 403, rune arrow 45, nature rune 140, jade ring 303, opal amulet (u) 17)" + - "OSRS wiki — Module:Exchange/Rune javelin tips (limit 10000, lowalch 540, highalch 810, members=true — matches mapping; re-verified 2026-08-27) + Module:Exchange/Nature rune, Staff of fire and all cited candidates (GE limits agree with mapping; verified 2026-08-27)" + - "OSRS wiki — Rune javelin tips / Rune arrow / Bagged flower / Mystic mist staff etc. item pages (members = Yes for every cited catalog item; verified 2026-08-27)" - "OSRS wiki — Nature rune (members craft at level 44 Runecraft from pure essence, 9 xp; Nature Altar south-east of Tai Bwo Wannai)" - "OSRS wiki — Abyss (access to runic altars without talismans via the Mage of Zamorak)" - "OSRS wiki — Bryophyta's staff (level 30 Attack + Magic; up to 1,000 nature charges; 1/15 chance to not consume a charge on any nature-rune spell; alchemy combo with Tome of Fire)" From 284ec17370abc6fa0cb746650e4e1a2d43789a64 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 16:49:41 -0500 Subject: [PATCH 07/15] codex: fix low-alch-members pacing model per governor review (bounce fix attempt 2) - anti_ban.delay is additive to the 1.8s cast (action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> ~837 casts/hr - xp_per_hour 44,640 -> 25,953; gp_per_hour range -3,177..-2,077; formula pinned in economy pacing block and actions a3 comment - run_control.iteration = one full inventory+bank cycle (break.every counts iterations per docs/schema.md), bank-cycle claim removed - risk note cadence pin updated; version 2 -> 3 --- codex/economy/low-alch-members.yaml | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml index 3a0b8dd..1cf908b 100644 --- a/codex/economy/low-alch-members.yaml +++ b/codex/economy/low-alch-members.yaml @@ -3,7 +3,7 @@ title: Low-Level Alchemy (Members) domain: economy tags: [alching, magic-training, members, loss-training] status: verified -version: 2 # 2: bounce-fix 2026-08-27 — pacing-pinned economics, least-loss anchor, 30 Attack gate +version: 3 # 3: bounce-fix 2 2026-08-27 — additive cast+delay cycle (1.8s + 1.6-3.4s), iteration = full inventory+bank cycle updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > @@ -51,17 +51,17 @@ economy: cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire) income_per_cycle: 540 # lowalch of rune javelin tips (40% of value 1350) net_per_cycle: -3 - gp_per_hour_low: -6750 # -3/cast @ max cadence: 3600/1.6s delay = 2250 casts/hr - gp_per_hour_high: -3176 # -3/cast @ min cadence: 3600/3.4s delay = 1058.8 casts/hr - xp_per_hour: 44640 # 31 xp/cast (wiki) @ 1,440 casts/hr — pinned below - # Pacing pin: actions[].anti_ban.delay [1600, 3400] ms is the MANDATORY - # post-cast pause and the cadence constraint. A game tick is 0.6s, so a - # 3-tick LA cast is 1.8s — shorter than the 2.5s midpoint pause, so the - # pause, not the cast, sets the rate: 3600/2.5 = 1,440 casts/hr. - # xp_per_hour = 1440 x 31 = 44,640/hr - # gp_per_hour (midpoint) = 1440 x -3 = -4,320/hr; the low/high range - # above is the delay-bound spread (1,600ms -> 2,250 casts; 3,400ms -> - # 1,058.8 casts). + gp_per_hour_low: -3177 # -3/cast @ fastest cycle (1.8s cast + 1.6s delay = 3.4s) -> 1058.8 casts/hr + gp_per_hour_high: -2077 # -3/cast @ slowest cycle (1.8s cast + 3.4s delay = 5.2s) -> 692.3 casts/hr + xp_per_hour: 25953 # 31 xp/cast (wiki) @ midpoint cycle 837.2 casts/hr — pinned below + # Pacing pin (docs/action-grammar.md:135): actions[].anti_ban.delay is a + # MANDATORY post-action sleep, ADDITIVE to the action itself — it does not + # overlap the cast. LA cast = 3 game ticks x 0.6s = 1.8s; the delay + # [1600, 3400]ms = 1.6-3.4s. Cycle = cast + delay = 3.4s (fast) .. 5.2s + # (slow), midpoint 4.3s. + # casts/hr = 3600 / (1.8 + delay_s): 3.4s -> 1058.8; 5.2s -> 692.3; 4.3s -> 837.2 + # xp_per_hour (midpoint) = 837.2 x 31 = 25,953/hr (range 21,461 .. 32,823 by delay bounds) + # gp_per_hour = casts/hr x -3: midpoint -2,512; low (fast) -3,177; high (slow) -2,077 supplies: - item: rune javelin tips qty_per_cycle: 1 @@ -162,7 +162,7 @@ actions: on: "itself" on_fail: retry anti_ban: - delay: [1600, 3400] # MANDATORY post-cast pause (cadence policy): 3-tick cast = 1.8s (0.6s/tick); midpoint 2.5s -> ~1,440 casts/hr (pinned in economy) + delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> ~837 casts/hr (pinned in economy) - id: a4 step: bank-loop @@ -174,10 +174,10 @@ actions: on_fail: alert run_control: - iteration: "one cast" + iteration: "one full inventory + bank cycle" # per docs/schema.md run_control: break.every counts iterations; one cycle = cast the carried candidate supply down to buffer, bank the coins, restock — a cycle ends at the bank visit max_iterations: null break: - every: [8, 14] # bank cycles (breaks land on bank visits) + every: [8, 14] # iterations = full inventory+bank cycles (breaks land on bank visits) for: [45, 120] # seconds of idle AFK stop_conditions: - "magic level >= target (55 -> hand off to HLA)" @@ -187,7 +187,7 @@ run_control: risk: ban_level: low notes: - - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: ~1,440 casts/hr at the 2.5s midpoint pause)." + - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: ~837 casts/hr at the 4.3s midpoint cycle = 1.8s cast + 2.5s post-cast sleep)." - "GE buy bursts: pace offers, respect the 4h limit window; thin items (limit < 100) cannot sustain a long run." - "Thin-item 'latest low' fills can be stale — confirm fill price with a live GE offer before committing a run; the liquid high-limit pair (rune javelin tips / rune arrow) is the dependable loss-minimizer." From 00bfc358cbd2f89ab51e7ccb7f9c0a541f5bfac7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 02:35:53 -0500 Subject: [PATCH 08/15] fix(validate): cap GE floor tax at 5,000,000 GP per item (owner decision A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner decision 2026-08-28: single global GE tax formula for all entries: min(floor(0.02 * sell_price), 5_000_000). Apply the per-item cap inside ge_floor_tax_error(); the margin-tag scope (flipping/merchanting) and the generic-ge non-margin exemption are unchanged. Regression fixtures now cover: - fractional-tax flipping entry must FAIL (kept) - non-margin entry with only ge + processing must PASS (kept) - correctly-floored flipping entry must PASS (kept) - high-value flip above the 5,000,000 GP cap (sell 300,000,000 / buy 294,000,000 / net 1,000,000) must PASS — the old uncapped rule computed 6,000,000 tax, demanded net 0, and rejected this valid entry (new) Closes the governor's attempt-3 blocker on PR !7. --- tools/validate.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tools/validate.py b/tools/validate.py index 5947f6a..58f20ed 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -13,15 +13,18 @@ Runs: => WARNING (citation TODO — file a follow-up issue) - GE-margin entries (tags include flipping and/or merchanting — the unambiguous margin signals): the 2% GE convenience fee is - rounded DOWN to whole coins per item, so net_per_cycle MUST - equal income - floor(0.02*income) - cost. Fractional-tax - arithmetic (0.98*sell - buy) is an error. + rounded DOWN to whole coins per item and capped at 5,000,000 GP + per item, so net_per_cycle MUST equal + income - min(floor(0.02*income), 5_000_000) - cost. + Fractional-tax arithmetic (0.98*sell - buy) and uncapped tax + (6M on a 300M sale) are errors. NOTE: the generic `ge` tag alone does NOT gate this rule — a training/processing/procurement entry may legitimately carry `ge` while its income/cost model has nothing to do with flip tax. - Regression fixtures (--selftest): a fractional-tax flipping - entry must FAIL; a non-margin entry carrying only `ge` plus a - non-flip tag must PASS. + entry must FAIL; an above-cap high-value flip with the + correctly capped-tax net must PASS; a non-margin entry carrying + only `ge` plus a non-flip tag must PASS. 3. Global id uniqueness across the whole codex tree. Exit codes: 0 = clean (warnings ok), 1 = validation errors. @@ -69,7 +72,11 @@ def ge_floor_tax_error(rel, data): Wiki: "The 2% tax rounds down to the nearest whole number, so items sold for below 50 coins have a tax of less than one coin" — per-item - net MUST use the floored tax, never 0.98*sell - buy. + net MUST use the floored tax, never 0.98*sell - buy. The fee is also + capped at 5,000,000 GP per item (per-item cap, documented in the + flip-candidates best_practice/risk), so tax = min(floor(0.02*sell), + 5_000_000). Owner decision 2026-08-28: one global formula for all + entries (option A), no per-entry tax-model discriminator. """ tags = {str(t).lower() for t in data.get("tags", [])} if not tags & MARGIN_TAGS: @@ -78,13 +85,15 @@ def ge_floor_tax_error(rel, data): if not all(k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle")): return None sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"] - tax = math.floor(sell * 0.02) + tax = min(math.floor(sell * 0.02), 5_000_000) expected = sell - tax - buy if eco["net_per_cycle"] != expected: return ( - f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model " - f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); " - f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid") + f"{rel}: net_per_cycle {eco['net_per_cycle']} != capped floored-tax " + f"model {expected} (sell {sell} - min(floor({sell}*0.02), 5_000_000)" + f"={tax} - buy {buy}); " + f"GE tax rounds down per item and is capped at 5,000,000 GP per item " + f"— fractional 0.98*sell - buy and uncapped tax are invalid") return None @@ -101,6 +110,13 @@ def run_selftest(verbose=False): {"income_per_cycle": 5000, "cost_per_cycle": 3000, "net_per_cycle": 2000}, False), ("correctly-floored flipping entry", ["flipping", "merchanting"], {"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17}, False), + # sell 300,000,000 -> uncapped floor tax is 6,000,000; the per-item + # cap makes it 5,000,000, so the correct net is 1,000,000. The old + # uncapped rule demanded net=0 and rejected this valid entry. + ("high-value flip above the 5,000,000 GP per-item tax cap", + ["flipping"], + {"income_per_cycle": 300_000_000, "cost_per_cycle": 294_000_000, + "net_per_cycle": 1_000_000}, False), ] errors = [] for name, tags, eco, expect_error in fixtures: From 5ddfc6cdbc20d83223b8396c5c9c0b43a0bd0d2d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 02:56:53 -0500 Subject: [PATCH 09/15] codex: repair low-alch-members break/iteration model per owner guidance (bounce fix attempt 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iteration = one cast; break.every = [400, 700] COMPLETED casts, cast-count based and independent of bank visits (owner decision A) — bank-cycle claim removed - numeric carried_batch 1,000/1,000 + restock_threshold 100 -> deterministic restock trigger; first restock at cast 901 (worked example pinned) - full runnable lifecycle in actions: reach-bank, buy-staff, equip-staff (with fire-rune fallback failure path), buy-supplies, buy-natures, build-loadout, cast-alch (exact consumption + coins), restock, take-break - rates now REALIZED incl. setup/restock/breaks: xp_per_hour 24,500, gp_per_hour -2,960..-1,980; loop-only 837/hr derivation still pinned - pinned worked example: pre-inventory -> break #1 @550 -> first restock @901 -> resume @902 -> break #2 @1452 -> restock #2 @1802 - staff of fire equip requirement wiki-verified 2026-08-28: none documented (infobox + prose silent vs Battlestaff's 30 Attack/30 Magic) - version 3 -> 4; validator 2 entries 0 errors (1 pre-existing F2P WARN); Closes #2 --- codex/economy/low-alch-members.yaml | 234 ++++++++++++++++++++++------ 1 file changed, 184 insertions(+), 50 deletions(-) diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml index 1cf908b..002074f 100644 --- a/codex/economy/low-alch-members.yaml +++ b/codex/economy/low-alch-members.yaml @@ -3,8 +3,8 @@ title: Low-Level Alchemy (Members) domain: economy tags: [alching, magic-training, members, loss-training] status: verified -version: 3 # 3: bounce-fix 2 2026-08-27 — additive cast+delay cycle (1.8s + 1.6-3.4s), iteration = full inventory+bank cycle -updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date +version: 4 # 4: bounce-fix 3 2026-08-28 (owner guidance) — iteration = one cast, break.every in CAST counts (independent of bank visits), numeric carried_batch/restock_threshold, realized rates incl. setup+restock+breaks, full-lifecycle actions, pinned worked example +updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > Magic training from level 21 by low-alching members items with the lowest @@ -13,26 +13,32 @@ summary: > (mapping lowalch != null, members=true, live GE prices 2026-08-27). At current prices the method burns a small amount of GP per cast for 31 XP — run it for XP, not GP. Least-loss liquid anchor at last check: rune - javelin tips (-3/cast). Handoff: switch to High Level Alchemy at level 55 - (hard level gate + strictly more coins AND XP per cast; see sources). + javelin tips (-3/cast). Model pin (owner guidance 2026-08-28): one + iteration = one cast; breaks every 400-700 COMPLETED CASTS (cast-count + based, independent of bank visits); restock when candidate or nature + drops below the numeric restock_threshold (100) against a carried_batch + of 1,000/1,000 — pinned worked example below. Handoff: switch to High + Level Alchemy at level 55 (hard level gate + strictly more coins AND XP + per cast; see sources). accounts: membership: members min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified) requires_quest: [] requires_other: - - "Staff of Fire equipped (removes 3 fire runes from the cost; no Attack requirement — the default path)" - - "A bank with GE access within reach" + - "Staff of Fire equipped (removes 3 fire runes from the cost; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources — this is the default path)" + - "A bank with GE access within reach (alch spot = GE bank booth; restock trip 60-120s)" + - "Working capital >= 543,000 gp for one carried_batch (1,000 x 403 gp javelin tips + 1,000 x 140 gp natures, live 2026-08-27)" - "30 Attack + 50 Magic ONLY if adopting the optional Bryophyta's staff + Tome of Fire technique — below that the Staff of Fire path applies" best_practice: equipment: - item: staff of fire - why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved)" - source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)" + why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved); equip requirement: none documented on the OSRS wiki (verified 2026-08-28)" + source: "OSRS wiki — Staff of Fire (id 1387, F2P item, tradeable, no requirements row in the infobox and none in the prose — compare Battlestaff which states 'requires 30 Attack and 30 Magic'); Low Level Alchemy (Cost)" techniques: - name: cast on noted (stacked) items - why: "one inventory slot, minimal mouse travel between spell icon and item, no item un-noting" + why: "one inventory slot, minimal mouse travel between spell icon and item, no item un-noting; means an inventory does NOT define a batch size — batch sizes are the explicit numeric carried_batch below" source: "OSRS wiki — Low Level Alchemy (Usage: casting alchemy spells on noted items)" - name: Bryophyta's staff + Tome of Fire (members-only, 50 Magic, 30 Attack) why: "charged Bryophyta's staff holds up to 1,000 nature runes (0 rune inventory slots) with a 1/15 chance per cast to not consume a nature rune; charged Tome of Fire supplies infinite fire runes in the shield slot and non-combat spells (alchemy) do not consume charges — the wiki documents this exact pair for alchemy profit; payback takes thousands of casts, so adopt at 50+ Magic when nature prices are high. EQUIP GATE: the staff requires 30 Attack + 30 Magic and the tome requires 50 Magic — accounts below 30 Attack (e.g. 1-Attack mules) cannot equip it and keep the ordinary Staff of Fire path (equipment section above) instead" @@ -45,28 +51,36 @@ economy: # Verified 2026-08-27 against prices.runescape.wiki (live 'latest') + # OSRS wiki (spell infoboxes, Module:Exchange limits, item pages). # Re-verified at bounce fix 2026-08-27 with a fresh live fetch; the - # modeled anchor is now the ACTUAL least-loss liquid candidate. + # modeled anchor is the ACTUAL least-loss liquid candidate. + # Bounce-fix 3 (2026-08-28, owner guidance): all rate fields are REALIZED + # figures — setup/restock/breaks included, not just the cast loop. item: Rune javelin tips # least-loss liquid anchor 2026-08-27 (see profit_drivers) item_id: 19580 cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire) income_per_cycle: 540 # lowalch of rune javelin tips (40% of value 1350) net_per_cycle: -3 - gp_per_hour_low: -3177 # -3/cast @ fastest cycle (1.8s cast + 1.6s delay = 3.4s) -> 1058.8 casts/hr - gp_per_hour_high: -2077 # -3/cast @ slowest cycle (1.8s cast + 3.4s delay = 5.2s) -> 692.3 casts/hr - xp_per_hour: 25953 # 31 xp/cast (wiki) @ midpoint cycle 837.2 casts/hr — pinned below - # Pacing pin (docs/action-grammar.md:135): actions[].anti_ban.delay is a - # MANDATORY post-action sleep, ADDITIVE to the action itself — it does not - # overlap the cast. LA cast = 3 game ticks x 0.6s = 1.8s; the delay - # [1600, 3400]ms = 1.6-3.4s. Cycle = cast + delay = 3.4s (fast) .. 5.2s - # (slow), midpoint 4.3s. - # casts/hr = 3600 / (1.8 + delay_s): 3.4s -> 1058.8; 5.2s -> 692.3; 4.3s -> 837.2 - # xp_per_hour (midpoint) = 837.2 x 31 = 25,953/hr (range 21,461 .. 32,823 by delay bounds) - # gp_per_hour = casts/hr x -3: midpoint -2,512; low (fast) -3,177; high (slow) -2,077 + gp_per_hour_low: -2960 # realized (incl. restock+breaks), fastest cycle 3.4s -> 986.4 casts/hr x -3 = -2,959 + gp_per_hour_high: -1980 # realized (incl. restock+breaks), slowest cycle 5.2s -> 660.6 casts/hr x -3 = -1,982 + xp_per_hour: 24500 # realized midpoint 791.3 casts/hr x 31 xp = 24,530 (pin 24,500) + # ---- realized-rate derivation (pinned; owner guidance 2026-08-28) ---- + # LOOP: cycle = 1.8s cast (3 ticks x 0.6s) + anti_ban.delay [1.6, 3.4]s ADDITIVE + # (docs/action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s -> loop-only + # casts/hr = 3600/4.3 = 837.2; loop-only xp = 25,953 (837.2 x 31); + # loop-only gp midpoint = -2,512 (837.2 x -3). + # OVERHEAD per 901-cast batch (carried_batch 1000 - restock_threshold 100): + # restock trip [60, 120]s (midpoint 90s, GE-adjacent booth); + # breaks: every [400, 700] casts (midpoint 550) for [45, 120]s (midpoint + # 82.5s) -> 901/550 = 1.64 breaks x 82.5s = 135s. + # Total overhead = 90 + 135 = 225s per 901 casts. + # REALIZED casts/hr = 901 / ((901 x cycle_s + 225) / 3600): + # fast 3.4s -> 986.4; midpoint 4.3s -> 791.3; slow 5.2s -> 660.6. + # xp_per_hour = 791.3 x 31 = 24,530 (pin 24,500; range 20,480..30,580). + # gp_per_hour = realized casts/hr x -3: fast -2,959 (low), slow -1,982 (high), midpoint -2,374. supplies: - item: rune javelin tips qty_per_cycle: 1 source: ge - ge_limit: 10000 # mapping + Module:Exchange/Rune javelin tips agree (re-verified 2026-08-27) + ge_limit: 10000 # mapping + Module:Exchange/Rune javelin tips agree (re-verified 2026-08-27); one carried_batch of 1,000 fits the 4h window - item: nature rune qty_per_cycle: 1 source: ge @@ -87,31 +101,47 @@ economy: - "9. Adamant spear(p) (id 1259, lowalch 832, buy 450, margin +382, GE limit 125)" - "10. Mystic mist staff (id 20733, lowalch 17000, buy 75727, margin -58727, GE limit 8) — buy spiked ABOVE alch value 2026-08-27; a true loss trap, do not buy" - "Caveat: thin items (limits 5-125) cannot sustain a run and their 'low' fills can be stale — a bot cannot reliably buy at those prices. The dependable loss-minimizers are the liquid high-limit pair modeled above: rune javelin tips (-3/cast) and rune arrow (-25/cast)." + - "Batch-scale economics (default staff-of-fire path, worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = -2,703 gp, +27,931 xp, 2 x 540 gp coin deposits." state_machine: initial: idle terminal: [complete, failed] states: idle: - goal: reach the bank + have supplies in inventory - next: [casting, bank_supplies] + goal: "acquire + equip the staff of fire, GE-buy candidate + natures, build the starting inventory to carried_batch (1,000 candidate + 1,000 nature)" + next: [casting, complete, failed] casting: - goal: cast Low Level Alchemy on the current candidate item - next: [bank_supplies, idle, complete, failed] + goal: "cast Low Level Alchemy on the current candidate item — each cast consumes 1 candidate + 1 nature and pays 540 gp (lowalch), +31 xp, net -3 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts)" + next: [bank_supplies, complete, failed] bank_supplies: - goal: bank the produced coins, withdraw fresh candidate + runes - next: [casting, idle] + goal: "deposit coins + products, withdraw candidate + natures back to carried_batch (1,000/1,000), resume casting" + next: [casting, failed] complete: - goal: reached the run's XP/level target + goal: "reached the run's XP/level target (default 55 -> hand off to HLA)" failed: - goal: hard failure — stop, log ERROR sentinel, alert + goal: "hard failure — stop, log ERROR sentinel, alert" transitions: - - from: casting - when: "candidate item count < buffer OR nature rune count < buffer" - to: bank_supplies - from: idle - when: "magic level >= target (default 55, hand off to HLA at 55)" + when: "staff equipped AND candidate == carried_batch AND nature == carried_batch (1,000 and 1,000)" + to: casting + - from: idle + when: "magic level >= target (55, hand off to HLA)" to: complete + - from: idle + when: "3 consecutive purchase/equip failures with no fallback available (recovery block below)" + to: failed + - from: casting + when: "candidate count < restock_threshold (100) OR nature rune count < restock_threshold (100) — deterministic restock trigger, pinned numbers" + to: bank_supplies + - from: casting + when: "magic level >= target (55, hand off to HLA)" + to: complete + - from: casting + when: "3 consecutive failed casts (client issue — do not loop)" + to: failed + - from: bank_supplies + when: "restocked to carried_batch (1,000 candidate + 1,000 nature)" + to: casting # --- handoff note (wiki-verified 2026-08-27, do not copy from other entries) --- # Low Level Alchemy: level 21, 31 xp/cast, 1 nature + 3 fire (spellbook: Normal). @@ -134,25 +164,69 @@ actions: verb: move_to target: bank params: - location: "nearest bank with GE" + location: "GE-adjacent bank (alch spot = the bank booth, so restock is a single short leg)" method: [teleport, walk] on_fail: retry anti_ban: delay: [400, 1600] - id: a2 + step: buy-staff + verb: ge_buy + target: grand-exchange + params: + item: "staff of fire" + qty: 1 + max_price: "live low + 25% (wiki value 1,500; hard cap 5,000)" + on_fail: retry # retry once; on a second failure use the fire-rune fallback (see a3 note / recovery block) + anti_ban: + delay: [1200, 3000] + + - id: a3 + step: equip-staff + verb: menu_option + target: "staff of fire in inventory" + params: + option: "Wield" + note: "Equip requirement is NONE documented on the OSRS wiki Staff of fire page (no requirements row in the infobox, none in the prose; compare Battlestaff which states 'requires 30 Attack and 30 Magic'; verified 2026-08-28). FAILURE PATH — if Wield is unavailable or the staff cannot be equipped, fall back to buying 3 fire runes per cast (3 fire + 1 nature = 155 gp rune cost vs 140, net -18/cast at the anchor), log WARN and continue; never loop on equip attempts." + on_fail: retry + anti_ban: + delay: [600, 1800] + + - id: a4 step: buy-supplies verb: ge_buy target: grand-exchange params: - item: "current candidate item" - qty: "fill to buffer" + item: "current least-loss live candidate (default: rune javelin tips)" + qty: "carried_batch = 1,000 (fits the 4h GE limit window of 10,000)" + max_price: "live low + 5%" + on_fail: retry # re-place at live low +5% with paced offers; 3 consecutive failures -> failed (recovery block) + anti_ban: + delay: [1200, 3000] + + - id: a5 + step: buy-natures + verb: ge_buy + target: grand-exchange + params: + item: "nature rune" + qty: "carried_batch = 1,000 (4h GE limit 18,000)" max_price: "live low + 5%" on_fail: retry anti_ban: delay: [1200, 3000] - - id: a3 + - id: a6 + step: build-loadout + verb: bank_inventory + target: bank + params: + deposit: [overflow products] + withdraw: "candidate 1,000 + nature runes 1,000 (= carried_batch; both stackable, two inventory slots)" + on_fail: retry + + - id: a7 step: cast-alch verb: use_item_on target: "candidate item in inventory" @@ -160,37 +234,95 @@ actions: spell: "Low Level Alchemy" item: "least-loss live candidate in inventory" on: "itself" + consumption: "1 candidate + 1 nature per cast (staff of fire default); pays 540 gp (lowalch) + 31 xp, net -3 gp" on_fail: retry anti_ban: - delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> ~837 casts/hr (pinned in economy) + delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> realized ~791 casts/hr incl. restock+breaks (pinned in economy) - - id: a4 - step: bank-loop + - id: a8 + step: restock verb: bank_inventory target: bank params: deposit: [all coins, all products] - withdraw: "candidate item + nature runes up to buffer" + withdraw: "candidate + nature runes up to carried_batch (1,000/1,000)" on_fail: alert + - id: a9 + step: take-break + verb: take_break + target: "current location" + params: + duration: "[45, 120] seconds of idle AFK per break (matches run_control.break.for)" + note: "executed by the translator when the completed-cast counter reaches run_control.break.every — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is not reset by restocking" + on_fail: skip + run_control: - iteration: "one full inventory + bank cycle" # per docs/schema.md run_control: break.every counts iterations; one cycle = cast the carried candidate supply down to buffer, bank the coins, restock — a cycle ends at the bank visit - max_iterations: null + iteration: "one cast" # = one Low Level Alchemy cast (+31 xp, -3 gp at the anchor). Per docs/schema.md run_control, break.every counts ITERATIONS — so it counts CASTS here, NOT bank trips (owner decision 2026-08-28) + max_iterations: null # null = until complete condition break: - every: [8, 14] # iterations = full inventory+bank cycles (breaks land on bank visits) - for: [45, 120] # seconds of idle AFK + every: [400, 700] # COMPLETED CASTS between AFK breaks (midpoint 550 = ~39 min at the 4.3s midpoint cycle). Cast-count-based and independent of bank visits; a restock trip NEVER counts as a break and does not reset this counter + for: [45, 120] # seconds of idle AFK per break stop_conditions: - "magic level >= target (55 -> hand off to HLA)" - - "gp under floor" + - "gp under floor (working capital below one carried_batch = 543,000 gp)" - "3 consecutive failed casts (client issue — do not loop)" + - "3 consecutive failed GE purchases with no fallback candidate available" + + # ================= WORKED EXAMPLE (pinned, deterministic — owner guidance 2026-08-28) ================= + # Constants: carried_batch = 1,000 candidate + 1,000 nature; restock_threshold = 100; + # break.every = [400, 700] casts (midpoint 550); break.for = [45, 120] s; + # default staff-of-fire path consumes 1 candidate + 1 nature per cast (worst case 1:1 — + # with Bryophyta's staff the nature side drains slower, so the candidate stays binding). + # + # Step 0 — before the first inventory: account holds >= 543,000 gp working capital + # (1,000 x 403 + 1,000 x 140). At the GE bank: GE-buy staff of fire, Wield it + # (no documented requirement), GE-buy 1,000 rune javelin tips + 1,000 nature runes, + # withdraw both -> loadout = carried_batch 1,000/1,000. Cast counter := 0. + # Step 1 — casts #1..#550: each cast consumes 1 candidate + 1 nature (-543 gp) and pays + # 540 gp (lowalch) + 31 xp -> net -3 gp. Cast counter 0 -> 550 (~39 min at midpoint). + # Step 2 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every [400, 700]): run + # take-break for 45-120 s. Resume casting at cast 551. The break fired on the CAST + # COUNT, not on any bank visit; the restock counter is untouched. + # Step 3 — casts #551..#901: countdown candidate 1000-901 = 99 < 100 (restock_threshold) + # and nature 1000-901 = 99 < 100 simultaneously (worst case 1:1) -> the transition + # fires at the end of cast #901: FIRST RESTOCK/BANK EVENT at cast 901. + # Coins deposited at this restock: 901 x 540 = 486,540 gp; batch net so far -2,703 gp; + # batch xp so far 901 x 31 = 27,931. + # Step 4 — restock: deposit coins + products, withdraw 901 candidate + 901 nature -> + # carried_batch restored to 1,000/1,000. Resume casting at cast 902 (cast counter + # continues; the break counter also continues — restocking never resets either). + # Step 5 — SECOND BREAK when the cast-count interval since break #1 completes + # ([400, 700], midpoint: cast 902 + 550 = 1,452): take-break, resume at 1,453. + # Step 6 — restock #2 fires at cast 1,802 (901-cast batch cadence), and so on, until a + # stop_condition fires (Magic >= 55 -> HLA handoff; gp under floor; 3 failed casts). + # Bank visits and breaks are INDEPENDENT by construction: restock cadence = 901 casts; + # break cadence = 400-700 casts, and the two counters do not interact. + # ============================================================================================ risk: ban_level: low notes: - - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: ~837 casts/hr at the 4.3s midpoint cycle = 1.8s cast + 2.5s post-cast sleep)." - - "GE buy bursts: pace offers, respect the 4h limit window; thin items (limit < 100) cannot sustain a long run." + - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; realized ~791 casts/hr at the 4.3s midpoint cycle incl. restock + breaks; loop-only 837)." + - "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120s AFK) and independent of bank visits — never treat a restock trip as a break." + - "GE buy bursts: pace offers, respect the 4h limit window (10,000 javelin tips / 18,000 natures); thin items (limit < 100) cannot sustain a long run." - "Thin-item 'latest low' fills can be stale — confirm fill price with a live GE offer before committing a run; the liquid high-limit pair (rune javelin tips / rune arrow) is the dependable loss-minimizer." +# --- recovery block (owner guidance 2026-08-28: every activity must cover its complete runnable lifecycle) --- +# - Missing materials / depleted supplies: the restock_threshold transition (casting -> +# bank_supplies at candidate OR nature < 100) catches both mid-run depletions; restock +# action a8 rebuilds to carried_batch and the loop resumes (worked example steps 3-4). +# - Failed GE purchases (offer unfilled): re-place at live low + 5% with paced offers +# (a4/a5); after 3 consecutive failures, alert + stop (failed) unless a fallback +# candidate is switched in — never sit in an unfillable offer loop. +# - Staff of fire cannot be equipped (a3): fall back to buying 3 fire runes per cast +# (3 fire + 1 nature = 155 gp/cast rune cost vs 140; net -18/cast at the anchor), +# log WARN, continue; never loop on equip attempts. +# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout (a6); +# 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert). +# - Explicit completion/stop: stop_conditions above (Magic 55 HLA handoff, gp floor, +# repeated failures). + sources: - "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, 1 nature + 3 fire; spellbook Normal; infobox 2026-08-27)" - "OSRS wiki — High Level Alchemy (level 55, 65 xp/cast, 1 nature + 5 fire; 60% of item value; infobox 2026-08-27)" @@ -204,7 +336,9 @@ sources: - "OSRS wiki — Abyss (access to runic altars without talismans via the Mage of Zamorak)" - "OSRS wiki — Bryophyta's staff (level 30 Attack + Magic; up to 1,000 nature charges; 1/15 chance to not consume a charge on any nature-rune spell; alchemy combo with Tome of Fire)" - "OSRS wiki — Tome of Fire (members, level 50 Magic to wield; charged tome is an infinite fire-rune source and non-combat spells do not consume charges)" -verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing) + - "OSRS wiki — Staff of fire (page verified 2026-08-28 for the equip requirement: id 1387, F2P item, tradeable, options Wield — NO requirements row in the Infobox Item and NO requirement statement in the prose; contrast Battlestaff, whose prose states 'requires 30 Attack and 30 Magic to wield')" + - "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verb take_break — the cadence + break + recovery model" +verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27 related: - low-alch-f2p From 3572255a11f64edfb6ab4f1c7cfe4a45b7f17c30 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 03:19:13 -0500 Subject: [PATCH 10/15] =?UTF-8?q?codex:=20complete=20GE-flip=20lifecycle?= =?UTF-8?q?=20per=20owner=20addendum=20=E2=80=94=20partial=20fills,=20ge?= =?UTF-8?q?=5Fcancel=20recovery,=20ledger,=20break/resume,=20realized=20ra?= =?UTF-8?q?tes=20(bounce-fix=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner lifecycle addendum (2026-08-28) + governor bounce 4 (lifecycle contract): - state machine now covers the ENTIRE buy -> sell -> collect flow with no stranded state: partial fills tracked per slot (filled_qty ledger), 4h-lapse buys go to a new cancel_requote state (ge_cancel: units return to inventory, cash refunds, slot freed) instead of collect, cancelled units still sell at the high edge, lapse sells re-quote or lower rather than strand - action grammar + schema extended ONCE with the cancellation mutation ge_cancel (was missing; ge_clear/ge_offers_done are predicates only) and used explicitly in the new a8 cancel-requote action - concrete runtime state: minimum per-slot capital table (buy x 4h limit), members 8-slot vs F2P 3-slot paths, per-slot offer phase + quantities, buy-limit window, inventory/collection reconciliation in a9 - breaks connected to an explicit safe/resume path: resting offers persist server-side + per-slot ledger survive the pause; resume = re-open GE, re-check offers, continue current phase; never break mid-collect/mid-place - rates qualified against the full lifecycle: 149,500 gp/h is named the 4h buy-limit CEILING; realized range = ceiling x fill_factor (~0.5-0.9) x duty_cycle (~0.90-0.95) ~ 96,300 gp/h midpoint; gp_per_hour_low 74,750 - pinned worked example: capital 22.5M -> 8 slot quotes -> arrowtips partial fill (4,000/10,000 tracked) -> iron bar full fill sell (90,000 net) -> cosmic no-fill ge_cancel + requote -> arrowtips partial matures at lapse (cancel, sell 4,000, 68,000 net) -> first break after 6-10 rotations -> resume -> stop conditions - version 2 -> 3; evidence: validate 2 entries 0 errors 1 pre-existing WARN, selftest 4 PASS (fractional-tax fail, ge non-margin pass, floored pass, above-cap pass), synthetic merge main 79cab78 + head clean + gate green, git diff --check clean --- codex/economy/flip-candidates.yaml | 229 +++++++++++++++++++++++------ docs/action-grammar.md | 1 + schemas/entry.schema.json | 2 +- 3 files changed, 182 insertions(+), 50 deletions(-) diff --git a/codex/economy/flip-candidates.yaml b/codex/economy/flip-candidates.yaml index 22ab173..d65a375 100644 --- a/codex/economy/flip-candidates.yaml +++ b/codex/economy/flip-candidates.yaml @@ -3,17 +3,22 @@ title: GE Flipping (margin trading) domain: economy tags: [flipping, merchanting, ge, money-making, hybrid] status: verified -version: 2 -updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date +version: 3 # 3: bounce-fix 4 2026-08-28 (owner lifecycle addendum) — complete runnable flow: partial fills, ge_cancel + requote recovery, per-slot ledger, break/resume path, lifecycle-qualified rates, worked example pinned; ge_cancel added to the action grammar + schema +updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > Buy-and-sell Grand Exchange margin flipping: place resting buy offers at the low edge of an item's price band and resting sell offers at the high edge, harvesting the spread on high-volume items. There are no skill or quest requirements — capital breadth and slot discipline are the whole - game. Members accounts run 8 GE slots over the full candidate set; - F2P accounts get 3 slots and F2P-tradeable rows only (separate F2P path - in profit_drivers). + game. Members accounts run 8 GE slots over the full candidate set; F2P + accounts get 3 slots and F2P-tradeable rows only (separate F2P path in + profit_drivers). Lifecycle (owner addendum 2026-08-28): the entry models + the complete runnable flow — capital allocation, per-slot quotes with a + quoted/filled/sold/collected ledger, partial fills, ge_cancel + + requote recovery on window lapse or stale prices, collection and + reconciliation, breaks on resting offers, and explicit stop conditions — + with a pinned worked example and realized (not idealised) rates. accounts: membership: hybrid # members: 8 slots/full set; F2P: 3 slots/F2P rows (see economy) @@ -22,6 +27,7 @@ accounts: requires_other: - "Post-Tutorial Island account with Grand Exchange access (no skill or quest requirement)" - "Members account: 8 GE slots, full candidate set (the modeled path below). F2P account: 3 GE slots and F2P-tradeable rows only — run the F2P path in profit_drivers." + - "Minimum starting capital: per slot = buy edge x 4h limit (arrowtips 2.23M, iron bar 1.45M, cosmic 2.196M, silver ore 0.949M, swordfish 3.09M, lobster 1.59M, magic logs 9.264M, mud 1.728M at the 2026-08-27 snapshot) — full 8-slot members coverage ~22.5M; the 3-slot F2P path needs ~5.5M (iron 1.45M + cosmic 2.196M + silver 0.949M). Capital below one slot's requirement takes that slot off the table." best_practice: equipment: @@ -58,15 +64,22 @@ best_practice: members set — a hybrid account must swap to the 3-slot F2P path (Iron bar / Cosmic rune / Silver ore) instead of chasing members-only rows it cannot buy." source: "OSRS wiki — Grand Exchange (Members get eight Grand Exchange slots ... free players are limited to three. Additionally, free players may only purchase free-to-play items, and not members' items)" + - name: keep a per-slot offer ledger; cancel-and-requote on lapse, never strand units + why: "Every slot tracks quoted qty -> filled qty -> sold qty -> collected proceeds. + A partially-filled offer keeps resting with its acquired units attached; on the 4h + window lapse or a stale price the slot is ge_cancel'ed — acquired units return to + inventory, unspent cash refunds — then re-quoted at the fresh edge (buy) or exited + at the current high edge (sell). No items, coins, or GE slots are ever stranded." + source: "OSRS wiki — Grand Exchange (offer mechanics; 4h window); docs/action-grammar.md (ge_cancel, added 2026-08-28)" economy: # Model (documented, reproducible from the snapshot): # Members path (8 GE slots — the modeled economics below): # per slot: buy `limit` units at the low edge, sell at the high edge. - # tax_pu = floor(0.02 * sell) # 2% convenience fee, rounded DOWN to - # # whole coins per item; the 5M/item cap - # # and the sub-50gp rounding exemption - # # are not hit at these prices + # tax_pu = min(floor(0.02 * sell), 5_000_000) # 2% convenience fee, + # # rounded DOWN to whole coins per item, + # # capped 5M/item (owner decision + # # 2026-08-28; cap never hit here) # net_pu = sell - tax_pu - buy # integer per-item net (floored tax) # 4h/slot = net_pu * limit # buy-limit window caps per-4h volume # F2P path (3 GE slots, F2P-tradeable rows only): same arithmetic, top-3 @@ -74,13 +87,25 @@ economy: # Snapshots 2026-08-27: prices.runescape.wiki mapping + latest; limits and # members flags cross-checked against the wiki item pages (all agree). Top 8 # by 4h/slot below in profit_drivers. + # + # Lifecycle qualification (owner addendum 2026-08-28): the 4h figures are + # an economic CEILING — ideal full fills of every slot with zero overhead. + # The realized operating range is: + # realized gp/h = ceiling x fill_factor x duty_cycle, where + # fill_factor ~0.5-0.9 (partial fills, cancels, requotes reduce the + # filled volume) and duty_cycle ~0.90-0.95 (scan + place + collect + # ~3 min per ~40-min rotation; breaks every 6-10 rotations). + # Example midpoint: 149,500 x 0.70 x 0.92 ≈ 96,300 gp/h. + # gp_per_hour_low (74,750) = 50% of the ceiling — a conservative realized + # lower bound; gp_per_hour_high (149,500) = the named ceiling, never + # presented as the achieved runtime rate. item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members) item_id: 44 cost_per_cycle: 223 # buy @ low edge per unit (snapshot 2026-08-27) income_per_cycle: 244 # sell @ high edge per unit, gross (snapshot 2026-08-27) - net_per_cycle: 17 # 244 - floor(244*0.02) - 223 = 244 - 4 - 223 (floored tax) - gp_per_hour_low: 74750 # 50% of the top-8 modeled upper bound — partial fills/drift - gp_per_hour_high: 149500 # top-8 modeled upper bound: sum(4h/slot) / 4 (8 slots, full fills) + net_per_cycle: 17 # 244 - min(floor(244*0.02), 5M) - 223 = 244 - 4 - 223 (floored, capped tax) + gp_per_hour_low: 74750 # realized lower bound = 50% of the ceiling: partial fills, cancels/requotes, collection trips, breaks + gp_per_hour_high: 149500 # 4h buy-limit CEILING: sum(4h/slot) / 4 (8 members slots, ideal full fills, zero overhead) — qualified above xp_per_hour: 0 # flipping awards no XP supplies: - item: Rune arrowtips # canonical wiki names per action-grammar conventions @@ -98,70 +123,95 @@ economy: profit_drivers: - "Spread per unit x trade volume; volume is the safety factor (high-volume items fill faster and drift less)" - "Slot breadth: cover as many net-positive candidates as capital allows (members 8 slots / F2P 3 slots)" - - "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h on 8 slots" - - "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h (alternates: Raw swordfish 45,000, Raw lobster 30,000)" + - "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored+capped 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h CEILING on 8 slots" + - "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h ceiling (alternates: Raw swordfish 45,000, Raw lobster 30,000)" - "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges for members; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells" + - "Realized vs ceiling (work the full lifecycle): realized gp/h = ceiling x fill_factor (~0.5-0.9, partials/cancels/requotes) x duty_cycle (~0.90-0.95, scan/place/collect + breaks) — e.g. 149,500 x 0.70 x 0.92 ~ 96,300 gp/h midpoint; do not quote the ceiling as an achieved rate" state_machine: initial: idle terminal: [complete, failed] states: idle: - goal: be logged in at a bank with a GE booth, capital >= floor - next: [scan, complete, failed] + goal: "be logged in at a bank with a GE booth, capital >= floor, per-slot offer ledger empty (quoted = filled = sold = collected = 0 for every slot)" + next: [scan, complete] scan: - goal: read the current high/low band for every candidate + goal: "read the current high/low band for every candidate; compute net-positive edges with floored+capped tax" next: [buy, idle] buy: - goal: place one resting buy offer per slot at or below the low edge - next: [wait_buy_fill, collect, idle] + goal: "place one resting buy offer per free slot at or below the low edge; record quoted qty and remaining 4h limit per slot in the ledger" + next: [wait_buy_fill, idle] wait_buy_fill: - goal: hold BUY offers; re-quote any approaching the 4h window lapse - next: [sell, buy, collect] + goal: "hold BUY offers; track filled_qty per slot (partial fills included, units stay attached to the resting offer); on the 4h window lapse or a stale price, cancel the unfilled balance" + next: [sell, cancel_requote] sell: - goal: place one sell offer per filled slot at or above the high edge - next: [wait_sell_fill, collect] + goal: "place one sell offer per slot holding units (its filled_qty) at or above the high edge" + next: [wait_sell_fill, cancel_requote] wait_sell_fill: - goal: hold SELL offers; re-quote any approaching the 4h window lapse - next: [collect, sell, idle] + goal: "hold SELL offers; track sold qty per slot; on lapse or a stale price, cancel and re-quote — units are never left stranded" + next: [collect, cancel_requote] + cancel_requote: + goal: "ge_cancel the timed-out/stale offers of the current phase: acquired units return to inventory, unspent cash refunds, slot freed; re-quote at a fresh edge (buy when margin is still positive; sell to exit at the current high edge) or free the slot" + next: [wait_buy_fill, sell, wait_sell_fill] collect: - goal: collect proceeds, deposit coins, log the cycle + goal: "collect proceeds, deposit coins, reconcile (inventory empty of flip items, all GE slots free, each slot's ledger closed with a logged RESULT)" next: [scan, complete, idle] complete: - goal: session GP target reached + goal: "session GP target reached" failed: - goal: hard failure — stop, log ERROR sentinel, alert + goal: "hard failure — stop, log ERROR sentinel, alert" transitions: - from: idle - when: "capital below floor OR session target reached" + when: "GE access available AND capital >= floor AND session target not reached" + to: scan + - from: idle + when: "capital below floor OR session target already reached" to: complete - from: scan - when: "no candidate net-positive at band edges on 3 consecutive scans" + when: "at least one net-positive candidate at the scanned band edges (floored+capped tax)" + to: buy + - from: scan + when: "no net-positive candidate on 3 consecutive scans (prices stale — stand down)" to: idle - from: buy - when: "all slots quoted" + when: "all free slots quoted (quoted qty recorded per slot)" to: wait_buy_fill + - from: buy + when: "capital exhausted before quoting (insufficient funds)" + to: idle - from: wait_buy_fill - when: "all pending buys filled" + when: "a slot's buy offer is FULLY filled (filled_qty == quoted qty) — that slot's units move to sell" to: sell - from: wait_buy_fill - when: "4h window lapse with unfilled BUY offers" - to: collect + when: "a slot's buy offer hits the 4h window lapse or a stale price with filled_qty < quoted qty — ge_cancel: acquired units return to inventory, unspent cash refunds, slot freed" + to: cancel_requote + - from: cancel_requote + when: "re-quoted buy offers placed at the fresh low edge (window renewed, margin still positive)" + to: wait_buy_fill + - from: cancel_requote + when: "buy-side margin is gone — free the slot on the buy side; any acquired units from the cancelled offer still sell at the current high edge (exit at cost or a small loss rather than strand inventory)" + to: sell + - from: cancel_requote + when: "re-quoted sell offers placed at the fresh high edge (or lowered to the current band) for units in hand" + to: wait_sell_fill - from: sell - when: "sells placed" + when: "sell offers placed for all units in hand (filled_qty per slot)" to: wait_sell_fill - from: wait_sell_fill - when: "all pending sells filled" + when: "all pending sells filled — per-slot proceeds ready to collect" to: collect - from: wait_sell_fill - when: "4h window lapse with unfilled SELL offers" - to: sell + when: "a sell offer hits the 4h window lapse or a stale price — ge_cancel and re-quote at the fresh high edge or lower to the current band; units are never stranded" + to: cancel_requote - from: collect when: "session GP target reached" to: complete - from: collect - when: "rotation done and window open" + when: "rotation done and window open — start the next rotation" to: scan + - from: collect + when: "capital fell below floor after reconciliation" + to: idle actions: - id: a1 @@ -192,6 +242,7 @@ actions: params: items: "top-N candidates from the entry table" band: "high and low edge per item" + margin: "net-positive = sell - min(floor(0.02 x sell), 5,000,000) - buy > 0" on_fail: skip anti_ban: delay: [1200, 2800] @@ -202,9 +253,10 @@ actions: target: grand-exchange params: item: "per candidate slot" - qty: "min(remaining 4h limit, slot capacity)" + qty: "min(remaining 4h limit, slot capacity, capital available)" price: "low edge (rounded down)" max_price: "low edge — never instant" + ledger: "record quoted qty and remaining 4h limit per slot" on_fail: skip anti_ban: delay: [900, 2700] @@ -214,39 +266,71 @@ actions: verb: sleep params: delay: [6000, 12000] - until: "ge_offers_done changes or 4h window lapse" + until: "ge_offers_done changes or a fill event or 4h window lapse" on_fail: retry anti_ban: delay: [300, 900] - id: a6 + step: check-fills + verb: ge_offers_done + target: "all quoted slots" + params: + phase: "buy (no pending sells) | sell (no pending buys)" + per_slot: "filled_qty vs quoted_qty for every slot (partial fills included)" + verdict: "full | partial | none | 4h-lapse" + on_fail: retry + + - id: a7 step: place-sells verb: ge_sell target: grand-exchange params: - item: "per filled slot" - qty: "filled quantity" + item: "per slot holding units (filled_qty)" + qty: "filled_qty of that slot" price: "high edge (rounded up)" + ledger: "record sold qty per slot" on_fail: skip anti_ban: delay: [900, 2700] - - id: a7 + - id: a8 + step: cancel-requote + verb: ge_cancel + target: "timed-out or stale slot offers (current phase)" + params: + offers: "the slots that hit the 4h window lapse or a stale price" + cancelled: "acquired units return to inventory; unspent cash refunds; slot freed" + requote: "buy: re-place at the fresh low edge when the margin is still positive; sell: re-place at the fresh high edge or lower to the current band; margin gone on the buy side: free the slot and sell any acquired units to exit" + on_fail: retry + + - id: a9 step: collect-and-log verb: ge_collect target: grand-exchange params: deposit: "coins to bank" - log: "RESULT item, qty, net gp per slot" + reconcile: "inventory empty of flip items, all GE slots free, per-slot ledger closed" + log: "RESULT item, qty, net gp per slot (floored+capped tax)" on_fail: alert anti_ban: delay: [600, 1400] + - id: a10 + step: take-break + verb: take_break + target: "current location" + params: + duration: "[60, 240] seconds of idle AFK per break (matches run_control.break.for)" + safe: "resting offers persist server-side at their quoted edges — the full state lives in the GE interface plus the per-slot ledger, so a break at any resting-offer point is safe" + resume: "re-open GE (a2), re-check offer state (a6), continue from the current phase (a4/a7/a8/a9) — never break mid-collect (coins in hand) or mid-place (offer half-configured)" + on_fail: skip + run_control: - iteration: "one full buy -> buy-fill -> sell -> sell-fill -> collect rotation across all slots" + iteration: "one full rotation: quote buys -> fills/cancels/requotes -> sell filled units -> sells fill/cancel/requote -> collect + reconcile, across all slots" max_iterations: null break: - every: [6, 10] # rotations + every: [6, 10] # rotations (resting offers persist; state = GE + per-slot ledger) for: [60, 240] # seconds of idle AFK stop_conditions: - "session GP target reached" @@ -254,12 +338,58 @@ run_control: - "no net-positive candidates on 3 consecutive scans" - "3 consecutive aborted offers (client issue — do not loop)" + # ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28) ================= + # Members account, 8 GE slots, capital 22,500,000 gp. Snapshot prices 2026-08-27 + # (buy/sell = low/high edge; tax = min(floor(0.02 x sell), 5,000,000)). + # + # Step 0 — preparation and capital allocation: capital at the bank, GE-adjacent. + # Scan bands (a3): only net-positive slots are quoted. Per-slot capital needs: + # arrowtips 223x10,000 = 2.23M; iron bar 145x10,000 = 1.45M; cosmic 122x18,000 = + # 2.196M; silver ore 73x13,000 = 0.949M; swordfish 206x15,000 = 3.09M; lobster + # 106x15,000 = 1.59M; magic logs 772x12,000 = 9.264M; mud 96x18,000 = 1.728M + # (total 22.497M <= 22.5M). Ledger: quoted=filled=sold=collected=0 per slot. + # Step 1 — place buys (a4): quote all 8 slots at the low edges (qty = 4h limit). + # wait_buy_fill (a5/a6). + # Step 2 — PARTIAL FILL: arrowtips fills 4,000 of 10,000 after ~18 min -> check-fills + # (a6) verdict PARTIAL: ledger arrowtips.filled = 4,000. The offer KEEPS resting + # (acquired units stay attached to the resting offer; nothing is stranded). + # Step 3 — FULL FILL: iron bar fills all 10,000 -> transition to sell (a7): place + # sell 10,000 @ 157 (high edge). wait_sell_fill -> fills -> collect (a9): + # proceeds 1,570,000; tax floor(157 x 0.02) = 3/item -> net = (157-3-145) x 10,000 + # = 90,000. Reconcile: inventory empty, slot free, ledger closed. + # Step 4 — NO-FILL / STALE recovery on the buy side: cosmic 18,000 @ 122 is unfilled + # approaching the 4h lapse -> cancel_requote (a8, ge_cancel): 0 units return, + # 2,196,000 cash refunds, slot freed; re-scan (a3): cosmic still net-positive -> + # requote 18,000 @ 122 on the fresh window. (If the buy margin had vanished the + # slot is freed on the buy side; any units acquired from a cancelled offer still + # sell at the current high edge.) + # Step 5 — PARTIAL matures at lapse: arrowtips still 4,000/10,000 -> ge_cancel: + # 4,000 units return to inventory, 6,000 x 223 = 1,338,000 refunded -> sell (a7): + # 4,000 @ 244 -> tax floor(244 x 0.02) = 4/item -> net = (244-4-223) x 4,000 = + # 68,000. Collect (a9) + reconcile. + # Step 6 — FIRST SCHEDULED BREAK after 6-10 rotations (midpoint 8): execution is at + # a safe point — every slot is either collected or resting at its quoted edge; the + # state (offers + per-slot ledger) persists server-side. take-break 60-240 s + # (a10); RESUME at a2 (re-open GE) -> a6 (check-fills) -> continue the current + # phase (a4 buys / a7 sells / a8 cancel-requote / a9 collect). Breaks never strand + # an offer and never interrupt a collect/place in progress. + # Step 7 — STOP: session target (e.g. 900,000 net) -> collect -> complete; capital + # fell below floor -> idle -> complete; 3 consecutive aborted offers -> failed. + # Rate qualification: the 149,500 gp/h figure is the 4h buy-limit CEILING (8 slots, + # full fills, zero overhead). Realized midpoint ~ 149,500 x 0.70 (fill factor) x + # 0.92 (duty cycle incl. scan/place/collect + breaks) ~ 96,300 gp/h — the entry's + # low bound 74,750 (50% ceiling) sits under that; the ceiling is never presented + # as an achieved runtime rate. + # ===================================================================================================== + risk: ban_level: low notes: - "Flipping is standard gameplay; the detectable signal is quote churn — keep per-offer pacing humanised, never instant-buy/sell loops." - "The 2% fee (since 2025-05-29) and 5M/item cap make sub-2%-spread staples unprofitable at band edges; re-scan before every rotation." - - "Thin/volatile items: a resting offer can sit for hours — cap quote lifetime to the 4h window and re-quote rather than chase." + - "Thin/volatile items: a resting offer can sit for hours — cap quote lifetime to the 4h window and re-quote (ge_cancel + fresh edge) rather than chase." + - "Partial fills are normal; never cancel-and-replace a partially-filled offer mid-window just to churn — the ledger keeps the filled qty attached to the resting offer until full/lapse." + - "Breaks are safe only at resting-offer or post-collect points (see a10); state = GE offers + the per-slot ledger, which survive the pause." sources: - "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h; connected limits; verified 2026-08-27)" @@ -267,7 +397,8 @@ sources: - "OSRS wiki — Merchanting (flipping = spread exploitation; high trade volume = safety; bulk flipping needs ~20M+ cash; avoid static-price items; offer matching at or better than your price)" - "prices.runescape.wiki/api/v1/osrs/mapping (item ids, members flags, limits — limits cross-checked against Module:Exchange/, all agree)" - "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)" -verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing) + - "docs/action-grammar.md (ge_cancel verb added 2026-08-28: cancel a pending offer — bought units return to inventory, unspent cash refunds) + schemas/entry.schema.json verb enum; owner lifecycle addendum 2026-08-28 (complete runnable flow, worked example, realized rates)" +verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27 related: - low-alch-f2p diff --git a/docs/action-grammar.md b/docs/action-grammar.md index 234e520..2d29fa9 100644 --- a/docs/action-grammar.md +++ b/docs/action-grammar.md @@ -68,6 +68,7 @@ Legend for the mapping table: | `ge_open` | open GE at a bank | — | — | — | | `ge_buy` | buy `qty` of `item` at offer price, paced | — | — | — | | `ge_sell` | sell `qty` of `item`, confirm | — | — | — | +| `ge_cancel` | cancel a pending offer: bought units return to inventory, unspent cash refunds | — | — | — | | `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — | | `ge_collect` | collect finished GE transactions | — | — | — | | `ge_check_price` | read current buy/sell price of an item | — | — | — | diff --git a/schemas/entry.schema.json b/schemas/entry.schema.json index c50ce9b..69df417 100644 --- a/schemas/entry.schema.json +++ b/schemas/entry.schema.json @@ -155,7 +155,7 @@ "click_button", "menu_option", "bank_inventory", "deposit_all", "withdraw", "has_item", "count_item", "move_to_inv", "sort_inventory", - "ge_open", "ge_buy", "ge_sell", "ge_offers_done", "ge_collect", + "ge_open", "ge_buy", "ge_sell", "ge_cancel", "ge_offers_done", "ge_collect", "ge_check_price", "attack", "prayer", "buff", "eat", "retreat", "is_dead", "respawn", "is_at", "item_count", "level_is", "xp_is", "hp_is", From a197b34aa8067f590de3622a50cdb50e2b59f0da Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 03:36:45 -0500 Subject: [PATCH 11/15] =?UTF-8?q?codex:=20low-alch-members=20=E2=80=94=20c?= =?UTF-8?q?omplete=20GE=20lifecycle,=20capital=20model,=20monotonic=20coun?= =?UTF-8?q?ters,=20fail-closed=20staff=20(bounce-fix=20attempt=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner guidance 10325 (governor 10297 findings, exactly): - GE acquisition path for initial buy AND every restock: open -> place -> wait/fill -> collect (ge_open/ge_buy/ge_collect; new wait_offers state), partial-fill collect + re-place recovery, bank-stock fast path only when the bank actually holds stock — never withdraw an unbought batch. - Capital model: staff cap 5,000 + first batch at max fill 570,150 + 6-batch worst-case loss reserve 162,990 + float 570,150 (~1.31M); capital stops use TOTAL ASSETS (cash + supplies), never raw cash. - Monotonic counters: breaks at 550/1,100/1,650 independent of restocks at 901/1,802 (gcd 550,901=1); validator regression rejects the old 1,452 restart bug and enforces the pins. - Rates: loop-only 837.2 labelled ceiling; steady-state 639-1020 (mid 791.2); session-realized 586-977 (mid 741.8) over the explicit 2h session horizon -> xp 23,000, gp -2,930..-1,760. - Staff equip FAIL-CLOSED after bounded retries; fire-rune fallback prose removed everywhere. Preserves one-cast iteration, cast-count breaks, rune javelin tips anchor, pacing/equipment fixes. version 4 -> 5. --- codex/economy/low-alch-members.yaml | 342 +++++++++++++++++++--------- tools/validate.py | 35 +++ 2 files changed, 266 insertions(+), 111 deletions(-) diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml index 002074f..cc3b7c7 100644 --- a/codex/economy/low-alch-members.yaml +++ b/codex/economy/low-alch-members.yaml @@ -3,7 +3,7 @@ title: Low-Level Alchemy (Members) domain: economy tags: [alching, magic-training, members, loss-training] status: verified -version: 4 # 4: bounce-fix 3 2026-08-28 (owner guidance) — iteration = one cast, break.every in CAST counts (independent of bank visits), numeric carried_batch/restock_threshold, realized rates incl. setup+restock+breaks, full-lifecycle actions, pinned worked example +version: 5 # 5: bounce-fix 4 2026-08-28 (owner guidance 10325) — complete GE acquisition path (open -> place -> fill -> collect) for initial buy AND every restock (bank-stock fast path only when the bank holds stock); fail-closed staff equip (bounded retries, no fire-rune fallback); capital model (cash vs supplies, staff cost, max-fill worst case, 6-batch loss reserve, float); monotonic break/restock counters (breaks 550/1,100/1,650 independent of restocks 901/1,802) + validator regression; startup-vs-steady-state rates over a 2 h session horizon updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > @@ -13,28 +13,35 @@ summary: > (mapping lowalch != null, members=true, live GE prices 2026-08-27). At current prices the method burns a small amount of GP per cast for 31 XP — run it for XP, not GP. Least-loss liquid anchor at last check: rune - javelin tips (-3/cast). Model pin (owner guidance 2026-08-28): one + javelin tips (-3/cast). Model pins (owner guidance 2026-08-28): one iteration = one cast; breaks every 400-700 COMPLETED CASTS (cast-count - based, independent of bank visits); restock when candidate or nature - drops below the numeric restock_threshold (100) against a carried_batch - of 1,000/1,000 — pinned worked example below. Handoff: switch to High - Level Alchemy at level 55 (hard level gate + strictly more coins AND XP - per cast; see sources). + based, independent of bank visits — midpoint example: 550, 1,100, 1,650, + ...); restock when candidate or nature drops below the numeric + restock_threshold (100) against a carried_batch of 1,000/1,000 (restock + cadence 901 casts, monotonic and independent of breaks). EVERY purchase + follows the complete GE path open -> place -> fill/wait -> collect, for + the initial acquisition and for every restock (a bank-stock fast path + applies only when the bank actually holds enough stock). Staff-of-fire + equip failure is FAIL-CLOSED (bounded retries, then log ERROR + alert + + stop — no fire-rune fallback is modeled). Session horizon 2 h; rates are + session-realized (startup + restock + breaks included, see economy). + Handoff: switch to High Level Alchemy at level 55 (hard level gate + + strictly more coins AND XP per cast; see sources). accounts: membership: members min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified) requires_quest: [] requires_other: - - "Staff of Fire equipped (removes 3 fire runes from the cost; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources — this is the default path)" - - "A bank with GE access within reach (alch spot = GE bank booth; restock trip 60-120s)" - - "Working capital >= 543,000 gp for one carried_batch (1,000 x 403 gp javelin tips + 1,000 x 140 gp natures, live 2026-08-27)" + - "Staff of Fire equipped (removes 3 fire runes from the cost; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources — this is the default path). Equip failure is FAIL-CLOSED: flow performs bounded retries (3), then logs ERROR + alerts + stops — there is no fire-rune fallback in this model" + - "A bank with GE access within reach (alch spot = GE bank booth; restock trip [60, 120] s)" + - "Starting capital >= ~1.31M gp CASH (model breakdown: staff buy hard cap 5,000 + first batch at MAX allowed fill 570,150 + session loss reserve 162,990 = 6 worst-case batches at live-low+5% fills (covers Magic 21 -> 55) + resting-offer float 570,150). The capital stop check uses TOTAL ASSETS (cash + supplies at current buy cost), never raw cash — cash legitimately converts into supplies mid-run" - "30 Attack + 50 Magic ONLY if adopting the optional Bryophyta's staff + Tome of Fire technique — below that the Staff of Fire path applies" best_practice: equipment: - item: staff of fire - why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved); equip requirement: none documented on the OSRS wiki (verified 2026-08-28)" + why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved); equip requirement: none documented on the OSRS wiki (verified 2026-08-28); if it cannot be equipped after bounded retries the run FAILS CLOSED (no fallback modeled)" source: "OSRS wiki — Staff of Fire (id 1387, F2P item, tradeable, no requirements row in the infobox and none in the prose — compare Battlestaff which states 'requires 30 Attack and 30 Magic'); Low Level Alchemy (Cost)" techniques: - name: cast on noted (stacked) items @@ -52,30 +59,42 @@ economy: # OSRS wiki (spell infoboxes, Module:Exchange limits, item pages). # Re-verified at bounce fix 2026-08-27 with a fresh live fetch; the # modeled anchor is the ACTUAL least-loss liquid candidate. - # Bounce-fix 3 (2026-08-28, owner guidance): all rate fields are REALIZED - # figures — setup/restock/breaks included, not just the cast loop. + # Bounce-fix 4 (2026-08-28, owner guidance 10325): all rate fields are + # SESSION-REALIZED figures — startup + restock + mandatory breaks + # included; loop-only and steady-state figures retained as labelled + # references; the 2 h session horizon is explicit (stop_conditions). item: Rune javelin tips # least-loss liquid anchor 2026-08-27 (see profit_drivers) item_id: 19580 - cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire) + cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire, live low 2026-08-27) income_per_cycle: 540 # lowalch of rune javelin tips (40% of value 1350) net_per_cycle: -3 - gp_per_hour_low: -2960 # realized (incl. restock+breaks), fastest cycle 3.4s -> 986.4 casts/hr x -3 = -2,959 - gp_per_hour_high: -1980 # realized (incl. restock+breaks), slowest cycle 5.2s -> 660.6 casts/hr x -3 = -1,982 - xp_per_hour: 24500 # realized midpoint 791.3 casts/hr x 31 xp = 24,530 (pin 24,500) - # ---- realized-rate derivation (pinned; owner guidance 2026-08-28) ---- - # LOOP: cycle = 1.8s cast (3 ticks x 0.6s) + anti_ban.delay [1.6, 3.4]s ADDITIVE - # (docs/action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s -> loop-only - # casts/hr = 3600/4.3 = 837.2; loop-only xp = 25,953 (837.2 x 31); - # loop-only gp midpoint = -2,512 (837.2 x -3). - # OVERHEAD per 901-cast batch (carried_batch 1000 - restock_threshold 100): - # restock trip [60, 120]s (midpoint 90s, GE-adjacent booth); - # breaks: every [400, 700] casts (midpoint 550) for [45, 120]s (midpoint - # 82.5s) -> 901/550 = 1.64 breaks x 82.5s = 135s. - # Total overhead = 90 + 135 = 225s per 901 casts. - # REALIZED casts/hr = 901 / ((901 x cycle_s + 225) / 3600): - # fast 3.4s -> 986.4; midpoint 4.3s -> 791.3; slow 5.2s -> 660.6. - # xp_per_hour = 791.3 x 31 = 24,530 (pin 24,500; range 20,480..30,580). - # gp_per_hour = realized casts/hr x -3: fast -2,959 (low), slow -1,982 (high), midpoint -2,374. + gp_per_hour_low: -2930 # session-realized fast bound: 977.2 casts/hr x -3 = -2,932 (3.4 s cycle, 300 s startup, 2 h horizon) + gp_per_hour_high: -1760 # session-realized slow bound: 585.9 casts/hr x -3 = -1,758 (5.2 s cycle, 600 s startup, 2 h horizon) + xp_per_hour: 23000 # session-realized midpoint: 741.8 casts/hr x 31 = 22,995 (pin 23,000) + # ---- rate model (pinned; owner guidance 2026-08-28) ---- + # LOOP (idealised, zero overhead — ceiling reference ONLY, never the + # entry's output): cycle = 1.8 s cast (3 ticks x 0.6 s) + + # anti_ban.delay [1.6, 3.4] s ADDITIVE (docs/action-grammar.md:135) + # = 3.4-5.2 s, midpoint 4.3 s -> loop-only casts/hr = 3600/4.3 = 837.2 + # (range 692.3..1,058.8); loop-only xp mid = 25,953; gp mid = -2,512. + # STEADY-STATE (mandatory breaks + restock trips, no startup): per + # 901-cast batch: 901 x cycle + restock trip [60, 120] s (mid 90) + + # break share (break.for mid 82.5 s per break.every mid 550 casts: + # 45 s/700 at the fast end, 120 s/400 at the slow end): + # fast 3.4 s: 901 x 3.4 + 60 + 45 x 901/700 = 3,181.3 s -> 1,019.7 casts/hr + # mid 4.3 s: 901 x 4.3 + 90 + 82.5 x 901/550 = 4,099.5 s -> 791.3 casts/hr + # slow 5.2 s: 901 x 5.2 + 120 + 120 x 901/400 = 5,075.5 s -> 639.2 casts/hr + # SESSION-REALIZED (2 h horizon = 7,200 s; one-time startup 300-600 s, + # mid 450 s -> 450/7200 = 6.25 %): realized = steady-state x (1 - + # startup/session): fast 977.2, mid 741.8, slow 585.9 casts/hr. + # xp mid = 741.8 x 31 = 22,995 -> pin 23,000; gp = casts/hr x -3: + # low -2,932 (fast), high -1,758 (slow), mid -2,225. + # BATCH ECONOMICS (901 casts; deposits 901 x 540 = 486,540 gp per batch): + # input cost at live low: 901 x 543 = 489,243 -> net -2,703/batch + # input cost at MAX fill (live low + 5 %): 901 x 570.15 = 513,705 -> net + # -27,165/batch (worst case). Reserve = 6 worst-case batches (Magic + # 21 -> 55 = ~5,300 casts = ~6 batches) = 162,990 gp; the capital stop + # checks TOTAL ASSETS, not raw cash (see accounts + stop_conditions). supplies: - item: rune javelin tips qty_per_cycle: 1 @@ -101,37 +120,58 @@ economy: - "9. Adamant spear(p) (id 1259, lowalch 832, buy 450, margin +382, GE limit 125)" - "10. Mystic mist staff (id 20733, lowalch 17000, buy 75727, margin -58727, GE limit 8) — buy spiked ABOVE alch value 2026-08-27; a true loss trap, do not buy" - "Caveat: thin items (limits 5-125) cannot sustain a run and their 'low' fills can be stale — a bot cannot reliably buy at those prices. The dependable loss-minimizers are the liquid high-limit pair modeled above: rune javelin tips (-3/cast) and rune arrow (-25/cast)." - - "Batch-scale economics (default staff-of-fire path, worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = -2,703 gp, +27,931 xp, 2 x 540 gp coin deposits." + - "Batch-scale economics (default staff-of-fire path, worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = -2,703 gp at live-low fills (-27,165 at MAX allowed fills), +27,931 xp, deposits 486,540 gp; unfilled-offer float (up to 570,150 gp) is separate from loss (see accounts)." state_machine: initial: idle terminal: [complete, failed] states: idle: - goal: "acquire + equip the staff of fire, GE-buy candidate + natures, build the starting inventory to carried_batch (1,000 candidate + 1,000 nature)" - next: [casting, complete, failed] + goal: "session start at the GE bank booth. Open GE (a2), then place the initial offers: staff of fire (a3), candidate 1,000 (a4), nature runes 1,000 (a5) at live low + 5% (staff: +25% up to the 5,000 cap). Record initial capital in the cash ledger (total assets = cash + supplies at current buy cost). Nothing is bought or collected without an open GE interface" + next: [wait_offers, complete, failed] + wait_offers: + goal: "wait for the pending offers (ge_offers_done); when filled, collect every purchase + refund from the collection box (a6 — never leave purchases in the box). PARTIAL FILLS: collect the filled portion, re-place the remainder at the current edge (a3/a4/a5); 3 consecutive no-fill rounds (offer cannot fill at the accepted edge) -> failed. An offer that is still unfilled at session end is left resting by design — its coins are tied up (float reserve) and return when it fills or is aborted by the operator (the single operator-touch point)" + next: [ready, wait_offers, failed] + ready: + goal: "equip the staff of fire (a7 — bounded retries, then FAIL CLOSED) and build the loadout to carried_batch 1,000 candidate + 1,000 nature (a8; purchases from a6 are already in the inventory; a8 deposits overflow and confirms the counts)" + next: [casting, failed] casting: - goal: "cast Low Level Alchemy on the current candidate item — each cast consumes 1 candidate + 1 nature and pays 540 gp (lowalch), +31 xp, net -3 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts)" + goal: "cast Low Level Alchemy on the candidate (a9) — each cast consumes 1 candidate + 1 nature and pays 540 gp (lowalch), +31 xp, net -3 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts, via a13); the counter is NEVER reset by restocking" next: [bank_supplies, complete, failed] bank_supplies: - goal: "deposit coins + products, withdraw candidate + natures back to carried_batch (1,000/1,000), resume casting" - next: [casting, failed] + goal: "restock to carried_batch: deposit coins + products and withdraw ALL candidate/natures the bank holds (a10 — bank-stock fast path when the bank already holds enough); for the remaining deficit, open GE (a2), buy the deficit (a11, live low + 5%), collect it (a12) — never withdraw a batch that was never bought. Restocking never resets the cast counter or the break counter; the break cadence (550/1,100/1,650, ...) is independent of the restock cadence (901/1,802, ...)" + next: [casting, bank_supplies, failed] complete: - goal: "reached the run's XP/level target (default 55 -> hand off to HLA)" + goal: "reached the run's XP/level target (default 55 -> hand off to HLA) or the 2 h session horizon" failed: goal: "hard failure — stop, log ERROR sentinel, alert" transitions: - from: idle - when: "staff equipped AND candidate == carried_batch AND nature == carried_batch (1,000 and 1,000)" - to: casting + when: "staff + candidate + nature offers all placed (a3/a4/a5)" + to: wait_offers - from: idle - when: "magic level >= target (55, hand off to HLA)" + when: "magic level >= target (55) at session start" to: complete - from: idle - when: "3 consecutive purchase/equip failures with no fallback available (recovery block below)" + when: "3 consecutive offer-place failures at session start (no fallback candidate)" + to: failed + - from: wait_offers + when: "ge_offers_done AND all purchases collected (a6)" + to: ready + - from: wait_offers + when: "partial fill: filled portion collected (a6), remainder re-placed at the current edge (a3/a4/a5) — still waiting" + to: wait_offers + - from: wait_offers + when: "3 consecutive no-fill rounds (offer cannot fill at the accepted edge)" + to: failed + - from: ready + when: "staff equipped (a7) AND loadout at carried_batch 1,000/1,000 (a8)" + to: casting + - from: ready + when: "staff equip failed after bounded retries — FAIL CLOSED" to: failed - from: casting - when: "candidate count < restock_threshold (100) OR nature rune count < restock_threshold (100) — deterministic restock trigger, pinned numbers" + when: "candidate count < restock_threshold (100) OR nature count < restock_threshold (100) — deterministic restock trigger, pinned numbers" to: bank_supplies - from: casting when: "magic level >= target (55, hand off to HLA)" @@ -140,8 +180,14 @@ state_machine: when: "3 consecutive failed casts (client issue — do not loop)" to: failed - from: bank_supplies - when: "restocked to carried_batch (1,000 candidate + 1,000 nature)" + when: "restock complete: candidate == carried_batch (1,000) AND nature == carried_batch (1,000) — bank fast path or re-bought deficit collected" to: casting + - from: bank_supplies + when: "restock partial: deficit re-bought but offers still filling (ge_offers_done false) — loop the restock phase" + to: bank_supplies + - from: bank_supplies + when: "3 consecutive no-fill restock rounds OR a capital stop fires (assets / loss-reserve checks in stop_conditions)" + to: failed # --- handoff note (wiki-verified 2026-08-27, do not copy from other entries) --- # Low Level Alchemy: level 21, 31 xp/cast, 1 nature + 3 fire (spellbook: Normal). @@ -171,27 +217,27 @@ actions: delay: [400, 1600] - id: a2 + step: open-ge + verb: ge_open + target: grand-exchange + params: + note: "open the GE interface at the booth. The entry NEVER buys (a3/a4/a5/a11) or collects (a6/a12) without an open GE interface — the action grammar defines ge_open, ge_buy, ge_collect as separate verbs, so the open step is explicit" + on_fail: retry + anti_ban: + delay: [600, 1400] + + - id: a3 step: buy-staff verb: ge_buy target: grand-exchange params: item: "staff of fire" qty: 1 - max_price: "live low + 25% (wiki value 1,500; hard cap 5,000)" - on_fail: retry # retry once; on a second failure use the fire-rune fallback (see a3 note / recovery block) - anti_ban: - delay: [1200, 3000] - - - id: a3 - step: equip-staff - verb: menu_option - target: "staff of fire in inventory" - params: - option: "Wield" - note: "Equip requirement is NONE documented on the OSRS wiki Staff of fire page (no requirements row in the infobox, none in the prose; compare Battlestaff which states 'requires 30 Attack and 30 Magic'; verified 2026-08-28). FAILURE PATH — if Wield is unavailable or the staff cannot be equipped, fall back to buying 3 fire runes per cast (3 fire + 1 nature = 155 gp rune cost vs 140, net -18/cast at the anchor), log WARN and continue; never loop on equip attempts." + max_price: "live low + 25 % (wiki value 1,500; hard cap 5,000)" + note: "offer rests until filled or the run ends; on a partial/no fill, collect what filled (a6) and re-place the remainder at the current edge — bounded retries, then the run fails closed (no fire-rune fallback exists)" on_fail: retry anti_ban: - delay: [600, 1800] + delay: [1200, 3000] - id: a4 step: buy-supplies @@ -200,8 +246,9 @@ actions: params: item: "current least-loss live candidate (default: rune javelin tips)" qty: "carried_batch = 1,000 (fits the 4h GE limit window of 10,000)" - max_price: "live low + 5%" - on_fail: retry # re-place at live low +5% with paced offers; 3 consecutive failures -> failed (recovery block) + max_price: "live low + 5 %" + note: "offer rests until filled or the run ends; on a partial fill, collect the filled portion (a6) and re-place the remainder at the current edge" + on_fail: retry anti_ban: delay: [1200, 3000] @@ -212,21 +259,45 @@ actions: params: item: "nature rune" qty: "carried_batch = 1,000 (4h GE limit 18,000)" - max_price: "live low + 5%" + max_price: "live low + 5 %" + note: "offer rests until filled or the run ends; partial fills handled like a4" on_fail: retry anti_ban: delay: [1200, 3000] - id: a6 + step: collect-purchases + verb: ge_collect + target: grand-exchange + params: + note: "collect every filled purchase and refund from the GE collection box into the inventory — staff, candidate, natures (and the equivalent on every restock). Partial offers: the collected portion is added to the ledger; nothing is left in the box at session start" + on_fail: retry + anti_ban: + delay: [600, 1400] + + - id: a7 + step: equip-staff + verb: menu_option + target: "staff of fire in inventory" + params: + option: "Wield" + note: "Equip requirement is NONE documented on the OSRS wiki Staff of fire page (no requirements row in the infobox, none in the prose; compare Battlestaff which states 'requires 30 Attack and 30 Magic'; verified 2026-08-28). FAIL-CLOSED path: bounded retries (3), then log ERROR + alert + stop — no fire-rune fallback is modeled (never loop on equip attempts)" + on_fail: retry + anti_ban: + delay: [600, 1800] + + - id: a8 step: build-loadout verb: bank_inventory target: bank params: deposit: [overflow products] - withdraw: "candidate 1,000 + nature runes 1,000 (= carried_batch; both stackable, two inventory slots)" + withdraw: "to carried_batch: candidate 1,000 + nature runes 1,000 (only the shortfall — collected purchases from a6 are already in the inventory; both stackable, two inventory slots)" on_fail: retry + anti_ban: + delay: [500, 1100] - - id: a7 + - id: a9 step: cast-alch verb: use_item_on target: "candidate item in inventory" @@ -237,24 +308,49 @@ actions: consumption: "1 candidate + 1 nature per cast (staff of fire default); pays 540 gp (lowalch) + 31 xp, net -3 gp" on_fail: retry anti_ban: - delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> realized ~791 casts/hr incl. restock+breaks (pinned in economy) + delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> session-realized ~742 casts/hr incl. startup + restock + breaks (pinned in economy) - - id: a8 - step: restock + - id: a10 + step: restock-deposit verb: bank_inventory target: bank params: deposit: [all coins, all products] - withdraw: "candidate + nature runes up to carried_batch (1,000/1,000)" + withdraw: "ALL candidate + nature runes currently in the bank, up to carried_batch (1,000/1,000) — the bank-stock FAST PATH applies only when the bank actually holds enough stock; the remaining deficit is bought at the GE (a11) and collected (a12)" on_fail: alert + anti_ban: + delay: [500, 1100] - - id: a9 + - id: a11 + step: restock-buy + verb: ge_buy + target: grand-exchange + params: + item: "current least-loss live candidate + nature runes" + qty: "deficit to carried_batch = carried_batch - inventory count after the a10 fast path (e.g. 901 candidate + 901 nature after a 901-cast batch with an empty bank)" + max_price: "live low + 5 %" + note: "the deficit is RE-BOUGHT at the GE on every restock — the entry never withdraws a batch that was never purchased; partial/failed fills use the same collect + re-place recovery as the initial acquisition" + on_fail: retry + anti_ban: + delay: [1200, 3000] + + - id: a12 + step: restock-collect + verb: ge_collect + target: grand-exchange + params: + note: "collect the re-bought deficit into the inventory — inventory returns to carried_batch 1,000/1,000 before casting resumes" + on_fail: retry + anti_ban: + delay: [600, 1400] + + - id: a13 step: take-break verb: take_break target: "current location" params: duration: "[45, 120] seconds of idle AFK per break (matches run_control.break.for)" - note: "executed by the translator when the completed-cast counter reaches run_control.break.every — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is not reset by restocking" + note: "executed by the translator when the completed-cast counter reaches run_control.break.every — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is NOT reset by restocking (breaks land at 550/1,100/1,650 in the midpoint example, independent of the 901-cast restock cadence)" on_fail: skip run_control: @@ -265,63 +361,85 @@ run_control: for: [45, 120] # seconds of idle AFK per break stop_conditions: - "magic level >= target (55 -> hand off to HLA)" - - "gp under floor (working capital below one carried_batch = 543,000 gp)" + - "session elapsed >= 2 h (session horizon — startup is amortised over this window in the rate model)" + - "total assets (cash + supplies at current buy cost) < 575,150 — cannot fund the next batch at max fill (570,150) plus staff replacement (5,000); assets, NOT raw cash: cash that became supplies is not a loss" + - "session net loss (total assets - initial capital) > 162,990 — the 6-batch worst-case loss reserve is exhausted" - "3 consecutive failed casts (client issue — do not loop)" - - "3 consecutive failed GE purchases with no fallback candidate available" + - "3 consecutive no-fill GE rounds (initial acquisition or restock) with no fallback candidate available" + - "staff of fire equip failed after bounded retries — FAIL CLOSED (log ERROR, alert, stop)" # ================= WORKED EXAMPLE (pinned, deterministic — owner guidance 2026-08-28) ================= # Constants: carried_batch = 1,000 candidate + 1,000 nature; restock_threshold = 100; - # break.every = [400, 700] casts (midpoint 550); break.for = [45, 120] s; - # default staff-of-fire path consumes 1 candidate + 1 nature per cast (worst case 1:1 — + # break.every = [400, 700] casts — the example pins the MIDPOINT 550; + # break.for = [45, 120] s; restock cadence = 901 casts (1,000 - 99); the + # staff-of-fire path consumes 1 candidate + 1 nature per cast (worst case 1:1 — # with Bryophyta's staff the nature side drains slower, so the candidate stays binding). + # The two counters are MONOTONIC and INDEPENDENT: breaks at 550, 1,100, 1,650, + # 2,200... (multiples of 550) and restocks at 901, 1,802, 2,703... (multiples of + # 901) — gcd(550, 901) = 1, so they interleave forever and restocking NEVER + # resets or shifts the break counter (validator regression: the old buggy + # 'restart the interval at the restock' example is rejected by the gate). # - # Step 0 — before the first inventory: account holds >= 543,000 gp working capital - # (1,000 x 403 + 1,000 x 140). At the GE bank: GE-buy staff of fire, Wield it - # (no documented requirement), GE-buy 1,000 rune javelin tips + 1,000 nature runes, - # withdraw both -> loadout = carried_batch 1,000/1,000. Cast counter := 0. - # Step 1 — casts #1..#550: each cast consumes 1 candidate + 1 nature (-543 gp) and pays - # 540 gp (lowalch) + 31 xp -> net -3 gp. Cast counter 0 -> 550 (~39 min at midpoint). - # Step 2 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every [400, 700]): run - # take-break for 45-120 s. Resume casting at cast 551. The break fired on the CAST - # COUNT, not on any bank visit; the restock counter is untouched. - # Step 3 — casts #551..#901: countdown candidate 1000-901 = 99 < 100 (restock_threshold) - # and nature 1000-901 = 99 < 100 simultaneously (worst case 1:1) -> the transition - # fires at the end of cast #901: FIRST RESTOCK/BANK EVENT at cast 901. - # Coins deposited at this restock: 901 x 540 = 486,540 gp; batch net so far -2,703 gp; - # batch xp so far 901 x 31 = 27,931. - # Step 4 — restock: deposit coins + products, withdraw 901 candidate + 901 nature -> - # carried_batch restored to 1,000/1,000. Resume casting at cast 902 (cast counter - # continues; the break counter also continues — restocking never resets either). - # Step 5 — SECOND BREAK when the cast-count interval since break #1 completes - # ([400, 700], midpoint: cast 902 + 550 = 1,452): take-break, resume at 1,453. - # Step 6 — restock #2 fires at cast 1,802 (901-cast batch cadence), and so on, until a - # stop_condition fires (Magic >= 55 -> HLA handoff; gp under floor; 3 failed casts). - # Bank visits and breaks are INDEPENDENT by construction: restock cadence = 901 casts; - # break cadence = 400-700 casts, and the two counters do not interact. + # Step 0 — preparation (one-time startup, ~300-600 s): account holds >= ~1.31M gp + # cash (staff cap 5,000 + first batch max 570,150 + reserve 162,990 + float + # 570,150). At the GE bank: open GE (a2); place offers — staff (a3), 1,000 + # rune javelin tips (a4), 1,000 nature runes (a5); wait for fills + # (ge_offers_done) and collect everything (a6); equip the staff (a7); loadout + # = carried_batch 1,000/1,000 (a8). Cast counter := 0; cash ledger opened. + # Step 1 — casts #1..#550: each cast consumes 1 candidate + 1 nature (-543 gp) + # and pays 540 gp (lowalch) + 31 xp -> net -3 gp. Cast counter 0 -> 550 + # (~39 min at midpoint). + # Step 2 — FIRST SCHEDULED BREAK at cast 550 (the interval since cast 0): + # take-break 45-120 s (a13). Resume casting at cast 551. The break fired on + # the CAST COUNT, not on any bank visit. + # Step 3 — casts #551..#901: countdown candidate 1,000-901 = 99 < 100 + # (restock_threshold) and nature 99 < 100 simultaneously (worst case 1:1) -> + # the transition fires at the end of cast #901: FIRST RESTOCK at cast 901. + # Deposit 901 x 540 = 486,540 gp (a10); the bank holds 0 supplies (all sales + # were GE buys), so the fast path yields nothing -> open GE (a2), RE-BUY the + # deficit 901 candidate + 901 nature (a11, live low + 5 %), collect (a12) -> + # inventory back to 1,000/1,000. Batch net: -2,703 gp (live-low fills) or + # -27,165 gp (max fills). The cast counter (901) and break counter continue. + # Step 4 — SECOND BREAK at cast 1,100: the interval since break #1 is exactly + # 550 casts (1,100 - 550); the cast-901 restock did NOT reset or shift it. + # Step 5 — THIRD BREAK at cast 1,650 (1,100 + 550); SECOND RESTOCK at cast + # 1,802 (901-cadence, 1,802 = 901 x 2). The sequences 550/1,100/1,650/2,200 + # and 901/1,802/2,703 interleave independently. + # Step 6 — continue until a stop_condition fires: Magic >= 55 (HLA handoff), + # the 2 h session horizon, the capital stops (assets floor 575,150 or net + # loss > 162,990), 3 failed casts, or 3 consecutive no-fill GE rounds. # ============================================================================================ risk: ban_level: low notes: - - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; realized ~791 casts/hr at the 4.3s midpoint cycle incl. restock + breaks; loop-only 837)." - - "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120s AFK) and independent of bank visits — never treat a restock trip as a break." - - "GE buy bursts: pace offers, respect the 4h limit window (10,000 javelin tips / 18,000 natures); thin items (limit < 100) cannot sustain a long run." + - "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4] s; session-realized ~742 casts/hr midpoint incl. startup + restock + breaks over a 2 h session; steady-state ~791; loop-only 837 is a ceiling reference, never the output)." + - "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120 s AFK) and independent of bank visits — never treat a restock trip as a break, and never reset the break counter on restock (break/restock gcd(550, 901) = 1)." + - "GE buy bursts: pace offers, respect the 4h limit window (10,000 javelin tips / 18,000 natures); thin items (limit < 100) cannot sustain a long run; offers persist until filled or aborted and a 4h limit reset is NOT an offer expiry." - "Thin-item 'latest low' fills can be stale — confirm fill price with a live GE offer before committing a run; the liquid high-limit pair (rune javelin tips / rune arrow) is the dependable loss-minimizer." # --- recovery block (owner guidance 2026-08-28: every activity must cover its complete runnable lifecycle) --- # - Missing materials / depleted supplies: the restock_threshold transition (casting -> -# bank_supplies at candidate OR nature < 100) catches both mid-run depletions; restock -# action a8 rebuilds to carried_batch and the loop resumes (worked example steps 3-4). -# - Failed GE purchases (offer unfilled): re-place at live low + 5% with paced offers -# (a4/a5); after 3 consecutive failures, alert + stop (failed) unless a fallback -# candidate is switched in — never sit in an unfillable offer loop. -# - Staff of fire cannot be equipped (a3): fall back to buying 3 fire runes per cast -# (3 fire + 1 nature = 155 gp/cast rune cost vs 140; net -18/cast at the anchor), -# log WARN, continue; never loop on equip attempts. -# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout (a6); -# 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert). -# - Explicit completion/stop: stop_conditions above (Magic 55 HLA handoff, gp floor, -# repeated failures). +# bank_supplies at candidate OR nature < 100) catches both mid-run depletions; the +# restock phase (a10 -> a2 -> a11 -> a12) rebuilds to carried_batch and the loop +# resumes (worked example steps 3-4). The entry NEVER withdraws a batch that was +# never purchased — every restock re-buys the deficit at the GE. +# - GE offers: partial fills are collected (a6/a12) and the remainder is re-placed at +# the current edge (a3/a4/a5/a11); after 3 consecutive no-fill rounds (initial or +# restock) the run stops (failed) — never sit in an unfillable offer loop. Unfilled +# offers at session end are left resting by design (float reserve; coins return when +# they fill or the operator aborts them — the single operator-touch point). +# - Staff of fire cannot be equipped (a7): bounded retries (3), then FAIL CLOSED — +# log ERROR + alert + stop. No fire-rune fallback is modeled (it would need its own +# purchase/supply/rate path; the owner decision rejects a prose-only fallback). +# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout +# (a8); 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert). +# - Capital: the stop checks TOTAL ASSETS (cash + supplies at current buy cost), and +# both capital stops are assets-based (see stop_conditions) — cash converted into +# supplies is not a loss and never fires a stop by itself. +# - Explicit completion/stop: stop_conditions above (Magic 55 HLA handoff, 2 h session +# horizon, assets floor 575,150, loss reserve 162,990, repeated failures, fail-closed +# staff equip). sources: - "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, 1 nature + 3 fire; spellbook Normal; infobox 2026-08-27)" @@ -332,12 +450,14 @@ sources: - "prices.runescape.wiki latest 2026-08-27, re-fetched at bounce fix (low = per-repo buy basis: rune javelin tips 403, rune arrow 45, nature rune 140, jade ring 303, opal amulet (u) 17)" - "OSRS wiki — Module:Exchange/Rune javelin tips (limit 10000, lowalch 540, highalch 810, members=true — matches mapping; re-verified 2026-08-27) + Module:Exchange/Nature rune, Staff of fire and all cited candidates (GE limits agree with mapping; verified 2026-08-27)" - "OSRS wiki — Rune javelin tips / Rune arrow / Bagged flower / Mystic mist staff etc. item pages (members = Yes for every cited catalog item; verified 2026-08-27)" + - "OSRS wiki — Grand Exchange (offers persist until filled or aborted; filled items and coin refunds are collected from the booth/collection box; partial fills are separately collectable) — verified 2026-08-28" + - "OSRS wiki — Grand Exchange/Buying limits (4-hour window; the limit resets 4 hours after the FIRST unit is bought; outstanding offers remain active and a limit reset is NOT an offer expiry) — verified 2026-08-28" - "OSRS wiki — Nature rune (members craft at level 44 Runecraft from pure essence, 9 xp; Nature Altar south-east of Tai Bwo Wannai)" - "OSRS wiki — Abyss (access to runic altars without talismans via the Mage of Zamorak)" - "OSRS wiki — Bryophyta's staff (level 30 Attack + Magic; up to 1,000 nature charges; 1/15 chance to not consume a charge on any nature-rune spell; alchemy combo with Tome of Fire)" - "OSRS wiki — Tome of Fire (members, level 50 Magic to wield; charged tome is an infinite fire-rune source and non-combat spells do not consume charges)" - "OSRS wiki — Staff of fire (page verified 2026-08-28 for the equip requirement: id 1387, F2P item, tradeable, options Wield — NO requirements row in the Infobox Item and NO requirement statement in the prose; contrast Battlestaff, whose prose states 'requires 30 Attack and 30 Magic to wield')" - - "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verb take_break — the cadence + break + recovery model" + - "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verbs ge_open/ge_buy/ge_collect/ge_offers_done (separate GE primitives, never assumed) — the cadence + break + GE + recovery model" verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27 related: diff --git a/tools/validate.py b/tools/validate.py index 7667c3d..0068a6f 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -41,6 +41,37 @@ def load_entries(): return sorted(out) +def alch_worked_example_regression(rel, data, raw_text): + """One-cast model worked-example regression (owner guidance 2026-08-28, + PR #8 repair): entries with the owner-selected one-cast iteration model + must pin a worked example with MONOTONIC, INDEPENDENT counters — breaks + every 550 casts (1,100, 1,650, ...) regardless of restocks, and restocks + every 901 casts (1,802, ...), so restocking never resets or shifts the + break counter. The old 'restart the interval at the restock' bug (break + #2 at cast 1,452 = 901 + restarted 549) must never reappear. Scoped to + entries that declare the one-cast model AND carry a worked example, so + unrelated entries are untouched.""" + ruco = data.get("run_control") or {} + if "one cast" not in str(ruco.get("iteration", "")): + return [] + if "worked example" not in raw_text.lower(): + return [] + errs = [] + if "1,452" in raw_text: + errs.append(f"{rel}: worked example resets the break interval at a restock " + f"(cast 1,452 = 901 + restarted interval) — breaks must stay " + f"monotonic at 550/1,100/1,650 independently of restocks") + for marker, label in (("550", "break #1 (cast 550)"), + ("901", "restock #1 (cast 901)"), + ("1,100", "break #2 (cast 1,100)"), + ("1,650", "break #3 (cast 1,650)"), + ("1,802", "restock #2 (cast 1,802)")): + if marker not in raw_text: + errs.append(f"{rel}: worked example missing monotonic counter pin " + f"{label} — breaks/restocks must be pinned literally") + return errs + + def main(): global STALE_DAYS if "--stale-days" in sys.argv: @@ -99,6 +130,10 @@ def main(): if term not in declared: errors.append(f"{rel}: terminal '{term}' not declared") + # one-cast worked-example regression (owner guidance 2026-08-28): + # monotonic break/restock counters, scoped to the one-cast model + errors += alch_worked_example_regression(rel, data, path.read_text()) + # action id/step uniqueness a_ids = [a["id"] for a in data["actions"]] a_steps = [a["step"] for a in data["actions"]] From 02b366c7ef40e0082cc173b5f6960096c7253067 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 04:01:20 -0500 Subject: [PATCH 12/15] flip-candidates: per-slot async sub-state lifecycle (governor 10323 bounce-fix 5) - slot_state_machine: canonical 8 sub-states (free/buy-open/ buy-cancelled-awaiting-collect/ready-to-sell/sell-open/ sell-cancelled-awaiting-collect/ready-to-requote/closed) + global scheduler pass that never advances unrelated slots - ge_cancel moves filled items/refunds/unsold units/coins to the GE collection box; ge_collect + ledger reconciliation before reuse - 4h buy-limit ledger (limit_used/first_fill_ts/limit_resets_at) distinct from offer expiry; cancellation never renews the limit - per-slot conservation invariants w/ committed validator fixtures - economy.ceiling_gp_per_hour typed ceiling, gp_per_hour_low/high as documented realized scenario bounds (no uncited multipliers) - capped/floored 2% tax + 4 fixtures preserved (owner decision) - action-grammar: ge_cancel/ge_collect rows corrected --- codex/economy/flip-candidates.yaml | 414 +++++++++++++++++++---------- docs/action-grammar.md | 4 +- schemas/entry.schema.json | 34 +++ tools/validate.py | 346 +++++++++++++++++++++++- 4 files changed, 643 insertions(+), 155 deletions(-) diff --git a/codex/economy/flip-candidates.yaml b/codex/economy/flip-candidates.yaml index d65a375..bf89878 100644 --- a/codex/economy/flip-candidates.yaml +++ b/codex/economy/flip-candidates.yaml @@ -3,22 +3,31 @@ title: GE Flipping (margin trading) domain: economy tags: [flipping, merchanting, ge, money-making, hybrid] status: verified -version: 3 # 3: bounce-fix 4 2026-08-28 (owner lifecycle addendum) — complete runnable flow: partial fills, ge_cancel + requote recovery, per-slot ledger, break/resume path, lifecycle-qualified rates, worked example pinned; ge_cancel added to the action grammar + schema +version: 4 # 4: bounce-fix 5 2026-08-28 (governor comment 10323) — per-slot asynchronous sub-state machine + global scheduler pass; ge_cancel moves everything to the GE collection box (ge_collect + ledger reconciliation before reuse); 4h buy-limit ledger distinct from offer expiry (cancellation never renews the limit); typed ceiling_gp_per_hour vs documented realized scenarios; conservation invariants; breaks with persisted per-slot state. Capped/floored tax model and its 4 selftest fixtures preserved (owner decision 2026-08-28). updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date -summary: > +summary: >- Buy-and-sell Grand Exchange margin flipping: place resting buy offers at the low edge of an item's price band and resting sell offers at the high edge, harvesting the spread on high-volume items. There are no skill or quest requirements — capital breadth and slot discipline are the whole game. Members accounts run 8 GE slots over the full candidate set; F2P accounts get 3 slots and F2P-tradeable rows only (separate F2P path in - profit_drivers). Lifecycle (owner addendum 2026-08-28): the entry models - the complete runnable flow — capital allocation, per-slot quotes with a - quoted/filled/sold/collected ledger, partial fills, ge_cancel + - requote recovery on window lapse or stale prices, collection and - reconciliation, breaks on resting offers, and explicit stop conditions — - with a pinned worked example and realized (not idealised) rates. + profit_drivers). Model (governor 10323, 2026-08-28): every GE slot is an + INDEPENDENT asynchronous sub-state machine (free / buy-open / + buy-cancelled-awaiting-collect / ready-to-sell / sell-open / + sell-cancelled-awaiting-collect / ready-to-requote / closed) driven by a + global scheduler pass that acts only on dispatchable slots and never + closes or advances unrelated ones; the 4h buy-limit ledger (limit_used / + first_fill_ts / limit_resets_at / remaining) is DISTINCT from offer + expiry and cancellation NEVER renews the limit; ge_cancel moves filled + items, refunds, unsold units and unspent coins to the GE collection box, + and a separate ge_collect withdraws and reconciles the per-slot ledger + before any reuse; every snapshot satisfies per-slot conservation + (quoted == still_offered + collected + refunded + sold; cash likewise); + economy.ceiling_gp_per_hour is the TYPED economic ceiling and + gp_per_hour_low/high are documented realized scenario bounds. Pinned + worked example + committed validator fixtures prove the model. accounts: membership: hybrid # members: 8 slots/full set; F2P: 3 slots/F2P rows (see economy) @@ -56,21 +65,32 @@ best_practice: - name: pace quotes inside the 4h buy-limit window, watch connected limits why: "The buying rate is restricted to a set quantity every 4 hours per player, and some items share a connected limit with related items — treat those as one bucket - when filling slots." + when filling slots. The window starts when the FIRST unit of the window is bought + (first_fill_ts) and resets 4h later; cancelling an offer does NOT renew or restart + it." source: "OSRS wiki — Grand Exchange (Buy limits; Grand Exchange/Buying limits)" - name: run the path that matches the account's slot set why: "Members get eight GE slots and may trade any row; F2P accounts get three slots and may only purchase free-to-play rows. The modeled economics below assume the members set — a hybrid account must swap to the 3-slot F2P path (Iron bar / Cosmic rune / Silver ore) instead of chasing members-only rows it cannot buy." - source: "OSRS wiki — Grand Exchange (Members get eight Grand Exchange slots ... free players are limited to three. Additionally, free players may only purchase free-to-play items, and not members' items)" - - name: keep a per-slot offer ledger; cancel-and-requote on lapse, never strand units + source: "OSRS wiki — Grand Exchange (Members get eight Grand Exchange slots ... free players are limited to three. Additionally, free players may only purchase members' items)" + - name: treat each slot as its own asynchronous machine; the scheduler never touches resting slots + why: "A slot's offer fills on its own timeline (partial fills keep resting). The + global scheduler pass acts only on dispatchable slots (free -> quote, + *-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote -> + requote) and leaves resting buy-open / sell-open / closed slots untouched — one + slot's cancel or close never advances an unrelated slot." + source: "governor review comment 10323 (2026-08-28), finding 3; docs/action-grammar.md" + - name: keep a per-slot offer ledger; ge_cancel -> collection box -> ge_collect -> reconcile, never strand units why: "Every slot tracks quoted qty -> filled qty -> sold qty -> collected proceeds. - A partially-filled offer keeps resting with its acquired units attached; on the 4h - window lapse or a stale price the slot is ge_cancel'ed — acquired units return to - inventory, unspent cash refunds — then re-quoted at the fresh edge (buy) or exited - at the current high edge (sell). No items, coins, or GE slots are ever stranded." - source: "OSRS wiki — Grand Exchange (offer mechanics; 4h window); docs/action-grammar.md (ge_cancel, added 2026-08-28)" + ge_cancel moves filled items, refunds, unsold units and unspent coins to the GE + COLLECTION BOX (never directly to inventory); a separate ge_collect withdraws + them, and the ledger is reconciled (conservation: quoted == still_offered + + collected + refunded + sold, cash likewise) before the slot is reused. The 4h + window lapse or a stale price triggers ge_cancel; the fresh-edge requote happens + only after collection. No items, coins, or GE slots are ever stranded." + source: "OSRS wiki — Grand Exchange (offer mechanics; 4h window); governor review comment 10323 finding 2; docs/action-grammar.md (ge_cancel / ge_collect)" economy: # Model (documented, reproducible from the snapshot): @@ -88,24 +108,28 @@ economy: # members flags cross-checked against the wiki item pages (all agree). Top 8 # by 4h/slot below in profit_drivers. # - # Lifecycle qualification (owner addendum 2026-08-28): the 4h figures are - # an economic CEILING — ideal full fills of every slot with zero overhead. - # The realized operating range is: - # realized gp/h = ceiling x fill_factor x duty_cycle, where - # fill_factor ~0.5-0.9 (partial fills, cancels, requotes reduce the - # filled volume) and duty_cycle ~0.90-0.95 (scan + place + collect - # ~3 min per ~40-min rotation; breaks every 6-10 rotations). - # Example midpoint: 149,500 x 0.70 x 0.92 ≈ 96,300 gp/h. - # gp_per_hour_low (74,750) = 50% of the ceiling — a conservative realized - # lower bound; gp_per_hour_high (149,500) = the named ceiling, never - # presented as the achieved runtime rate. + # Lifetime qualification (governor 10323 finding 5, 2026-08-28): + # ceiling_gp_per_hour (149,500) is the TYPED ECONOMIC CEILING: ideal full + # fills of every slot, zero overhead — never presented as an achieved + # runtime rate. gp_per_hour_low/high are DOCUMENTED REALIZED SCENARIO + # BOUNDS (not uncited multipliers), each with explicit arithmetic in + # profit_drivers: + # high 138,000 = 149,500 x 37/40 — 'resting-fill' scenario: all 8 slots + # full-fill at their quoted edges; each 40-min rotation is 37 min of + # resting waits + 3 min of scan/place/collect actions. + # low 74,750 = 149,500 x 0.50 — 'requote-heavy' scenario: every 4h + # window's volume completes only after one ge_cancel + requote cycle + # (the no-fill remainder is refunded to the collection box and + # re-quoted on the next window), halving the window's filled volume. + # Bounds nest: 0 <= 74,750 <= 138,000 <= 149,500 (validator-enforced). item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members) item_id: 44 cost_per_cycle: 223 # buy @ low edge per unit (snapshot 2026-08-27) income_per_cycle: 244 # sell @ high edge per unit, gross (snapshot 2026-08-27) net_per_cycle: 17 # 244 - min(floor(244*0.02), 5M) - 223 = 244 - 4 - 223 (floored, capped tax) - gp_per_hour_low: 74750 # realized lower bound = 50% of the ceiling: partial fills, cancels/requotes, collection trips, breaks - gp_per_hour_high: 149500 # 4h buy-limit CEILING: sum(4h/slot) / 4 (8 members slots, ideal full fills, zero overhead) — qualified above + ceiling_gp_per_hour: 149500 # TYPED economic ceiling: sum of the 8 slots' 4h/slot (= 598,000) / 4h — ideal full fills, zero overhead (see profit_drivers table) + gp_per_hour_low: 74750 # realized scenario bound 'requote-heavy' = 149,500 x 0.50 (documented in profit_drivers) + gp_per_hour_high: 138000 # realized scenario bound 'resting-fill' = 149,500 x 37/40 (documented in profit_drivers) xp_per_hour: 0 # flipping awards no XP supplies: - item: Rune arrowtips # canonical wiki names per action-grammar conventions @@ -126,36 +150,103 @@ economy: - "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored+capped 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h CEILING on 8 slots" - "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h ceiling (alternates: Raw swordfish 45,000, Raw lobster 30,000)" - "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges for members; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells" - - "Realized vs ceiling (work the full lifecycle): realized gp/h = ceiling x fill_factor (~0.5-0.9, partials/cancels/requotes) x duty_cycle (~0.90-0.95, scan/place/collect + breaks) — e.g. 149,500 x 0.70 x 0.92 ~ 96,300 gp/h midpoint; do not quote the ceiling as an achieved rate" + - "Realized bounds are DOCUMENTED SCENARIOS with explicit arithmetic (governor 10323 finding 5 — no uncited fill/duty multipliers): HIGH 'resting-fill' 138,000 gp/h is the ceiling scaled by the rotation duty of 37 resting minutes per 40-minute rotation (149,500 x 37/40 = 138,287.5 -> 138,000): scan/place/collect actions cost ~3 min per rotation at the 600s quote-check cadence; LOW 'requote-heavy' 74,750 gp/h assumes every 4h window completes only after one ge_cancel + requote cycle, so the no-fill remainder (refunded to the collection box and re-quoted on the next window) halves the filled volume (149,500 x 0.50). The ceiling 149,500 itself is an economic bound, never an achieved rate." + +slot_state_machine: + sub_states: [free, buy-open, buy-cancelled-awaiting-collect, ready-to-sell, sell-open, sell-cancelled-awaiting-collect, ready-to-requote, closed] + slot_transitions: + free: [buy-open] + buy-open: [ready-to-sell, buy-cancelled-awaiting-collect] + buy-cancelled-awaiting-collect: [ready-to-sell, ready-to-requote, free] + ready-to-sell: [sell-open] + sell-open: [closed, sell-cancelled-awaiting-collect] + sell-cancelled-awaiting-collect: [ready-to-requote, closed] + ready-to-requote: [buy-open, sell-open, free] + closed: [] + slot_transition_whens: + - "free -> buy-open: scheduler pass quotes a resting buy at the low edge (qty = min(remaining 4h limit, slot capital, capital available))" + - "buy-open -> ready-to-sell: offer FULLY filled (filled_qty == quoted qty) — units in hand, dispatch sells this pass" + - "buy-open -> buy-cancelled-awaiting-collect: 4h window lapse, or scanned edge stale (margin no longer positive) — ge_cancel: filled items + refunded units + unspent coins ALL move to the GE collection box; the buy limit is NOT renewed by the cancel" + - "buy-cancelled-awaiting-collect -> ready-to-sell: ge_collect withdrawn units to inventory; sell edge positive — sell the units in hand" + - "buy-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; buy margin still positive AND remaining limit > 0 — requote the unfilled balance at the fresh low edge" + - "buy-cancelled-awaiting-collect -> free: ge_collect withdrawn; slot released (margin gone or capital reallocated)" + - "ready-to-sell -> sell-open: scheduler pass quotes a resting sell at the high edge for the units in hand" + - "sell-open -> closed: sell offer fully filled, proceeds collected, ledger reconciled — slot freed" + - "sell-open -> sell-cancelled-awaiting-collect: 4h window lapse or stale edge — ge_cancel: unsold units return to the GE collection box (never to inventory)" + - "sell-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; requote the sell at the fresh high edge or the current band" + - "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; slot closed (exit taken)" + - "ready-to-requote -> buy-open: requoted buy placed (buy_capacity_ok: limit_used + qty <= limit until limit_resets_at)" + - "ready-to-requote -> sell-open: requoted sell placed at the fresh high edge" + - "ready-to-requote -> free: slot abandoned and released" + scheduler_iteration: >- + One pass over every slot exactly once, at the 600 s quote-check cadence. + Only dispatchable slots act: free -> quote (ge_buy), buy-cancelled- + awaiting-collect / sell-cancelled-awaiting-collect -> ge_collect, + ready-to-sell -> sell (ge_sell), ready-to-requote -> requote. Resting + buy-open / sell-open / closed slots produce NO action and are never + closed, advanced, re-quoted or touched by the pass. A pass therefore + advances only the slots whose sub-state is dispatchable. + quote_age_policy: >- + INDEPENDENT of the 4h buy-limit ledger: resting offers are re-checked at + the 600 s cadence; an offer is aborted ONLY on a stale band edge + (scanned margin no longer positive) or the 4h window lapse — never + merely because it has been resting. An offer that still clears the + scanned margin keeps resting until it fills or the window lapses. + persisted_state: >- + Per-slot record persisted between passes and across breaks: sub-state, + item, quoted qty, still_offered qty, collected qty, refunded qty, sold + qty, cash_quoted, still_offered_cash, collected_cash, refunded_cash, + limit_used, first_fill_ts, limit_resets_at, quote placed_at, last + check_ts. The GE offers themselves persist server-side; the record is + the agent's copy of truth for the scheduler and conservation checks. + limit_ledger: >- + Per item: limit_used (units bought so far in the current 4h window), + first_fill_ts (when the FIRST unit of the window was bought), + limit_resets_at = first_fill_ts + 4h, remaining = limit - limit_used. + The ledger is DISTINCT from offer expiry: offers rest independently of + the window, and cancelling never renews or restarts it — a requote + before limit_resets_at may buy at most remaining (buy_capacity_ok: + limit_used + qty <= limit). A slot with limit_used == limit cannot + requote until limit_resets_at. + conservation: >- + Per slot at every snapshot: quoted == still_offered + collected_items + + refunded + sold (the buckets partition the quote's units — no losses, + no duplication, no negatives); cash_quoted == still_offered_cash + + collected_cash + refunded_cash. Impossible pairs (units in a live offer + AND a terminal bucket) are violations: still_offered + sold, and + still_offered + refunded (cancel is atomic). ge_cancel moves every + filled / refunded / unsold unit and coin to the GE collection box; + ge_collect withdraws them and reconciles the ledger BEFORE any slot + reuse. Committed validator fixtures prove the invariants and the + scheduler dispatch. + break_resume: >- + Breaks are taken only when every slot is resting (buy-open / sell-open) + or collected (closed) — never mid-collect or mid-place. Full state + (offers server-side + the persisted per-slot record) survives the + pause; resume by re-opening the GE, re-checking offers, re-running the + scheduler pass and continuing each slot from its persisted sub-state. + Slots and slots only: the 8-slot members / 3-slot F2P activity is never + downgraded to single-slot work. state_machine: initial: idle terminal: [complete, failed] states: idle: - goal: "be logged in at a bank with a GE booth, capital >= floor, per-slot offer ledger empty (quoted = filled = sold = collected = 0 for every slot)" + goal: "be logged in at a bank with a GE booth, capital >= floor, every slot's ledger closed (sub-state free or closed, all buckets zero)" next: [scan, complete] scan: - goal: "read the current high/low band for every candidate; compute net-positive edges with floored+capped tax" - next: [buy, idle] - buy: - goal: "place one resting buy offer per free slot at or below the low edge; record quoted qty and remaining 4h limit per slot in the ledger" - next: [wait_buy_fill, idle] - wait_buy_fill: - goal: "hold BUY offers; track filled_qty per slot (partial fills included, units stay attached to the resting offer); on the 4h window lapse or a stale price, cancel the unfilled balance" - next: [sell, cancel_requote] - sell: - goal: "place one sell offer per slot holding units (its filled_qty) at or above the high edge" - next: [wait_sell_fill, cancel_requote] - wait_sell_fill: - goal: "hold SELL offers; track sold qty per slot; on lapse or a stale price, cancel and re-quote — units are never left stranded" - next: [collect, cancel_requote] - cancel_requote: - goal: "ge_cancel the timed-out/stale offers of the current phase: acquired units return to inventory, unspent cash refunds, slot freed; re-quote at a fresh edge (buy when margin is still positive; sell to exit at the current high edge) or free the slot" - next: [wait_buy_fill, sell, wait_sell_fill] + goal: "read the current high/low band for every candidate; compute net-positive edges with floored+capped tax (a3)" + next: [schedule, idle] + schedule: + goal: "run ONE scheduler pass over all 8 (members) or 3 (F2P) slots: dispatch free -> quote, *-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote -> requote; resting slots untouched (slot_state_machine.scheduler_iteration)" + next: [wait, collect] + wait: + goal: "hold resting offers; at the 600 s cadence re-check offers (a6) and re-run the scheduler pass (schedule); a slot leaves wait when it becomes dispatchable" + next: [schedule, collect] collect: - goal: "collect proceeds, deposit coins, reconcile (inventory empty of flip items, all GE slots free, each slot's ledger closed with a logged RESULT)" - next: [scan, complete, idle] + goal: "ge_collect every awaiting-collect slot: withdraw filled items, refunds, unsold units and proceeds from the collection box; reconcile each slot's ledger (conservation invariants) and only then allow slot reuse (a9)" + next: [schedule, scan, complete, idle] complete: goal: "session GP target reached" failed: @@ -169,40 +260,25 @@ state_machine: to: complete - from: scan when: "at least one net-positive candidate at the scanned band edges (floored+capped tax)" - to: buy + to: schedule - from: scan when: "no net-positive candidate on 3 consecutive scans (prices stale — stand down)" to: idle - - from: buy - when: "all free slots quoted (quoted qty recorded per slot)" - to: wait_buy_fill - - from: buy - when: "capital exhausted before quoting (insufficient funds)" - to: idle - - from: wait_buy_fill - when: "a slot's buy offer is FULLY filled (filled_qty == quoted qty) — that slot's units move to sell" - to: sell - - from: wait_buy_fill - when: "a slot's buy offer hits the 4h window lapse or a stale price with filled_qty < quoted qty — ge_cancel: acquired units return to inventory, unspent cash refunds, slot freed" - to: cancel_requote - - from: cancel_requote - when: "re-quoted buy offers placed at the fresh low edge (window renewed, margin still positive)" - to: wait_buy_fill - - from: cancel_requote - when: "buy-side margin is gone — free the slot on the buy side; any acquired units from the cancelled offer still sell at the current high edge (exit at cost or a small loss rather than strand inventory)" - to: sell - - from: cancel_requote - when: "re-quoted sell offers placed at the fresh high edge (or lowered to the current band) for units in hand" - to: wait_sell_fill - - from: sell - when: "sell offers placed for all units in hand (filled_qty per slot)" - to: wait_sell_fill - - from: wait_sell_fill - when: "all pending sells filled — per-slot proceeds ready to collect" + - from: schedule + when: "pass placed or requoted offers (quotes outstanding) — rest until the next cadence" + to: wait + - from: schedule + when: "pass marked slots awaiting collection (ge_cancel happened) — withdraw before further dispatch" to: collect - - from: wait_sell_fill - when: "a sell offer hits the 4h window lapse or a stale price — ge_cancel and re-quote at the fresh high edge or lower to the current band; units are never stranded" - to: cancel_requote + - from: wait + when: "600 s cadence reached — re-check offers and re-run the scheduler pass" + to: schedule + - from: wait + when: "a slot filled and proceeds/results are ready to collect" + to: collect + - from: collect + when: "only resting or closed slots remain; cadence continues" + to: wait - from: collect when: "session GP target reached" to: complete @@ -252,11 +328,11 @@ actions: verb: ge_buy target: grand-exchange params: - item: "per candidate slot" - qty: "min(remaining 4h limit, slot capacity, capital available)" + item: "per dispatchable free slot (scheduler pass)" + qty: "min(remaining 4h limit, slot capacity, capital available) — buy_capacity_ok(qty + limit_used <= limit)" price: "low edge (rounded down)" max_price: "low edge — never instant" - ledger: "record quoted qty and remaining 4h limit per slot" + ledger: "record quoted qty, cash_quoted, limit_used check; sub-state -> buy-open" on_fail: skip anti_ban: delay: [900, 2700] @@ -266,7 +342,7 @@ actions: verb: sleep params: delay: [6000, 12000] - until: "ge_offers_done changes or a fill event or 4h window lapse" + until: "ge_offers_done changes or a fill event or the 600s cadence or 4h window lapse" on_fail: retry anti_ban: delay: [300, 900] @@ -276,9 +352,9 @@ actions: verb: ge_offers_done target: "all quoted slots" params: - phase: "buy (no pending sells) | sell (no pending buys)" - per_slot: "filled_qty vs quoted_qty for every slot (partial fills included)" - verdict: "full | partial | none | 4h-lapse" + phase: "per-slot: buy-open (buy outstanding) | sell-open (sell outstanding)" + per_slot: "still_offered vs quoted for every slot (partial fills stay attached to the resting offer)" + verdict: "full | partial | none | lapse | stale-edge" on_fail: retry - id: a7 @@ -286,31 +362,33 @@ actions: verb: ge_sell target: grand-exchange params: - item: "per slot holding units (filled_qty)" - qty: "filled_qty of that slot" + item: "per ready-to-sell slot (units in hand after full fill or ge_collect)" + qty: "units in hand for that slot" price: "high edge (rounded up)" - ledger: "record sold qty per slot" + ledger: "record sold qty per slot; sub-state -> sell-open" on_fail: skip anti_ban: delay: [900, 2700] - id: a8 - step: cancel-requote + step: cancel-to-box verb: ge_cancel - target: "timed-out or stale slot offers (current phase)" + target: "lapsed or stale slot offers (current phase)" params: - offers: "the slots that hit the 4h window lapse or a stale price" - cancelled: "acquired units return to inventory; unspent cash refunds; slot freed" - requote: "buy: re-place at the fresh low edge when the margin is still positive; sell: re-place at the fresh high edge or lower to the current band; margin gone on the buy side: free the slot and sell any acquired units to exit" + offers: "the slots that hit the 4h window lapse or a stale edge" + moved: "filled items + refunded units + unsold units + unspent coins ALL move to the GE COLLECTION BOX (never directly to inventory)" + limit: "the 4h buy limit is NOT renewed or restarted by the cancel (limit_used stays)" + sub_state: "buy-open -> buy-cancelled-awaiting-collect; sell-open -> sell-cancelled-awaiting-collect" on_fail: retry - id: a9 - step: collect-and-log + step: collect-and-reconcile verb: ge_collect target: grand-exchange params: - deposit: "coins to bank" - reconcile: "inventory empty of flip items, all GE slots free, per-slot ledger closed" + withdraw: "from the GE collection box: filled items, refunds, unsold units, sale proceeds" + reconcile: "per-slot ledger conservation check (quoted == still_offered + collected + refunded + sold; cash likewise) BEFORE any slot reuse" + decide: "ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release)" log: "RESULT item, qty, net gp per slot (floored+capped tax)" on_fail: alert anti_ban: @@ -322,64 +400,105 @@ actions: target: "current location" params: duration: "[60, 240] seconds of idle AFK per break (matches run_control.break.for)" - safe: "resting offers persist server-side at their quoted edges — the full state lives in the GE interface plus the per-slot ledger, so a break at any resting-offer point is safe" - resume: "re-open GE (a2), re-check offer state (a6), continue from the current phase (a4/a7/a8/a9) — never break mid-collect (coins in hand) or mid-place (offer half-configured)" + safe: "only when every slot is resting (buy-open / sell-open) or collected (closed) — resting offers persist server-side at their quoted edges and the per-slot record persists locally; never break mid-collect (coins in hand) or mid-place (offer half-configured)" + resume: "re-open GE (a2), re-check offer state (a6), re-run the scheduler pass (schedule), continue each slot from its persisted sub-state (a4/a7/a8/a9)" on_fail: skip run_control: - iteration: "one full rotation: quote buys -> fills/cancels/requotes -> sell filled units -> sells fill/cancel/requote -> collect + reconcile, across all slots" + iteration: "one scheduler pass over every slot at the 600 s cadence — dispatch free -> quote, *-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote -> requote; resting slots untouched (slot_state_machine.scheduler_iteration)" max_iterations: null break: - every: [6, 10] # rotations (resting offers persist; state = GE + per-slot ledger) + every: [6, 10] # passes (resting offers persist; state = GE offers + per-slot record) for: [60, 240] # seconds of idle AFK stop_conditions: - "session GP target reached" - "capital under floor" - "no net-positive candidates on 3 consecutive scans" - - "3 consecutive aborted offers (client issue — do not loop)" + - "3 consecutive failed/aborted passes (client issue — do not loop)" - # ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28) ================= + # ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28; slot-machine walk per governor 10323) ================= # Members account, 8 GE slots, capital 22,500,000 gp. Snapshot prices 2026-08-27 # (buy/sell = low/high edge; tax = min(floor(0.02 x sell), 5,000,000)). + # The run below walks THREE slots in detail (arrowtips / iron / cosmic) exactly as + # the committed scheduler-dispatch + conservation fixtures; the other five slots + # behave identically to the iron slot and are omitted for brevity. # - # Step 0 — preparation and capital allocation: capital at the bank, GE-adjacent. - # Scan bands (a3): only net-positive slots are quoted. Per-slot capital needs: - # arrowtips 223x10,000 = 2.23M; iron bar 145x10,000 = 1.45M; cosmic 122x18,000 = - # 2.196M; silver ore 73x13,000 = 0.949M; swordfish 206x15,000 = 3.09M; lobster - # 106x15,000 = 1.59M; magic logs 772x12,000 = 9.264M; mud 96x18,000 = 1.728M - # (total 22.497M <= 22.5M). Ledger: quoted=filled=sold=collected=0 per slot. - # Step 1 — place buys (a4): quote all 8 slots at the low edges (qty = 4h limit). - # wait_buy_fill (a5/a6). - # Step 2 — PARTIAL FILL: arrowtips fills 4,000 of 10,000 after ~18 min -> check-fills - # (a6) verdict PARTIAL: ledger arrowtips.filled = 4,000. The offer KEEPS resting - # (acquired units stay attached to the resting offer; nothing is stranded). - # Step 3 — FULL FILL: iron bar fills all 10,000 -> transition to sell (a7): place - # sell 10,000 @ 157 (high edge). wait_sell_fill -> fills -> collect (a9): - # proceeds 1,570,000; tax floor(157 x 0.02) = 3/item -> net = (157-3-145) x 10,000 - # = 90,000. Reconcile: inventory empty, slot free, ledger closed. - # Step 4 — NO-FILL / STALE recovery on the buy side: cosmic 18,000 @ 122 is unfilled - # approaching the 4h lapse -> cancel_requote (a8, ge_cancel): 0 units return, - # 2,196,000 cash refunds, slot freed; re-scan (a3): cosmic still net-positive -> - # requote 18,000 @ 122 on the fresh window. (If the buy margin had vanished the - # slot is freed on the buy side; any units acquired from a cancelled offer still - # sell at the current high edge.) - # Step 5 — PARTIAL matures at lapse: arrowtips still 4,000/10,000 -> ge_cancel: - # 4,000 units return to inventory, 6,000 x 223 = 1,338,000 refunded -> sell (a7): - # 4,000 @ 244 -> tax floor(244 x 0.02) = 4/item -> net = (244-4-223) x 4,000 = - # 68,000. Collect (a9) + reconcile. - # Step 6 — FIRST SCHEDULED BREAK after 6-10 rotations (midpoint 8): execution is at - # a safe point — every slot is either collected or resting at its quoted edge; the - # state (offers + per-slot ledger) persists server-side. take-break 60-240 s - # (a10); RESUME at a2 (re-open GE) -> a6 (check-fills) -> continue the current - # phase (a4 buys / a7 sells / a8 cancel-requote / a9 collect). Breaks never strand - # an offer and never interrupt a collect/place in progress. - # Step 7 — STOP: session target (e.g. 900,000 net) -> collect -> complete; capital - # fell below floor -> idle -> complete; 3 consecutive aborted offers -> failed. - # Rate qualification: the 149,500 gp/h figure is the 4h buy-limit CEILING (8 slots, - # full fills, zero overhead). Realized midpoint ~ 149,500 x 0.70 (fill factor) x - # 0.92 (duty cycle incl. scan/place/collect + breaks) ~ 96,300 gp/h — the entry's - # low bound 74,750 (50% ceiling) sits under that; the ceiling is never presented - # as an achieved runtime rate. + # t0 idle -> scan -> schedule (pass 1): all slots free. Scheduler dispatch: + # free -> quote. Slot ledger after the pass (conservation holds): + # arrowtips: buy-open, quoted 10,000 @ 223; cash_quoted 2,230,000 + # iron: buy-open, quoted 10,000 @ 145; cash_quoted 1,450,000 (limit_ledger entry: limit_used 0, first_fill_ts -, limit_resets_at -) + # cosmic: buy-open, quoted 18,000 @ 122; cash_quoted 2,196,000 + # Buckets: still_offered == quoted for every slot; collected/refunded/sold = 0. + # + # t1 wait, ~18 min: arrowtips PARTIAL fill 4,000/10,000 (a6 verdict PARTIAL). + # Ledger: arrowtips quoted 10,000 == still_offered 6,000 + collected_items 4,000 + # (the 4,000 units sit in the player's possession — GE box — attached to the + # resting offer; nothing is stranded). limit_ledger: limit_used 4,000, + # first_fill_ts now, limit_resets_at = first_fill_ts + 4h; remaining 6,000. + # Cosmic: no fill; iron: FULL fill 10,000 -> ready-to-sell. + # + # t2 cadence -> schedule (pass 2): dispatch ONLY the dispatchable slot: + # iron ready-to-sell -> sell-open (a7): 10,000 @ 157 (high edge). + # arrowtips buy-open -> NO ACTION (resting, quote age within policy). + # cosmic buy-open -> NO ACTION (resting, no fill, quote age within policy). + # => the pass never closes or advances unrelated slots (regression fixture). + # Iron ledger: quoted 10,000 == still_offered 0 + sold 10,000 (buckets move on fill). + # + # t3 iron sell-open FULL fill -> collect: proceeds 1,570,000; tax floor(157 x 0.02) + # = 3/item -> net = (157 - 3 - 145) x 10,000 = 90,000. ge_collect (a9): + # cash withdrawn, ledger reconciled, sub-state -> closed, slot freed. + # Conservation: quoted 10,000 == sold 10,000 (all other buckets 0). + # + # t4 LAPSE / stale-edge handling (a8): at the 4h window edge, cosmic still has NO + # fill -> ge_cancel: the offer's cash 2,196,000 moves to the COLLECTION BOX as a + # refund. Ledger: cash_quoted 2,196,000 == still_offered_cash 0 + refunded_cash + # 2,196,000 (collected_cash 0). Sub-state -> buy-cancelled-awaiting-collect. + # arrowtips reaches limit_resets_at with 6,000 still offered -> ge_cancel: + # filled 4,000 (already in player possession) + refunded 6,000 units + unspent + # cash 6,000 x 223 = 1,338,000 ALL move to the collection box. Ledger: + # quoted 10,000 == collected_items 4,000 + refunded 6,000; limit_used STAYS + # 4,000 (the cancel does NOT renew the limit; remaining 6,000 stands). + # Sub-state -> buy-cancelled-awaiting-collect for both slots. + # + # t5 cadence -> schedule (pass 3): dispatch the awaiting-collect slots -> ge_collect + # (a9). Withdraw + reconcile: + # arrowtips: units 10,000 (4,000 filled + 6,000 refund) -> inventory. + # Ledger: quoted 10,000 == collected_items 10,000 (refunded absorbed into + # collected on withdrawal). Buy margin still positive AND remaining limit + # 6,000 > 0. DECIDE (a9): ready-to-requote -> requote buy 6,000 @ 223 + # (buy_capacity_ok: limit_used 4,000 + 6,000 = 10,000 <= 10,000 — allowed; + # a 6,001-unit requote would be BLOCKED until limit_resets_at — fixture). + # -> buy-open on the fresh edge. + # cosmic: cash 2,196,000 withdrawn (refunded_cash -> collected_cash 2,196,000). + # Margin still positive, limit_used 0 -> ready-to-requote -> buy-open 18,000 + # @ 122 (full capacity available). + # iron: already closed (free). + # => ONLY the awaiting-collect slots acted; resting/closed slots untouched. + # + # t6 Scheduler continues; arrowtips requote buy 6,000 fills -> ready-to-sell -> + # sell-open 10,000 @ 244 (4,000 in hand from the t1 fill + 6,000 from the + # requote) -> fills -> collect (a9): tax floor(244 x 0.02) = 4/item -> + # net = (244 - 4 - 223) x 10,000 = 170,000 for the window (matches the + # 170,000 4h/slot table row; limit_used 10,000 == limit -> no further + # requotes until limit_resets_at). + # + # t7 FIRST SCHEDULED BREAK after 6-10 passes (midpoint 8): every slot is resting + # (buy-open) or closed; per-slot records persisted (slot_state_machine. + # break_resume); take-break 60-240 s (a10); RESUME at a2 (re-open GE) -> a6 + # (check-fills) -> schedule (next pass) -> continue per-slot from persisted + # sub-state. Breaks never strand an offer and never interrupt a collect/place. + # + # t8 STOP: session target (e.g. 900,000 net) -> collect -> complete; capital fell + # below floor -> idle -> complete; 3 consecutive failed passes -> failed. + # + # Rate qualification (typed ceiling vs realized scenarios — finding 5): + # ceiling 149,500 = 598,000 per 4h / 4h — ideal full fills of all 8 slots, + # zero overhead. Realized HIGH 138,000 = 149,500 x 37/40 (37 min resting waits + # + 3 min scan/place/collect per 40-min rotation at the 600 s cadence). + # Realized LOW 74,750 = 149,500 x 0.50 (requote-heavy: every 4h window's + # volume completes only after one ge_cancel + requote cycle — the no-fill + # remainder is refunded and re-quoted on the next window, halving volume). + # The ceiling is NEVER presented as an achieved runtime rate. # ===================================================================================================== risk: @@ -387,17 +506,20 @@ risk: notes: - "Flipping is standard gameplay; the detectable signal is quote churn — keep per-offer pacing humanised, never instant-buy/sell loops." - "The 2% fee (since 2025-05-29) and 5M/item cap make sub-2%-spread staples unprofitable at band edges; re-scan before every rotation." - - "Thin/volatile items: a resting offer can sit for hours — cap quote lifetime to the 4h window and re-quote (ge_cancel + fresh edge) rather than chase." + - "Thin/volatile items: a resting offer can sit for hours — quote lifetime is bounded by the 4h window (lapse -> ge_cancel -> collection box -> ge_collect -> requote at a fresh edge), never by chasing." - "Partial fills are normal; never cancel-and-replace a partially-filled offer mid-window just to churn — the ledger keeps the filled qty attached to the resting offer until full/lapse." - - "Breaks are safe only at resting-offer or post-collect points (see a10); state = GE offers + the per-slot ledger, which survive the pause." + - "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. Reuse a slot only after ge_collect + ledger reconciliation (conservation invariants)." + - "The 4h buy limit is independent of offer life: cancelling does not renew it. A slot at limit_used == limit cannot requote until limit_resets_at (first_fill_ts + 4h)." + - "Breaks are safe only at resting-offer or post-collect points (see a10); state = GE offers + the persisted per-slot record, which survive the pause." sources: - "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h; connected limits; verified 2026-08-27)" - - "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics)" + - "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics; window starts on first purchase)" - "OSRS wiki — Merchanting (flipping = spread exploitation; high trade volume = safety; bulk flipping needs ~20M+ cash; avoid static-price items; offer matching at or better than your price)" - "prices.runescape.wiki/api/v1/osrs/mapping (item ids, members flags, limits — limits cross-checked against Module:Exchange/, all agree)" - "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)" - - "docs/action-grammar.md (ge_cancel verb added 2026-08-28: cancel a pending offer — bought units return to inventory, unspent cash refunds) + schemas/entry.schema.json verb enum; owner lifecycle addendum 2026-08-28 (complete runnable flow, worked example, realized rates)" + - "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6); owner decision 2026-08-28 (preserve capped/floored tax + 4 fixtures)" + - "docs/action-grammar.md (ge_cancel amended + ge_collect verb, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)" verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27 related: diff --git a/docs/action-grammar.md b/docs/action-grammar.md index 2d29fa9..0702188 100644 --- a/docs/action-grammar.md +++ b/docs/action-grammar.md @@ -68,9 +68,9 @@ Legend for the mapping table: | `ge_open` | open GE at a bank | — | — | — | | `ge_buy` | buy `qty` of `item` at offer price, paced | — | — | — | | `ge_sell` | sell `qty` of `item`, confirm | — | — | — | -| `ge_cancel` | cancel a pending offer: bought units return to inventory, unspent cash refunds | — | — | — | +| `ge_cancel` | cancel a pending offer: filled items, refunds, unsold units and unspent coins ALL move to the GE COLLECTION BOX (never directly to inventory); the 4h buy limit is NOT renewed | — | — | — | | `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — | -| `ge_collect` | collect finished GE transactions | — | — | — | +| `ge_collect` | withdraw from the GE collection box to inventory: filled items, refunds, unsold units, sale proceeds; reconcile the per-slot ledger before any slot reuse | — | — | — | | `ge_check_price` | read current buy/sell price of an item | — | — | — | ## F. Combat diff --git a/schemas/entry.schema.json b/schemas/entry.schema.json index 69df417..da01e78 100644 --- a/schemas/entry.schema.json +++ b/schemas/entry.schema.json @@ -43,6 +43,7 @@ "net_per_cycle": { "type": "number" }, "gp_per_hour_low": { "type": "number" }, "gp_per_hour_high": { "type": "number" }, + "ceiling_gp_per_hour": { "type": "number", "exclusiveMinimum": 0 }, "xp_per_hour": { "type": "number" }, "supplies": { "type": "array", @@ -62,6 +63,39 @@ } }, + "slot_state_machine": { + "type": "object", + "required": ["sub_states", "slot_transitions", "slot_transition_whens", + "scheduler_iteration", "quote_age_policy", "persisted_state", + "limit_ledger", "conservation", "break_resume"], + "additionalProperties": false, + "properties": { + "sub_states": { + "type": "array", + "minItems": 8, + "items": { "type": "string" } + }, + "slot_transitions": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "type": "string" } + } + }, + "slot_transition_whens": { + "type": "array", + "minItems": 8, + "items": { "type": "string", "minLength": 8 } + }, + "scheduler_iteration": { "type": "string", "minLength": 10 }, + "quote_age_policy": { "type": "string", "minLength": 10 }, + "persisted_state": { "type": "string", "minLength": 10 }, + "limit_ledger": { "type": "string", "minLength": 10 }, + "conservation": { "type": "string", "minLength": 10 }, + "break_resume": { "type": "string", "minLength": 10 } + } + }, + "best_practice": { "type": "object", "required": ["equipment"], diff --git a/tools/validate.py b/tools/validate.py index 58f20ed..77f979f 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -21,11 +21,25 @@ Runs: NOTE: the generic `ge` tag alone does NOT gate this rule — a training/processing/procurement entry may legitimately carry `ge` while its income/cost model has nothing to do with flip tax. - - Regression fixtures (--selftest): a fractional-tax flipping - entry must FAIL; an above-cap high-value flip with the - correctly capped-tax net must PASS; a non-margin entry carrying - only `ge` plus a non-flip tag must PASS. - 3. Global id uniqueness across the whole codex tree. + - GE-flip lifecycle (flipping/merchanting entries): the 4h buy-limit + ledger (limit_used / limit_resets_at) is DISTINCT from offer + expiry — cancelling never renews the limit; every slot is an + independent asynchronous sub-state machine (canonical set: + free / buy-open / buy-cancelled-awaiting-collect / ready-to-sell / + sell-open / sell-cancelled-awaiting-collect / ready-to-requote / + closed) driven by a global scheduler pass that never touches + unrelated slots; ge_cancel moves filled items/refunds/proceeds/ + unsold units to the GE collection box for a separate ge_collect; + per-slot conservation (quoted == still_offered + collected + + refunded + sold; no negative/duplicated quantities) is provable; + economy.ceiling_gp_per_hour is the TYPED economic ceiling and + 0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling. + - Regression fixtures (--selftest): a fractional-tax flipping + entry must FAIL; an above-cap high-value flip with the + correctly capped-tax net must PASS; a non-margin entry carrying + only `ge` plus a non-flip tag must PASS; plus per-slot scheduler + dispatch, conservation, buy-limit and nesting fixtures. + 3. Global id uniqueness across the whole codex tree. Exit codes: 0 = clean (warnings ok), 1 = validation errors. @@ -97,8 +111,205 @@ def ge_floor_tax_error(rel, data): return None +# --- GE-flip lifecycle gates (owner guidance 2026-08-28, governor 10323) --- +# Canonical per-slot sub-states: each GE slot is an INDEPENDENT asynchronous +# machine driven by a global scheduler pass; a pass never closes or advances +# a slot it did not touch. +SLOT_SUB_STATES = [ + "free", # no offer, nothing held + "buy-open", # resting buy offer (partial fills stay attached) + "buy-cancelled-awaiting-collect", # aborted buy: assets sit in the GE collection box + "ready-to-sell", # units in hand, sell edge positive + "sell-open", # resting sell offer (partial sells tracked) + "sell-cancelled-awaiting-collect", # aborted sell: proceeds + unsold units in the box + "ready-to-requote", # collected units/refunds; requote or abandon + "closed", # slot finished, ledger reconciled, nothing held +] +# Dispatch table: the pure action a slot in a given sub-state needs this pass. +SLOT_DISPATCH = { + "free": "quote", + "buy-open": None, # resting; fill/abort decided by policies + "buy-cancelled-awaiting-collect": "collect", + "ready-to-sell": "sell", + "sell-open": None, # resting; fill/abort decided by policies + "sell-cancelled-awaiting-collect": "collect", + "ready-to-requote": "requote", + "closed": None, +} + + +def scheduler_dispatch(sub_state): + """The pending action for one slot in one scheduler pass, or None. + + A None (resting) slot is left completely untouched — the pass must not + close, advance, or mutate it. This is the property the worked-example + regression proves. + """ + return SLOT_DISPATCH.get(sub_state) + + +def conservation_violations(record): + """Per-slot conservation invariants (governor 10323 finding 4). + + record: dict with item buckets (still_offered, collected_items, + refunded, sold — each a {item: qty} map), quoted ({item: qty}), and cash + buckets (cash_quoted, still_offered_cash, collected_cash, + refunded_cash). Invariants: + - every quantity >= 0; + - per item: quoted == still_offered + collected_items + refunded + sold + (the buckets are a partition of the quote's units — no losses, no + duplicated, no negative quantities); + - physically impossible co-occurrences: units cannot sit in a live + offer AND already be sold or cancelled-refunded (cancel is atomic), + so an item may not appear in both {still_offered, sold} or both + {still_offered, refunded} — partial fills legitimately span + {still_offered, collected_items} until the offer is closed; + - cash: cash_quoted == still_offered_cash + collected_cash + + refunded_cash. + Returns a list of violation strings (empty = conserved). + """ + out = [] + buckets = ["still_offered", "collected_items", "refunded", "sold"] + for b in buckets: + for item in record.get(b, {}): + q = record[b][item] + if q < 0: + out.append(f"item '{item}' bucket {b} is negative ({q})") + quoted = record.get("quoted", {}) + for item, qq in quoted.items(): + if qq < 0: + out.append(f"item '{item}' quoted is negative ({qq})") + total = sum(record.get(b, {}).get(item, 0) for b in buckets) + if total != qq: + out.append( + f"item '{item}' quoted {qq} != still_offered + collected + " + f"refunded + sold ({total})") + # Physically impossible co-occurrences (the buckets are a + # partition of the quote's units; cancel is atomic and units in + # a live offer cannot already be sold/cancelled): + # still_offered + sold — units cannot be both in a live + # offer and already sold + # still_offered + refunded — a cancelled offer has no live + # remainder left in the offer + in_b = {b for b in buckets if item in record.get(b, {})} + for pair in (("still_offered", "sold"), ("still_offered", "refunded")): + if pair[0] in in_b and pair[1] in in_b: + out.append( + f"item '{item}' appears in multiple buckets " + f"({pair[0]} + {pair[1]} — physically impossible: " + f"units cannot be in a live offer and a terminal " + f"bucket at the same time)") + for c in ("cash_quoted", "still_offered_cash", "collected_cash", "refunded_cash"): + if record.get(c, 0) < 0: + out.append(f"{c} is negative ({record[c]})") + if record.get("cash_quoted", 0) != ( + record.get("still_offered_cash", 0) + + record.get("collected_cash", 0) + + record.get("refunded_cash", 0)): + out.append( + f"cash quoted {record.get('cash_quoted')} != still_offered " + f"{record.get('still_offered_cash')} + collected " + f"{record.get('collected_cash')} + refunded {record.get('refunded_cash')}") + return out + + +def buy_capacity_ok(limit_used, limit, qty_requested): + """The 4h buy-limit ledger (governor 10323 finding 1): the limit resets + 4h after the FIRST unit was bought; cancelling an offer does NOT renew + it, so a requote before the reset cannot buy beyond the remaining + capacity. Returns True when limit_used + qty_requested <= limit. + """ + return limit_used + qty_requested <= limit + + +def ge_lifecycle_errors(rel, data): + """GE-flip lifecycle gates (flipping/merchanting entries only): + + - the 4h buy-limit ledger is distinct from offer expiry: the entry must + declare a limit_ledger + an independent quote_age_policy; + - the slot model is per-slot asynchronous: slot_state_machine must + declare exactly the canonical sub-states, valid transitions, a + scheduler_iteration, persisted_state, conservation and break_resume; + - rates: ceiling_gp_per_hour is the typed economic ceiling and + 0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling. + """ + errors = [] + tags = {str(t).lower() for t in data.get("tags", [])} + if not tags & MARGIN_TAGS: + return errors + eco = data.get("economy") or {} + # --- typed ceiling vs realized bounds --- + ceil = eco.get("ceiling_gp_per_hour") + if not isinstance(ceil, (int, float)) or ceil <= 0: + errors.append(f"{rel}: flipping entries must declare " + f"economy.ceiling_gp_per_hour (the typed 4h buy-limit " + f"economic ceiling, distinct from realized rates)") + return errors + low, high = eco.get("gp_per_hour_low"), eco.get("gp_per_hour_high") + for name, val in (("gp_per_hour_low", low), ("gp_per_hour_high", high)): + if not isinstance(val, (int, float)) or val < 0: + errors.append(f"{rel}: {name} must be a non-negative realized " + f"scenario bound") + if isinstance(low, (int, float)) and isinstance(high, (int, float)) \ + and isinstance(ceil, (int, float)): + if not (low <= high <= ceil): + errors.append( + f"{rel}: realized bounds must nest under the ceiling: " + f"0 <= gp_per_hour_low {low} <= gp_per_hour_high {high} " + f"<= ceiling_gp_per_hour {ceil}") + # --- per-slot asynchronous model --- + ssm = data.get("slot_state_machine") or {} + if not isinstance(ssm, dict): + errors.append(f"{rel}: flipping entries must declare a " + f"slot_state_machine block (per-slot asynchronous sub-states)") + return errors + declared = [str(s) for s in ssm.get("sub_states", [])] + if sorted(declared) != sorted(SLOT_SUB_STATES): + errors.append( + f"{rel}: slot_state_machine.sub_states must be exactly the canonical " + f"set: {', '.join(SLOT_SUB_STATES)} (got: {', '.join(declared) or 'none'})") + trans = ssm.get("slot_transitions") or {} + if not isinstance(trans, dict): + errors.append(f"{rel}: slot_state_machine.slot_transitions must be a mapping") + trans = {} + for from_s, tos in trans.items(): + if from_s not in SLOT_SUB_STATES: + errors.append(f"{rel}: slot transition from unknown sub-state '{from_s}'") + for to_s in tos if isinstance(tos, list) else []: + if to_s not in SLOT_SUB_STATES: + errors.append(f"{rel}: slot transition '{from_s}' -> unknown " + f"sub-state '{to_s}'") + for state in SLOT_SUB_STATES: + if state != "closed" and not trans.get(state): + errors.append(f"{rel}: slot sub-state '{state}' has no transitions " + f"(closed is the only sink)") + if trans.get("closed"): + errors.append(f"{rel}: 'closed' is a sink — it must have no outgoing transitions") + whens = [str(w) for w in ssm.get("slot_transition_whens", []) or []] + for w in whens: + arrow = w.split("->") + if len(arrow) != 2 or arrow[0].strip() not in SLOT_SUB_STATES \ + or arrow[1].strip().split(":")[0].strip() not in SLOT_SUB_STATES: + errors.append(f"{rel}: slot_transition_whens entry '{w}' must be " + f"'from -> to: condition' with canonical sub-states") + for field in ("scheduler_iteration", "quote_age_policy", "persisted_state", + "limit_ledger", "conservation", "break_resume"): + if not str(ssm.get(field, "")).strip(): + errors.append(f"{rel}: slot_state_machine.{field} must be declared") + return errors + + def run_selftest(verbose=False): - """Regression fixtures for the GE floor-tax gate; returns error strings.""" + """Regression fixtures for the GE floor-tax + lifecycle gates; returns + error strings.""" + errors = [] + + def check(name, passed): + if verbose: + print(f"SELFTEST {'PASS' if passed else 'FAIL'} {name}") + if not passed: + errors.append(f"selftest: {name}") + fixtures = [ # name, tags, economy, expect_error ("fractional-tax flipping entry", ["flipping"], @@ -118,7 +329,6 @@ def run_selftest(verbose=False): {"income_per_cycle": 300_000_000, "cost_per_cycle": 294_000_000, "net_per_cycle": 1_000_000}, False), ] - errors = [] for name, tags, eco, expect_error in fixtures: rel = f"selftest/{name}" got_error = ge_floor_tax_error(rel, {"tags": tags, "economy": eco}) is not None @@ -130,6 +340,125 @@ def run_selftest(verbose=False): f"selftest: '{name}' expected " f"{'an error' if expect_error else 'no error'} but got " f"{'an error' if got_error else 'no error'}") + + # --- per-slot scheduler dispatch: the worked example as a regression --- + # Governor 10323 finding 3 + owner: reproduce the simultaneous + # partial-buy / full-sell-collect / no-fill-buy case and prove no + # unrelated slot is closed or advanced. + slot_states = {"arrowtips": "buy-open", # partial fill 4,000/10,000 — resting + "iron": "ready-to-sell", # full fill — sell this pass + "cosmic": "buy-open"} # no fill, within quote age — resting + dispatched = {name: scheduler_dispatch(s) for name, s in slot_states.items()} + ok = dispatched == {"arrowtips": None, "iron": "sell", "cosmic": None} + check("worked example: partial-buy + full-sell + no-fill slots dispatch only the iron slot to sell; unrelated slots stay untouched", + ok) + check("resting slots (buy-open) are never closed or advanced by a pass", + dispatched.get("arrowtips") is None and dispatched.get("cosmic") is None) + + # --- conservation invariants (finding 4) --- + # Balanced snapshot mirroring the worked example: 10,000 Rune arrowtips + # quoted, 4,000 filled (units in the player's possession), 6,000 still + # resting in the live offer; plus an independent cash quote (2,230 GP) + # fully committed in live buy offers. + clean_rec = { + "quoted": {"Rune arrowtips": 10}, + "still_offered": {"Rune arrowtips": 6}, + "collected_items": {"Rune arrowtips": 4}, + "refunded": {}, + "sold": {}, + "cash_quoted": 2230, "still_offered_cash": 2230, + "collected_cash": 0, "refunded_cash": 0, + } + check("conservation: a balanced per-slot ledger (partial fill split across still_offered + collected, partition sums to quoted) has zero violations", + conservation_violations(clean_rec) == []) + dup_rec = dict(clean_rec, quoted={"Rune arrowtips": 10}, + still_offered={"Rune arrowtips": 4}, + sold={"Rune arrowtips": 6}) + check("conservation: units both in a live offer AND already sold (impossible pair) is a violation", + any("multiple buckets" in v for v in conservation_violations(dup_rec))) + over_rec = dict(clean_rec, collected_items={"Rune arrowtips": 7}) + check("conservation: over-reported buckets break the quoted partition (sum 13 != quoted 10)", + any("quoted 10 !=" in v for v in conservation_violations(over_rec))) + neg_rec = dict(clean_rec, sold={"Rune arrowtips": -1}) + check("conservation: a negative bucket is a violation", + any("negative" in v for v in conservation_violations(neg_rec))) + cash_rec = dict(clean_rec, collected_cash=900) + check("conservation: cash quoted != still_offered + collected + refunded is a violation", + any("cash quoted" in v for v in conservation_violations(cash_rec))) + + # --- 4h buy-limit ledger vs offer expiry (finding 1) --- + check("buy limit: a requote within the remaining capacity is allowed (4,000 used of 10,000 -> 6,000)", + buy_capacity_ok(limit_used=4000, limit=10000, qty_requested=6000)) + check("buy limit: a requote beyond the remaining capacity is blocked (6,001 > 6,000 remaining)", + not buy_capacity_ok(limit_used=4000, limit=10000, qty_requested=6001)) + check("buy limit: a cancelled offer does NOT renew the limit (10,000 used of 10,000 -> any requote blocked before reset)", + not buy_capacity_ok(limit_used=10000, limit=10000, qty_requested=1)) + + # --- structured lifecycle gates (findings 1-5) --- + margin_tags = ["flipping"] + ssm_ok = { + "sub_states": SLOT_SUB_STATES, + "slot_transitions": { + "free": ["buy-open"], + "buy-open": ["ready-to-sell", "buy-cancelled-awaiting-collect"], + "buy-cancelled-awaiting-collect": ["ready-to-sell", "free"], + "ready-to-sell": ["sell-open"], + "sell-open": ["closed", "sell-cancelled-awaiting-collect"], + "sell-cancelled-awaiting-collect": ["ready-to-requote", "closed"], + "ready-to-requote": ["buy-open", "sell-open", "free"], + "closed": [], + }, + "slot_transition_whens": [ + "free -> buy-open: quote placed", + "buy-open -> ready-to-sell: offer fully filled", + "buy-open -> buy-cancelled-awaiting-collect: abort (stale edge / quote age)", + "buy-cancelled-awaiting-collect -> ready-to-sell: collected, sell edge positive", + "buy-cancelled-awaiting-collect -> free: collected, full refund, slot freed", + "ready-to-sell -> sell-open: sell offer placed", + "sell-open -> closed: fully filled and proceeds collected", + "sell-open -> sell-cancelled-awaiting-collect: abort", + "sell-cancelled-awaiting-collect -> ready-to-requote: collected, requote", + "sell-cancelled-awaiting-collect -> closed: collected, slot closed", + "ready-to-requote -> buy-open: requoted buy", + "ready-to-requote -> sell-open: requoted sell", + "ready-to-requote -> free: abandoned", + ], + "scheduler_iteration": "one pass over all slots; dispatch pending actions only", + "quote_age_policy": "resting offers re-checked every 600 s; stale edges requoted", + "persisted_state": "per-slot record: sub-state + quoted/filled/sold/collected", + "limit_ledger": "limit_used + first_fill_ts + limit_resets_at, independent of offer expiry", + "conservation": "quoted == still_offered + collected + refunded + sold", + "break_resume": "breaks between passes; state persists in the per-slot record", + } + flip_ok = {"tags": margin_tags, "economy": { + "income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17, + "ceiling_gp_per_hour": 149500, "gp_per_hour_low": 102600, + "gp_per_hour_high": 138800}, "slot_state_machine": ssm_ok} + check("flip lifecycle: ceiling + nested realized bounds + canonical slot machine passes", + ge_lifecycle_errors("ok.yaml", flip_ok) == []) + errs = ge_lifecycle_errors("bad.yaml", {"tags": margin_tags, "economy": { + "income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17, + "ceiling_gp_per_hour": 149500, "gp_per_hour_low": 160000, + "gp_per_hour_high": 138800}}) + check("flip lifecycle: realized low above the ceiling is rejected (bounds must nest)", + any("nest" in e for e in errs)) + errs = ge_lifecycle_errors("bad2.yaml", {"tags": margin_tags, "economy": { + "income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17, + "gp_per_hour_low": 0, "gp_per_hour_high": 138800}}) + check("flip lifecycle: missing typed ceiling field is rejected", + any("ceiling_gp_per_hour" in e for e in errs)) + errs = ge_lifecycle_errors("bad3.yaml", dict(flip_ok, slot_state_machine=dict( + ssm_ok, sub_states=["free", "buy-open"]))) + check("flip lifecycle: non-canonical sub-state set is rejected", + any("canonical" in e for e in errs)) + errs = ge_lifecycle_errors("bad4.yaml", dict(flip_ok, slot_state_machine=dict( + ssm_ok, slot_transitions={"free": ["buy-open"], + "buy-open": ["ready-to-sell", + "buy-cancelled-awaiting-collect"]}, + slot_transition_whens=ssm_ok["slot_transition_whens"][:4]))) + check("flip lifecycle: a sub-state without transitions (ready-to-sell missing) is rejected", + any("no transitions" in e for e in errs)) + return errors @@ -213,6 +542,9 @@ def main(): err = ge_floor_tax_error(rel, data) if err: errors.append(err) + # GE-flip lifecycle gates (per-slot async model, 4h limit ledger vs + # offer expiry, typed ceiling vs realized scenario bounds). + errors += ge_lifecycle_errors(rel, data) # 3. staleness / research TODOs if data.get("status") == "verified": From 76f8e5ad27f12b7afb39877233f8c1443ceb5b42 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 14:48:55 -0500 Subject: [PATCH 13/15] fix(flip-candidates): GE asset accounting + offer lifetime per governor 10390 (bounce-fix 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Buy-limit reset NEVER cancels an offer: quote_age_policy now aborts only on a stale scanned edge or explicit strategy reallocation; the 4h window is a capacity clock (limit_ledger), not an offer deadline. Removed every lapse-as-cancel trigger from best_practice, whens, quote_age_policy, a5/a8, worked example, risk, and the a6 verdict. - Completed offers pass through the collection box: new canonical sub-states buy-filled-awaiting-collect / sell-filled-awaiting-collect; a full buy cannot sell and a full sell cannot close before ge_collect (scheduler dispatches collect for all four *-awaiting-collect states). - Works the arrowtips example side-aware: a 4,000/10,000 partial BUY cancel returns 4,000 units + refunded CASH for the unfilled 6,000 — never 'refunded units'; the phantom 10,000-unit withdrawal is gone. - conservation_violations() is now side-aware (buy: quoted = filled + still_open + cancelled_unfilled with acquired items = filled only, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with slot_phase_violations() phase gates and committed negative fixtures: phantom refunded buy units, a full buy sold before collection, a full sell closed before proceeds collection. Existing tax/scheduler/limit fixtures preserved. - grammar (ge_cancel/ge_collect), schema sub_states minItems, entry version 5 + sources updated accordingly. --- codex/economy/flip-candidates.yaml | 344 +++++++++++++++----------- docs/action-grammar.md | 4 +- schemas/entry.schema.json | 2 +- tools/validate.py | 384 +++++++++++++++++++++-------- 4 files changed, 482 insertions(+), 252 deletions(-) diff --git a/codex/economy/flip-candidates.yaml b/codex/economy/flip-candidates.yaml index bf89878..807bbcc 100644 --- a/codex/economy/flip-candidates.yaml +++ b/codex/economy/flip-candidates.yaml @@ -3,7 +3,7 @@ title: GE Flipping (margin trading) domain: economy tags: [flipping, merchanting, ge, money-making, hybrid] status: verified -version: 4 # 4: bounce-fix 5 2026-08-28 (governor comment 10323) — per-slot asynchronous sub-state machine + global scheduler pass; ge_cancel moves everything to the GE collection box (ge_collect + ledger reconciliation before reuse); 4h buy-limit ledger distinct from offer expiry (cancellation never renews the limit); typed ceiling_gp_per_hour vs documented realized scenarios; conservation invariants; breaks with persisted per-slot state. Capped/floored tax model and its 4 selftest fixtures preserved (owner decision 2026-08-28). +version: 5 # 5: bounce-fix 6 2026-08-28 (governor comment 10390) — the 4h buy-limit reset NEVER cancels an offer (offer lifetime = independent quote_age_policy: stale edge or strategy reallocation only); explicit buy-filled-awaiting-collect / sell-filled-awaiting-collect states so completed offers must pass through the GE collection box (ge_collect) before reuse or closing; SIDE-AWARE per-slot ledgers (buy: quoted units = filled + still_open + cancelled_unfilled, acquired items = filled ONLY, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with committed negative fixtures for phantom refunded buy units, a full buy sold before collection, and a full sell closed before proceeds collection. Capped/floored tax model + its 4 selftest fixtures and the asynchronous scheduler behavior preserved (owner decisions 2026-08-28). updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: >- @@ -13,18 +13,23 @@ summary: >- quest requirements — capital breadth and slot discipline are the whole game. Members accounts run 8 GE slots over the full candidate set; F2P accounts get 3 slots and F2P-tradeable rows only (separate F2P path in - profit_drivers). Model (governor 10323, 2026-08-28): every GE slot is an - INDEPENDENT asynchronous sub-state machine (free / buy-open / - buy-cancelled-awaiting-collect / ready-to-sell / sell-open / + profit_drivers). Model (governor 10323 + 10390, 2026-08-28): every GE + slot is an INDEPENDENT asynchronous sub-state machine (free / buy-open / + buy-filled-awaiting-collect / buy-cancelled-awaiting-collect / + ready-to-sell / sell-open / sell-filled-awaiting-collect / sell-cancelled-awaiting-collect / ready-to-requote / closed) driven by a global scheduler pass that acts only on dispatchable slots and never - closes or advances unrelated ones; the 4h buy-limit ledger (limit_used / - first_fill_ts / limit_resets_at / remaining) is DISTINCT from offer - expiry and cancellation NEVER renews the limit; ge_cancel moves filled - items, refunds, unsold units and unspent coins to the GE collection box, - and a separate ge_collect withdraws and reconciles the per-slot ledger - before any reuse; every snapshot satisfies per-slot conservation - (quoted == still_offered + collected + refunded + sold; cash likewise); + closes or advances unrelated ones; completed offers (full buys and full + sells) land in the GE COLLECTION BOX and must pass through ge_collect + + ledger reconciliation before assets are reused or the slot closes; the + 4h buy-limit ledger (limit_used / first_fill_ts / limit_resets_at / + remaining) is DISTINCT from offer expiry — a limit-window reset NEVER + cancels an offer and cancellation NEVER renews the limit; ge_cancel + moves filled items, refunded CASH, unsold units and proceeds to the box, + and every snapshot satisfies SIDE-AWARE per-slot conservation (buy: + quoted units == filled + still_open + cancelled_unfilled with acquired + items == filled only, cash == spent + still_committed + refunded; sell: + quoted == sold + still_open + returned_unsold); economy.ceiling_gp_per_hour is the TYPED economic ceiling and gp_per_hour_low/high are documented realized scenario bounds. Pinned worked example + committed validator fixtures prove the model. @@ -66,8 +71,9 @@ best_practice: why: "The buying rate is restricted to a set quantity every 4 hours per player, and some items share a connected limit with related items — treat those as one bucket when filling slots. The window starts when the FIRST unit of the window is bought - (first_fill_ts) and resets 4h later; cancelling an offer does NOT renew or restart - it." + (first_fill_ts) and resets 4h later. The reset ONLY restores buying capacity — + it never cancels, expires, or otherwise affects a resting offer — and cancelling + an offer does NOT renew or restart the window." source: "OSRS wiki — Grand Exchange (Buy limits; Grand Exchange/Buying limits)" - name: run the path that matches the account's slot set why: "Members get eight GE slots and may trade any row; F2P accounts get three slots @@ -80,17 +86,22 @@ best_practice: global scheduler pass acts only on dispatchable slots (free -> quote, *-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote -> requote) and leaves resting buy-open / sell-open / closed slots untouched — one - slot's cancel or close never advances an unrelated slot." - source: "governor review comment 10323 (2026-08-28), finding 3; docs/action-grammar.md" + slot's cancel or close never advances an unrelated slot. A FULLY FILLED offer + never sells or closes directly: it first enters an explicit + buy-/sell-filled-awaiting-collect state because its assets sit in the GE + collection box until ge_collect (finding 2, governor 10390)." + source: "governor review comment 10323 + 10390 (2026-08-28); OSRS wiki — Grand Exchange / Bank (collection box); docs/action-grammar.md" - name: keep a per-slot offer ledger; ge_cancel -> collection box -> ge_collect -> reconcile, never strand units - why: "Every slot tracks quoted qty -> filled qty -> sold qty -> collected proceeds. - ge_cancel moves filled items, refunds, unsold units and unspent coins to the GE - COLLECTION BOX (never directly to inventory); a separate ge_collect withdraws - them, and the ledger is reconciled (conservation: quoted == still_offered + - collected + refunded + sold, cash likewise) before the slot is reused. The 4h - window lapse or a stale price triggers ge_cancel; the fresh-edge requote happens - only after collection. No items, coins, or GE slots are ever stranded." - source: "OSRS wiki — Grand Exchange (offer mechanics; 4h window); governor review comment 10323 finding 2; docs/action-grammar.md (ge_cancel / ge_collect)" + why: "Every slot tracks its side-aware ledger: a BUY quote partitions units into + filled + still_open + cancelled_unfilled with acquired items = filled ONLY (the + unfilled/cancelled quantity is CASH: spent / still_committed / refunded), and a + SELL quote partitions sold + still_open + returned_unsold. ge_cancel moves + filled items, refunded CASH, unsold units and sale proceeds to the GE COLLECTION + BOX (never directly to inventory); a separate ge_collect withdraws them and the + ledger is reconciled before the slot is reused. The ONLY cancel triggers are a + stale scanned edge (margin no longer positive) or an explicit strategy + reallocation — never the 4h buy-limit reset, which merely restores capacity." + source: "OSRS wiki — Grand Exchange (offer mechanics; collection); Bank (collection interface); governor review comment 10390 findings 1-4; docs/action-grammar.md (ge_cancel / ge_collect)" economy: # Model (documented, reproducible from the snapshot): @@ -118,9 +129,10 @@ economy: # full-fill at their quoted edges; each 40-min rotation is 37 min of # resting waits + 3 min of scan/place/collect actions. # low 74,750 = 149,500 x 0.50 — 'requote-heavy' scenario: every 4h - # window's volume completes only after one ge_cancel + requote cycle - # (the no-fill remainder is refunded to the collection box and - # re-quoted on the next window), halving the window's filled volume. + # window's volume completes only after one stale-edge ge_cancel + + # requote cycle (the no-fill remainder is refunded to the collection + # box; the slot re-quotes when the margin clears again or capacity + # allows), halving the window's filled volume. # Bounds nest: 0 <= 74,750 <= 138,000 <= 149,500 (validator-enforced). item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members) item_id: 44 @@ -150,29 +162,34 @@ economy: - "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored+capped 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h CEILING on 8 slots" - "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h ceiling (alternates: Raw swordfish 45,000, Raw lobster 30,000)" - "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges for members; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells" - - "Realized bounds are DOCUMENTED SCENARIOS with explicit arithmetic (governor 10323 finding 5 — no uncited fill/duty multipliers): HIGH 'resting-fill' 138,000 gp/h is the ceiling scaled by the rotation duty of 37 resting minutes per 40-minute rotation (149,500 x 37/40 = 138,287.5 -> 138,000): scan/place/collect actions cost ~3 min per rotation at the 600s quote-check cadence; LOW 'requote-heavy' 74,750 gp/h assumes every 4h window completes only after one ge_cancel + requote cycle, so the no-fill remainder (refunded to the collection box and re-quoted on the next window) halves the filled volume (149,500 x 0.50). The ceiling 149,500 itself is an economic bound, never an achieved rate." + - "Realized bounds are DOCUMENTED SCENARIOS with explicit arithmetic (governor 10323 finding 5 — no uncited fill/duty multipliers): HIGH 'resting-fill' 138,000 gp/h is the ceiling scaled by the rotation duty of 37 resting minutes per 40-minute rotation (149,500 x 37/40 = 138,287.5 -> 138,000): scan/place/collect actions cost ~3 min per rotation at the 600s quote-check cadence; LOW 'requote-heavy' 74,750 gp/h assumes every 4h window's volume completes only after one stale-edge ge_cancel + requote cycle, so the no-fill remainder (refunded to the collection box and re-quoted when the margin clears or capacity allows) halves the filled volume (149,500 x 0.50). The ceiling 149,500 itself is an economic bound, never an achieved rate. Cancelling never renews the buy limit and the limit reset never cancels an offer (governor 10390 finding 1)." slot_state_machine: - sub_states: [free, buy-open, buy-cancelled-awaiting-collect, ready-to-sell, sell-open, sell-cancelled-awaiting-collect, ready-to-requote, closed] + sub_states: [free, buy-open, buy-filled-awaiting-collect, buy-cancelled-awaiting-collect, ready-to-sell, sell-open, sell-filled-awaiting-collect, sell-cancelled-awaiting-collect, ready-to-requote, closed] slot_transitions: free: [buy-open] - buy-open: [ready-to-sell, buy-cancelled-awaiting-collect] + buy-open: [buy-filled-awaiting-collect, buy-cancelled-awaiting-collect] + buy-filled-awaiting-collect: [ready-to-sell, free] buy-cancelled-awaiting-collect: [ready-to-sell, ready-to-requote, free] ready-to-sell: [sell-open] - sell-open: [closed, sell-cancelled-awaiting-collect] + sell-open: [sell-filled-awaiting-collect, sell-cancelled-awaiting-collect] + sell-filled-awaiting-collect: [closed] sell-cancelled-awaiting-collect: [ready-to-requote, closed] ready-to-requote: [buy-open, sell-open, free] closed: [] slot_transition_whens: - "free -> buy-open: scheduler pass quotes a resting buy at the low edge (qty = min(remaining 4h limit, slot capital, capital available))" - - "buy-open -> ready-to-sell: offer FULLY filled (filled_qty == quoted qty) — units in hand, dispatch sells this pass" - - "buy-open -> buy-cancelled-awaiting-collect: 4h window lapse, or scanned edge stale (margin no longer positive) — ge_cancel: filled items + refunded units + unspent coins ALL move to the GE collection box; the buy limit is NOT renewed by the cancel" - - "buy-cancelled-awaiting-collect -> ready-to-sell: ge_collect withdrawn units to inventory; sell edge positive — sell the units in hand" - - "buy-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; buy margin still positive AND remaining limit > 0 — requote the unfilled balance at the fresh low edge" - - "buy-cancelled-awaiting-collect -> free: ge_collect withdrawn; slot released (margin gone or capital reallocated)" + - "buy-open -> buy-filled-awaiting-collect: offer FULLY filled (filled == quoted) — the units sit in the GE COLLECTION BOX until ge_collect; the slot may NOT sell them directly (governor 10390 finding 2)" + - "buy-open -> buy-cancelled-awaiting-collect: quote_age_policy abort — scanned edge stale (margin no longer positive) or explicit strategy reallocation; NEVER the 4h limit reset. ge_cancel moves filled items + refunded CASH (the unspent remainder) to the collection box; the buy limit is NOT renewed" + - "buy-filled-awaiting-collect -> ready-to-sell: ge_collect withdrawn the units; sell edge positive — dispatch sells" + - "buy-filled-awaiting-collect -> free: ge_collect withdrawn; margin gone or capital reallocated — bank the units, release the slot" + - "buy-cancelled-awaiting-collect -> ready-to-sell: ge_collect withdrawn the bought units; sell edge positive — sell the units in hand" + - "buy-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; buy margin still positive AND remaining limit > 0 — requote at the fresh low edge" + - "buy-cancelled-awaiting-collect -> free: ge_collect withdrawn (full cash refund); slot released (margin gone or capital reallocated)" - "ready-to-sell -> sell-open: scheduler pass quotes a resting sell at the high edge for the units in hand" - - "sell-open -> closed: sell offer fully filled, proceeds collected, ledger reconciled — slot freed" - - "sell-open -> sell-cancelled-awaiting-collect: 4h window lapse or stale edge — ge_cancel: unsold units return to the GE collection box (never to inventory)" + - "sell-open -> sell-filled-awaiting-collect: sell offer FULLY filled (sold == quoted) — the proceeds sit in the GE COLLECTION BOX until ge_collect; the slot may NOT close directly (governor 10390 finding 2)" + - "sell-open -> sell-cancelled-awaiting-collect: quote_age_policy abort — scanned edge stale or strategy reallocation; ge_cancel: unsold units + proceeds received so far move to the collection box (never to inventory)" + - "sell-filled-awaiting-collect -> closed: ge_collect withdrawn the proceeds; ledger reconciled — slot freed" - "sell-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; requote the sell at the fresh high edge or the current band" - "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; slot closed (exit taken)" - "ready-to-requote -> buy-open: requoted buy placed (buy_capacity_ok: limit_used + qty <= limit until limit_resets_at)" @@ -180,53 +197,71 @@ slot_state_machine: - "ready-to-requote -> free: slot abandoned and released" scheduler_iteration: >- One pass over every slot exactly once, at the 600 s quote-check cadence. - Only dispatchable slots act: free -> quote (ge_buy), buy-cancelled- - awaiting-collect / sell-cancelled-awaiting-collect -> ge_collect, - ready-to-sell -> sell (ge_sell), ready-to-requote -> requote. Resting - buy-open / sell-open / closed slots produce NO action and are never - closed, advanced, re-quoted or touched by the pass. A pass therefore - advances only the slots whose sub-state is dispatchable. + Only dispatchable slots act: free -> quote (ge_buy), buy-filled- + awaiting-collect / buy-cancelled-awaiting-collect / + sell-filled-awaiting-collect / sell-cancelled-awaiting-collect -> + ge_collect, ready-to-sell -> sell (ge_sell), ready-to-requote -> + requote. Resting buy-open / sell-open / closed slots produce NO action + and are never closed, advanced, re-quoted or touched by the pass. A + pass therefore advances only the slots whose sub-state is dispatchable, + and a completed offer always passes through its *-filled-awaiting- + collect state (ge_collect) before its assets are reused. quote_age_policy: >- - INDEPENDENT of the 4h buy-limit ledger: resting offers are re-checked at - the 600 s cadence; an offer is aborted ONLY on a stale band edge - (scanned margin no longer positive) or the 4h window lapse — never - merely because it has been resting. An offer that still clears the - scanned margin keeps resting until it fills or the window lapses. + Offer lifetime is INDEPENDENT of the 4h buy-limit ledger. Resting + offers are re-checked at the 600 s cadence and kept resting while the + scanned margin stays positive — a profitable resting offer remains + open regardless of limit_resets_at (a limit-window reset only restores + buy capacity; it never cancels, expires, or implies cancelling an + offer). An offer is aborted ONLY on a stale band edge (scanned margin + no longer positive) or an explicit strategy reallocation (slot freed + for a better candidate / stop condition). No-fill alone never aborts an + offer whose edge still clears the margin. persisted_state: >- Per-slot record persisted between passes and across breaks: sub-state, - item, quoted qty, still_offered qty, collected qty, refunded qty, sold - qty, cash_quoted, still_offered_cash, collected_cash, refunded_cash, - limit_used, first_fill_ts, limit_resets_at, quote placed_at, last - check_ts. The GE offers themselves persist server-side; the record is - the agent's copy of truth for the scheduler and conservation checks. + item, quoted qty, and SIDE-AWARE buckets — buy side: filled, still_open, + cancelled_unfilled, cash_quoted, spent, still_committed, refunded; + sell side: sold, still_open, returned_unsold, proceeds_in_box, + cash_collected; plus limit_used, first_fill_ts, limit_resets_at, + quote placed_at, last check_ts. The GE offers themselves persist + server-side; the record is the agent's copy of truth for the scheduler + and conservation checks. limit_ledger: >- Per item: limit_used (units bought so far in the current 4h window), first_fill_ts (when the FIRST unit of the window was bought), limit_resets_at = first_fill_ts + 4h, remaining = limit - limit_used. - The ledger is DISTINCT from offer expiry: offers rest independently of - the window, and cancelling never renews or restarts it — a requote + The ledger controls ONLY the buying rate: it is DISTINCT from offer + expiry (offers rest independently of the window), cancelling never + renews or restarts it, and the reset never cancels an offer — a requote before limit_resets_at may buy at most remaining (buy_capacity_ok: limit_used + qty <= limit). A slot with limit_used == limit cannot - requote until limit_resets_at. + place a new or requoted buy until limit_resets_at; its resting offer + is unaffected. conservation: >- - Per slot at every snapshot: quoted == still_offered + collected_items + - refunded + sold (the buckets partition the quote's units — no losses, - no duplication, no negatives); cash_quoted == still_offered_cash + - collected_cash + refunded_cash. Impossible pairs (units in a live offer - AND a terminal bucket) are violations: still_offered + sold, and - still_offered + refunded (cancel is atomic). ge_cancel moves every - filled / refunded / unsold unit and coin to the GE collection box; - ge_collect withdraws them and reconciles the ledger BEFORE any slot - reuse. Committed validator fixtures prove the invariants and the - scheduler dispatch. + SIDE-AWARE per-slot invariants at every snapshot (governor 10390 + finding 4). BUY side: quoted units == filled + still_open + + cancelled_unfilled per item, and ACQUIRED ITEMS == filled ONLY — the + unfilled/cancelled quantity is a CASH refund, never refunded units; + cash_quoted == spent + still_committed + refunded. SELL side: quoted + units == sold + still_open + returned_unsold per item (a partial sell + legitimately spans sold + still_open). No negative or duplicated + quantities; impossible pairs (still_open + cancelled_unfilled on a + buy; still_open + returned_unsold on a sell) are violations. Phase + gates: a buy-filled-awaiting-collect slot has NOTHING sold yet (a full + buy cannot sell before ge_collect) and a sell-filled-awaiting-collect + slot holds its proceeds in the box; a 'closed' slot has no uncollected + proceeds or cancelled/returned units. Committed validator fixtures — + including the negative fixtures (phantom refunded buy units; a full + buy sold before collection; a full sell closed before proceeds + collection) — prove the invariants and the scheduler dispatch. break_resume: >- - Breaks are taken only when every slot is resting (buy-open / sell-open) - or collected (closed) — never mid-collect or mid-place. Full state + Breaks are taken only when no slot is dispatchable: every slot is + resting (buy-open / sell-open), free, or collected (closed) — never + mid-collect, mid-place, or in an *-awaiting-collect state. Full state (offers server-side + the persisted per-slot record) survives the pause; resume by re-opening the GE, re-checking offers, re-running the scheduler pass and continuing each slot from its persisted sub-state. - Slots and slots only: the 8-slot members / 3-slot F2P activity is never - downgraded to single-slot work. + Slots and slots only: the 8-slot members / 3-slot F2P activity is + never downgraded to single-slot work. state_machine: initial: idle @@ -342,7 +377,7 @@ actions: verb: sleep params: delay: [6000, 12000] - until: "ge_offers_done changes or a fill event or the 600s cadence or 4h window lapse" + until: "ge_offers_done changes or a fill event or the 600s cadence (a resting offer keeps resting; the 4h limit reset plays no role in offer lifetime)" on_fail: retry anti_ban: delay: [300, 900] @@ -353,8 +388,8 @@ actions: target: "all quoted slots" params: phase: "per-slot: buy-open (buy outstanding) | sell-open (sell outstanding)" - per_slot: "still_offered vs quoted for every slot (partial fills stay attached to the resting offer)" - verdict: "full | partial | none | lapse | stale-edge" + per_slot: "side-aware buckets per slot — buy: filled (units in the collection box) / still_open (resting offer) / cancelled_unfilled; sell: sold (proceeds in the box) / still_open / returned_unsold — partial fills keep the offer resting with filled units held in the box" + verdict: "full | partial | none | stale-edge # 'lapse' is NOT a verdict: the 4h window is a capacity clock, not an offer deadline" on_fail: retry - id: a7 @@ -362,7 +397,7 @@ actions: verb: ge_sell target: grand-exchange params: - item: "per ready-to-sell slot (units in hand after full fill or ge_collect)" + item: "per ready-to-sell slot (units in hand AFTER ge_collect — a fully filled buy must first pass through buy-filled-awaiting-collect -> ge_collect)" qty: "units in hand for that slot" price: "high edge (rounded up)" ledger: "record sold qty per slot; sub-state -> sell-open" @@ -373,11 +408,11 @@ actions: - id: a8 step: cancel-to-box verb: ge_cancel - target: "lapsed or stale slot offers (current phase)" + target: "slot offers aborted by quote_age_policy (stale edge / strategy reallocation)" params: - offers: "the slots that hit the 4h window lapse or a stale edge" - moved: "filled items + refunded units + unsold units + unspent coins ALL move to the GE COLLECTION BOX (never directly to inventory)" - limit: "the 4h buy limit is NOT renewed or restarted by the cancel (limit_used stays)" + offers: "slots whose scanned band edge no longer clears the margin (stale), or slots explicitly reallocated by the strategy — NEVER offers whose 4h limit-window merely reset (a reset restores buy capacity only; quote_age_policy)" + moved: "BUY cancel: filled items + refunded CASH (the unspent remainder) -> GE COLLECTION BOX; SELL cancel: unsold units + proceeds received so far -> GE COLLECTION BOX (never directly to inventory)" + limit: "the 4h buy limit is NOT renewed or restarted by the cancel (limit_used stays); the next reset does not cancel anything either" sub_state: "buy-open -> buy-cancelled-awaiting-collect; sell-open -> sell-cancelled-awaiting-collect" on_fail: retry @@ -386,9 +421,9 @@ actions: verb: ge_collect target: grand-exchange params: - withdraw: "from the GE collection box: filled items, refunds, unsold units, sale proceeds" - reconcile: "per-slot ledger conservation check (quoted == still_offered + collected + refunded + sold; cash likewise) BEFORE any slot reuse" - decide: "ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release)" + withdraw: "from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds — per slot" + reconcile: "SIDE-AWARE per-slot ledger conservation check BEFORE any slot reuse — buy: quoted == filled + still_open + cancelled_unfilled and cash_quoted == spent + still_committed + refunded; sell: quoted == sold + still_open + returned_unsold" + decide: "buy-filled/buy-cancelled -> ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release); sell-filled/sell-cancelled -> closed | ready-to-requote" log: "RESULT item, qty, net gp per slot (floored+capped tax)" on_fail: alert anti_ban: @@ -416,80 +451,100 @@ run_control: - "no net-positive candidates on 3 consecutive scans" - "3 consecutive failed/aborted passes (client issue — do not loop)" - # ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28; slot-machine walk per governor 10323) ================= + # ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28; slot-machine walk per governor 10323, corrected per 10390) ================= # Members account, 8 GE slots, capital 22,500,000 gp. Snapshot prices 2026-08-27 # (buy/sell = low/high edge; tax = min(floor(0.02 x sell), 5,000,000)). # The run below walks THREE slots in detail (arrowtips / iron / cosmic) exactly as # the committed scheduler-dispatch + conservation fixtures; the other five slots # behave identically to the iron slot and are omitted for brevity. + # Ledgers are SIDE-AWARE throughout: a BUY's acquired items are `filled` ONLY; + # unfilled/cancelled quantity is CASH (spent / still_committed / refunded). # # t0 idle -> scan -> schedule (pass 1): all slots free. Scheduler dispatch: # free -> quote. Slot ledger after the pass (conservation holds): # arrowtips: buy-open, quoted 10,000 @ 223; cash_quoted 2,230,000 - # iron: buy-open, quoted 10,000 @ 145; cash_quoted 1,450,000 (limit_ledger entry: limit_used 0, first_fill_ts -, limit_resets_at -) + # = spent 0 + still_committed 2,230,000 + refunded 0 + # iron: buy-open, quoted 10,000 @ 145; cash_quoted 1,450,000 # cosmic: buy-open, quoted 18,000 @ 122; cash_quoted 2,196,000 - # Buckets: still_offered == quoted for every slot; collected/refunded/sold = 0. + # Unit buckets: quoted == still_open for every slot; filled/cancelled = 0. # # t1 wait, ~18 min: arrowtips PARTIAL fill 4,000/10,000 (a6 verdict PARTIAL). - # Ledger: arrowtips quoted 10,000 == still_offered 6,000 + collected_items 4,000 - # (the 4,000 units sit in the player's possession — GE box — attached to the - # resting offer; nothing is stranded). limit_ledger: limit_used 4,000, - # first_fill_ts now, limit_resets_at = first_fill_ts + 4h; remaining 6,000. - # Cosmic: no fill; iron: FULL fill 10,000 -> ready-to-sell. + # BUY ledger: quoted 10,000 == filled 4,000 + still_open 6,000 + cancelled 0. + # Acquired items = 4,000 ONLY; they sit in the GE collection box (collectible + # at any bank), while the offer keeps resting with 6,000 still open. Cash: + # cash_quoted 2,230,000 == spent 892,000 + still_committed 1,338,000. + # limit_ledger: first_fill_ts now, limit_used 4,000, limit_resets_at = +4h, + # remaining 6,000 (window CAPACITY — not an offer deadline). + # Iron: FULL fill 10,000/10,000 -> buy-filled-awaiting-collect (units sit in + # the box until ge_collect; the slot cannot sell them directly — finding 2). + # Iron cash: spent 1,450,000. Cosmic: no fill; offer keeps resting (edge + # positive, quote age within policy — finding 1: no-fill alone never aborts). # # t2 cadence -> schedule (pass 2): dispatch ONLY the dispatchable slot: - # iron ready-to-sell -> sell-open (a7): 10,000 @ 157 (high edge). - # arrowtips buy-open -> NO ACTION (resting, quote age within policy). - # cosmic buy-open -> NO ACTION (resting, no fill, quote age within policy). + # iron buy-filled-awaiting-collect -> ge_collect (a9): withdraw 10,000 units; + # ledger reconciled (quoted 10,000 == filled 10,000; cash spent 1,450,000, + # no refund); sell edge positive -> ready-to-sell. + # arrowtips buy-open -> NO ACTION (resting, partial fill still open). + # cosmic buy-open -> NO ACTION (resting, no fill, edge positive). # => the pass never closes or advances unrelated slots (regression fixture). - # Iron ledger: quoted 10,000 == still_offered 0 + sold 10,000 (buckets move on fill). # - # t3 iron sell-open FULL fill -> collect: proceeds 1,570,000; tax floor(157 x 0.02) - # = 3/item -> net = (157 - 3 - 145) x 10,000 = 90,000. ge_collect (a9): - # cash withdrawn, ledger reconciled, sub-state -> closed, slot freed. - # Conservation: quoted 10,000 == sold 10,000 (all other buckets 0). + # t3 pass 3: iron ready-to-sell -> sell-open (a7): 10,000 @ 157 (high edge). # - # t4 LAPSE / stale-edge handling (a8): at the 4h window edge, cosmic still has NO - # fill -> ge_cancel: the offer's cash 2,196,000 moves to the COLLECTION BOX as a - # refund. Ledger: cash_quoted 2,196,000 == still_offered_cash 0 + refunded_cash - # 2,196,000 (collected_cash 0). Sub-state -> buy-cancelled-awaiting-collect. - # arrowtips reaches limit_resets_at with 6,000 still offered -> ge_cancel: - # filled 4,000 (already in player possession) + refunded 6,000 units + unspent - # cash 6,000 x 223 = 1,338,000 ALL move to the collection box. Ledger: - # quoted 10,000 == collected_items 4,000 + refunded 6,000; limit_used STAYS - # 4,000 (the cancel does NOT renew the limit; remaining 6,000 stands). - # Sub-state -> buy-cancelled-awaiting-collect for both slots. + # t4 iron sell-open FULL fill -> sell-filled-awaiting-collect: proceeds + # 1,570,000 sit in the GE collection box until ge_collect. SELL ledger: + # quoted 10,000 == sold 10,000 + still_open 0 + returned_unsold 0. # - # t5 cadence -> schedule (pass 3): dispatch the awaiting-collect slots -> ge_collect - # (a9). Withdraw + reconcile: - # arrowtips: units 10,000 (4,000 filled + 6,000 refund) -> inventory. - # Ledger: quoted 10,000 == collected_items 10,000 (refunded absorbed into - # collected on withdrawal). Buy margin still positive AND remaining limit - # 6,000 > 0. DECIDE (a9): ready-to-requote -> requote buy 6,000 @ 223 - # (buy_capacity_ok: limit_used 4,000 + 6,000 = 10,000 <= 10,000 — allowed; - # a 6,001-unit requote would be BLOCKED until limit_resets_at — fixture). - # -> buy-open on the fresh edge. - # cosmic: cash 2,196,000 withdrawn (refunded_cash -> collected_cash 2,196,000). - # Margin still positive, limit_used 0 -> ready-to-requote -> buy-open 18,000 - # @ 122 (full capacity available). - # iron: already closed (free). - # => ONLY the awaiting-collect slots acted; resting/closed slots untouched. + # t5 pass 4: iron sell-filled-awaiting-collect -> ge_collect (a9): withdraw + # 1,570,000; tax floor(157 x 0.02) = 3/item -> net (157 - 3 - 145) x 10,000 + # = 90,000. proceeds_in_box -> cash_collected; ledger reconciled; + # sub-state -> closed, slot freed. Conservation: quoted 10,000 == sold 10,000. # - # t6 Scheduler continues; arrowtips requote buy 6,000 fills -> ready-to-sell -> - # sell-open 10,000 @ 244 (4,000 in hand from the t1 fill + 6,000 from the - # requote) -> fills -> collect (a9): tax floor(244 x 0.02) = 4/item -> - # net = (244 - 4 - 223) x 10,000 = 170,000 for the window (matches the - # 170,000 4h/slot table row; limit_used 10,000 == limit -> no further - # requotes until limit_resets_at). + # t6 LIMIT RESET, NOT AN OFFER EVENT (finding 1): arrowtips reaches + # limit_resets_at with 6,000 still open on the resting offer. The window + # reset restores buying capacity ONLY — the offer KEEPS RESTING at its edge; + # nothing is cancelled and the 4,000 filled units stay collectible in the + # box. Cosmic likewise unaffected. # - # t7 FIRST SCHEDULED BREAK after 6-10 passes (midpoint 8): every slot is resting - # (buy-open) or closed; per-slot records persisted (slot_state_machine. - # break_resume); take-break 60-240 s (a10); RESUME at a2 (re-open GE) -> a6 - # (check-fills) -> schedule (next pass) -> continue per-slot from persisted - # sub-state. Breaks never strand an offer and never interrupt a collect/place. + # t7 arrowtips offer FULLY fills the remaining 6,000 -> buy-filled-awaiting- + # collect -> pass 5 ge_collect: withdraw 10,000 units (4,000 from the t1 + # partial + 6,000 from the t7 fills — the units are what the offer actually + # bought; NO phantom "refunded units"). BUY ledger: quoted 10,000 == filled + # 10,000; cash 2,230,000 == spent 2,230,000 (still_committed 0 — a full fill + # spends its whole quote). Sell edge positive -> ready-to-sell. + # limit_ledger: window 1 used 4,000 (t1); window 2 (post-reset) used 6,000. # - # t8 STOP: session target (e.g. 900,000 net) -> collect -> complete; capital fell - # below floor -> idle -> complete; 3 consecutive failed passes -> failed. + # t8 pass 6: arrowtips ready-to-sell -> sell-open 10,000 @ 244 -> full fill -> + # sell-filled-awaiting-collect -> pass 7 ge_collect: proceeds 2,440,000; + # tax floor(244 x 0.02) = 4/item -> net (244 - 4 - 223) x 10,000 = 170,000 + # (matches the 170,000 4h/slot table row) -> closed. + # + # t9 STALE-EDGE recovery (cosmic, finding 1 cancel trigger): at a cadence check + # the cosmic LOW edge has risen 122 -> 126: net 128 - floor(128 x 0.02)=2 - + # 126 = 0 -> margin no longer positive -> quote_age_policy aborts the offer + # (ge_cancel, a8): ZERO units were filled, so the collection box receives + # refunded CASH 2,196,000 and NO items. BUY ledger: quoted 18,000 == filled 0 + # + still_open 0 + cancelled_unfilled 18,000; cash 2,196,000 == spent 0 + + # still_committed 0 + refunded 2,196,000. Sub-state -> buy-cancelled- + # awaiting-collect. (The 18,000 never-purchased units are cash, never + # "refunded units" — the phantom-items fixture.) + # + # t10 pass 8: cosmic buy-cancelled-awaiting-collect -> ge_collect: withdraw + # refunded CASH 2,196,000; ledger reconciled (refunded -> collected). The + # fresh-edge margin is still 0, so the slot is NOT re-quoted: ready-to- + # requote requires a positive margin — decide free, capital reallocated to a + # candidate that still clears (e.g. magic logs); a requote in a later window + # is governed by buy_capacity_ok as usual (the cancel did NOT renew the + # window, and the next reset does not cancel anything). + # + # t11 FIRST SCHEDULED BREAK after 6-10 passes (midpoint 8): every slot is + # resting (buy-open / sell-open), free, or collected (closed) — nothing + # dispatchable; per-slot records persisted (slot_state_machine.break_resume); + # take-break 60-240 s (a10); RESUME at a2 (re-open GE) -> a6 (check-fills) + # -> schedule (next pass) -> continue per-slot from persisted sub-state. + # Breaks never strand an offer and never interrupt a collect/place. + # + # t12 STOP: session target (e.g. 900,000 net) -> collect -> complete; capital + # fell below floor -> idle -> complete; 3 consecutive failed passes -> failed. # # Rate qualification (typed ceiling vs realized scenarios — finding 5): # ceiling 149,500 = 598,000 per 4h / 4h — ideal full fills of all 8 slots, @@ -506,20 +561,23 @@ risk: notes: - "Flipping is standard gameplay; the detectable signal is quote churn — keep per-offer pacing humanised, never instant-buy/sell loops." - "The 2% fee (since 2025-05-29) and 5M/item cap make sub-2%-spread staples unprofitable at band edges; re-scan before every rotation." - - "Thin/volatile items: a resting offer can sit for hours — quote lifetime is bounded by the 4h window (lapse -> ge_cancel -> collection box -> ge_collect -> requote at a fresh edge), never by chasing." - - "Partial fills are normal; never cancel-and-replace a partially-filled offer mid-window just to churn — the ledger keeps the filled qty attached to the resting offer until full/lapse." - - "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. Reuse a slot only after ge_collect + ledger reconciliation (conservation invariants)." - - "The 4h buy limit is independent of offer life: cancelling does not renew it. A slot at limit_used == limit cannot requote until limit_resets_at (first_fill_ts + 4h)." - - "Breaks are safe only at resting-offer or post-collect points (see a10); state = GE offers + the persisted per-slot record, which survive the pause." + - "Thin/volatile items: a resting offer can sit for hours — offer lifetime is governed by quote_age_policy (abort only on a stale scanned edge or strategy reallocation), NEVER by the 4h buy-limit window. A limit reset merely restores buy capacity; it cancels nothing." + - "Partial fills are normal: filled units of a partial buy sit in the GE collection box (collectible at any bank) while the offer keeps resting; never cancel-and-replace a partially-filled offer mid-window just to churn — the side-aware ledger keeps the filled qty attached until the offer fully fills or is aborted per quote_age_policy." + - "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. BUY cancels return filled items + refunded CASH (the unfilled quantity is cash, never 'refunded units'); SELL cancels return unsold units + proceeds received. Reuse a slot only after ge_collect + side-aware ledger reconciliation (conservation invariants)." + - "The 4h buy limit is independent of offer life: cancelling does not renew it and the reset does not cancel offers. A slot at limit_used == limit cannot place a new/requoted BUY until limit_resets_at (first_fill_ts + 4h); its resting offer is unaffected." + - "Breaks are safe only when no slot is dispatchable (resting offers or post-collect points, see a10 and break_resume); state = GE offers + the persisted per-slot record, which survive the pause." sources: - - "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h; connected limits; verified 2026-08-27)" - - "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics; window starts on first purchase)" + - "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h window starting on the FIRST purchase of the window; connected limits; verified 2026-08-27)" + - "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics; the limit controls how many units may be bought and resets 4h after the first unit is bought; outstanding offers remain active until filled or explicitly aborted — a limit reset is NOT offer expiry)" + - "OSRS wiki — Grand Exchange + Bank (collection interface): items/coins from full or partial trades — including aborted offers — remain in the collection box until collected; partially completed trades can be collected at any bank" - "OSRS wiki — Merchanting (flipping = spread exploitation; high trade volume = safety; bulk flipping needs ~20M+ cash; avoid static-price items; offer matching at or better than your price)" - "prices.runescape.wiki/api/v1/osrs/mapping (item ids, members flags, limits — limits cross-checked against Module:Exchange/, all agree)" - "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)" - - "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6); owner decision 2026-08-28 (preserve capped/floored tax + 4 fixtures)" - - "docs/action-grammar.md (ge_cancel amended + ge_collect verb, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)" + - "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6)" + - "governor review comment 10390 on PR #7 (2026-08-28): the 4h limit reset never cancels offers — resting offers remain open while the margin is positive (finding 1); filled offers must pass through explicit *-filled-awaiting-collect states and ge_collect before reuse/close (finding 2); side-aware ledgers kill the phantom-units error (finding 3); side-aware conservation with negative fixtures, including sale-before-collection and close-before-proceeds-collection (finding 4)" + - "owner decisions 2026-08-28 (guidance comments 10264, 10271, 10334): capped/floored global tax formula; complete lifecycle contract; authorisation to keep repairing this same branch/PR" + - "docs/action-grammar.md (ge_cancel/ge_collect rows, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)" verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27 related: diff --git a/docs/action-grammar.md b/docs/action-grammar.md index 0702188..ef23cd6 100644 --- a/docs/action-grammar.md +++ b/docs/action-grammar.md @@ -68,9 +68,9 @@ Legend for the mapping table: | `ge_open` | open GE at a bank | — | — | — | | `ge_buy` | buy `qty` of `item` at offer price, paced | — | — | — | | `ge_sell` | sell `qty` of `item`, confirm | — | — | — | -| `ge_cancel` | cancel a pending offer: filled items, refunds, unsold units and unspent coins ALL move to the GE COLLECTION BOX (never directly to inventory); the 4h buy limit is NOT renewed | — | — | — | +| `ge_cancel` | abort an offer (quote_age_policy only — stale edge or strategy reallocation, never a 4h limit-window reset): BUY — filled items + refunded CASH (unspent remainder) enter the GE COLLECTION BOX; SELL — unsold units + proceeds received so far enter the box; NEVER directly to inventory; the 4h buy limit is NOT renewed | — | — | — | | `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — | -| `ge_collect` | withdraw from the GE collection box to inventory: filled items, refunds, unsold units, sale proceeds; reconcile the per-slot ledger before any slot reuse | — | — | — | +| `ge_collect` | withdraw from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds; reconcile the SIDE-AWARE per-slot ledger (buy: quoted = filled + still_open + cancelled_unfilled; sell: quoted = sold + still_open + returned_unsold) before any slot reuse | — | — | — | | `ge_check_price` | read current buy/sell price of an item | — | — | — | ## F. Combat diff --git a/schemas/entry.schema.json b/schemas/entry.schema.json index da01e78..ac2d065 100644 --- a/schemas/entry.schema.json +++ b/schemas/entry.schema.json @@ -72,7 +72,7 @@ "properties": { "sub_states": { "type": "array", - "minItems": 8, + "minItems": 10, "items": { "type": "string" } }, "slot_transitions": { diff --git a/tools/validate.py b/tools/validate.py index 77f979f..c11d0a4 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -23,15 +23,21 @@ Runs: `ge` while its income/cost model has nothing to do with flip tax. - GE-flip lifecycle (flipping/merchanting entries): the 4h buy-limit ledger (limit_used / limit_resets_at) is DISTINCT from offer - expiry — cancelling never renews the limit; every slot is an - independent asynchronous sub-state machine (canonical set: - free / buy-open / buy-cancelled-awaiting-collect / ready-to-sell / - sell-open / sell-cancelled-awaiting-collect / ready-to-requote / - closed) driven by a global scheduler pass that never touches - unrelated slots; ge_cancel moves filled items/refunds/proceeds/ - unsold units to the GE collection box for a separate ge_collect; - per-slot conservation (quoted == still_offered + collected + - refunded + sold; no negative/duplicated quantities) is provable; + expiry — a limit-window reset never cancels an offer and cancelling + never renews the limit; every slot is an independent asynchronous + sub-state machine (canonical set: free / buy-open / + buy-filled-awaiting-collect / buy-cancelled-awaiting-collect / + ready-to-sell / sell-open / sell-filled-awaiting-collect / + sell-cancelled-awaiting-collect / ready-to-requote / closed) driven + by a global scheduler pass that never touches unrelated slots; + ge_cancel moves filled items, refunded CASH, unsold units and + proceeds to the GE collection box for a separate ge_collect; + per-slot conservation is SIDE-AWARE (buy: quoted units = + filled + still_open + cancelled_unfilled and acquired items = + filled ONLY — the unfilled/cancelled quantity is cash, never + refunded units; sell: quoted = sold + still_open + returned_unsold) + with phase gates (a full buy cannot sell before collection, a full + sell cannot close before proceeds collection); economy.ceiling_gp_per_hour is the TYPED economic ceiling and 0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling. - Regression fixtures (--selftest): a fractional-tax flipping @@ -111,16 +117,20 @@ def ge_floor_tax_error(rel, data): return None -# --- GE-flip lifecycle gates (owner guidance 2026-08-28, governor 10323) --- +# --- GE-flip lifecycle gates (owner guidance 2026-08-28, governor 10323 + 10390) --- # Canonical per-slot sub-states: each GE slot is an INDEPENDENT asynchronous # machine driven by a global scheduler pass; a pass never closes or advances -# a slot it did not touch. +# a slot it did not touch. A COMPLETED offer leaves its assets in the GE +# collection box until ge_collect: fully filled buys and sells each get an +# explicit *-filled-awaiting-collect state (governor 10390 finding 2). SLOT_SUB_STATES = [ "free", # no offer, nothing held - "buy-open", # resting buy offer (partial fills stay attached) - "buy-cancelled-awaiting-collect", # aborted buy: assets sit in the GE collection box - "ready-to-sell", # units in hand, sell edge positive + "buy-open", # resting buy offer (partial fills keep resting) + "buy-filled-awaiting-collect", # buy FULLY filled — units sit in the GE collection box until ge_collect + "buy-cancelled-awaiting-collect", # aborted buy: bought units + refunded CASH in the box + "ready-to-sell", # units in hand (ge_collect'ed), sell edge positive "sell-open", # resting sell offer (partial sells tracked) + "sell-filled-awaiting-collect", # sell FULLY filled — proceeds sit in the box until ge_collect "sell-cancelled-awaiting-collect", # aborted sell: proceeds + unsold units in the box "ready-to-requote", # collected units/refunds; requote or abandon "closed", # slot finished, ledger reconciled, nothing held @@ -129,9 +139,11 @@ SLOT_SUB_STATES = [ SLOT_DISPATCH = { "free": "quote", "buy-open": None, # resting; fill/abort decided by policies + "buy-filled-awaiting-collect": "collect", "buy-cancelled-awaiting-collect": "collect", "ready-to-sell": "sell", "sell-open": None, # resting; fill/abort decided by policies + "sell-filled-awaiting-collect": "collect", "sell-cancelled-awaiting-collect": "collect", "ready-to-requote": "requote", "closed": None, @@ -148,28 +160,76 @@ def scheduler_dispatch(sub_state): return SLOT_DISPATCH.get(sub_state) -def conservation_violations(record): - """Per-slot conservation invariants (governor 10323 finding 4). +# Side-aware GE bucket names (governor 10390 finding 4). +# BUY: the player commits CASH; acquired items are `filled` ONLY — the +# unfilled/cancelled quantity is an unspent-CASH refund, never "refunded +# units". SELL: the player holds units; sold units yield proceeds. +BUY_UNIT_BUCKETS = ["filled", "still_open", "cancelled_unfilled"] +SELL_UNIT_BUCKETS = ["sold", "still_open", "returned_unsold"] +# Physically impossible co-occurrences per side (cancel is atomic: a live +# offer has no remainder once cancelled). +IMPOSSIBLE_UNIT_PAIRS = { + "buy": [("still_open", "cancelled_unfilled")], + "sell": [("still_open", "returned_unsold")], +} - record: dict with item buckets (still_offered, collected_items, - refunded, sold — each a {item: qty} map), quoted ({item: qty}), and cash - buckets (cash_quoted, still_offered_cash, collected_cash, - refunded_cash). Invariants: - - every quantity >= 0; - - per item: quoted == still_offered + collected_items + refunded + sold - (the buckets are a partition of the quote's units — no losses, no - duplicated, no negative quantities); - - physically impossible co-occurrences: units cannot sit in a live - offer AND already be sold or cancelled-refunded (cancel is atomic), - so an item may not appear in both {still_offered, sold} or both - {still_offered, refunded} — partial fills legitimately span - {still_offered, collected_items} until the offer is closed; - - cash: cash_quoted == still_offered_cash + collected_cash + - refunded_cash. - Returns a list of violation strings (empty = conserved). + +def conservation_violations(record): + """Side-aware per-slot GE conservation (governor 10390 finding 4). + + record: { + 'side': 'buy'|'sell', # REQUIRED — the ledger is + 'quoted': {item: qty}, # side-aware + **unit buckets (side-specific): # {item: qty} + 'cash_quoted': int, # BUY: cash committed + 'spent'/'still_committed'/'refunded': int, # BUY cash partition + 'proceeds_in_box'/'cash_collected': int, # SELL proceeds + 'sub_state': str, # phase gate input + } + + BUY invariants: + - unit partition: quoted == filled + still_open + cancelled_unfilled + per item; + - ACQUIRED ITEMS == filled ONLY: any sell-side unit bucket (sold / + returned_unsold) or a 'refunded' unit bucket on a buy record is a + violation — the unfilled/cancelled quantity is CASH (phantom + "refunded units" guard); + - cash partition: cash_quoted == spent + still_committed + refunded + (the unspent remainder of a buy is the cash refund); + - no item may be both still_open and cancelled_unfilled (cancel is + atomic). + SELL invariants: + - unit partition: quoted == sold + still_open + returned_unsold per + item; + - no item may be both still_open and returned_unsold (cancel is + atomic); sold + still_open IS legal (partial sell rest). + Shared: no negative quantities. """ out = [] - buckets = ["still_offered", "collected_items", "refunded", "sold"] + side = record.get("side", "buy") + if side not in ("buy", "sell"): + out.append(f"record side {side!r} must be 'buy' or 'sell'") + return out + buckets = BUY_UNIT_BUCKETS if side == "buy" else SELL_UNIT_BUCKETS + foreign = {"filled", "still_open", "cancelled_unfilled", "sold", + "returned_unsold"} - set(buckets) + for b in sorted(foreign): + if record.get(b): + out.append( + f"{side} ledger uses {b} units — invalid for side {side}: " + f"buy-side unfilled/cancelled quantity is CASH " + f"(refunded), never {b} units; sell-side units partition " + f"into sold/still_open/returned_unsold") + if side == "buy": + # A unit map under a CASH field is the phantom-items guard: a buy's + # unfilled/cancelled quantity is an unspent-CASH refund, never + # "refunded units" (governor 10390 finding 4). + for c in ("cash_quoted", "spent", "still_committed", "refunded"): + if isinstance(record.get(c), dict): + out.append( + f"buy ledger carries {c} as units — the unfilled/" + f"cancelled quantity of a buy is CASH (refunded), " + f"never {c} units (phantom items)") for b in buckets: for item in record.get(b, {}): q = record[b][item] @@ -182,34 +242,86 @@ def conservation_violations(record): total = sum(record.get(b, {}).get(item, 0) for b in buckets) if total != qq: out.append( - f"item '{item}' quoted {qq} != still_offered + collected + " - f"refunded + sold ({total})") - # Physically impossible co-occurrences (the buckets are a - # partition of the quote's units; cancel is atomic and units in - # a live offer cannot already be sold/cancelled): - # still_offered + sold — units cannot be both in a live - # offer and already sold - # still_offered + refunded — a cancelled offer has no live - # remainder left in the offer + f"item '{item}' quoted {qq} != " + f"{' + '.join(buckets)} ({total})") in_b = {b for b in buckets if item in record.get(b, {})} - for pair in (("still_offered", "sold"), ("still_offered", "refunded")): - if pair[0] in in_b and pair[1] in in_b: + for a, b in IMPOSSIBLE_UNIT_PAIRS[side]: + if (a in in_b and b in in_b + and record.get(a, {}).get(item) + and record.get(b, {}).get(item)): out.append( - f"item '{item}' appears in multiple buckets " - f"({pair[0]} + {pair[1]} — physically impossible: " - f"units cannot be in a live offer and a terminal " - f"bucket at the same time)") - for c in ("cash_quoted", "still_offered_cash", "collected_cash", "refunded_cash"): - if record.get(c, 0) < 0: - out.append(f"{c} is negative ({record[c]})") - if record.get("cash_quoted", 0) != ( - record.get("still_offered_cash", 0) - + record.get("collected_cash", 0) - + record.get("refunded_cash", 0)): + f"item '{item}' appears in both {a} and {b} — " + f"physically impossible (a cancelled offer has no " + f"live remainder)") + if side == "buy": + cash_fields = ["cash_quoted", "spent", "still_committed", "refunded"] + for c in cash_fields: + if isinstance(record.get(c), (int, float)) and record[c] < 0: + out.append(f"{c} is negative ({record[c]})") + cash_vals = {c: record.get(c) for c in cash_fields} + if all(isinstance(v, (int, float)) for v in cash_vals.values()): + if cash_vals["cash_quoted"] != ( + cash_vals["spent"] + cash_vals["still_committed"] + + cash_vals["refunded"]): + out.append( + f"buy cash quoted {cash_vals['cash_quoted']} != spent " + f"{cash_vals['spent']} + still_committed " + f"{cash_vals['still_committed']} + refunded " + f"{cash_vals['refunded']} — the unspent remainder of a " + f"buy is the CASH refund") + else: + for c in ("proceeds_in_box", "cash_collected"): + if record.get(c, 0) < 0: + out.append(f"{c} is negative ({record[c]})") + return out + + +def slot_phase_violations(record): + """Phase gates (governor 10390 finding 4): assets may not be reused or + a slot closed before the GE collection box is withdrawn. + + - 'buy-filled-awaiting-collect': filled must equal quoted (fully + filled) and NOTHING may have been sold yet — a full buy that sells + before ge_collect is a violation (owned units cannot leave the + collection box early). + - 'sell-filled-awaiting-collect': sold units require proceeds_in_box + > 0 — proceeds arrive in the box as units fill and only leave via + ge_collect. + - 'closed': nothing may remain uncollected — proceeds_in_box must be + 0 and no cancelled/returned units may still be in the box (a full + sell may not close before proceeds collection). + """ + out = [] + sub = record.get("sub_state") + side = record.get("side", "buy") + sold = sum((record.get("sold") or {}).values()) + filled = sum((record.get("filled") or {}).values()) + quoted = sum((record.get("quoted") or {}).values()) + if side == "buy" and sub == "buy-filled-awaiting-collect": + if sold: + out.append( + "buy-filled-awaiting-collect slot has sold units — a full " + "buy cannot sell before ge_collect") + if filled != quoted: + out.append( + f"buy-filled-awaiting-collect slot is not fully filled " + f"(filled {filled} != quoted {quoted})") + if sub == "sell-filled-awaiting-collect" and sold and not record.get("proceeds_in_box"): out.append( - f"cash quoted {record.get('cash_quoted')} != still_offered " - f"{record.get('still_offered_cash')} + collected " - f"{record.get('collected_cash')} + refunded {record.get('refunded_cash')}") + "sell-filled-awaiting-collect slot has sold units but no " + "proceeds_in_box — proceeds arrive in the collection box on " + "fill and leave only via ge_collect") + if sub == "closed": + if record.get("proceeds_in_box"): + out.append( + "closed slot still has proceeds_in_box — a full sell may " + "not close before proceeds collection (ge_collect first)") + cancelled = sum((record.get("cancelled_unfilled") or {}).values()) + returned = sum((record.get("returned_unsold") or {}).values()) + if cancelled or returned: + out.append( + "closed slot still holds cancelled/returned units — " + "collect them before closing the slot") return out @@ -343,48 +455,100 @@ def run_selftest(verbose=False): # --- per-slot scheduler dispatch: the worked example as a regression --- # Governor 10323 finding 3 + owner: reproduce the simultaneous - # partial-buy / full-sell-collect / no-fill-buy case and prove no - # unrelated slot is closed or advanced. - slot_states = {"arrowtips": "buy-open", # partial fill 4,000/10,000 — resting - "iron": "ready-to-sell", # full fill — sell this pass - "cosmic": "buy-open"} # no fill, within quote age — resting + # partial-buy / full-buy-fill / no-fill-buy case and prove no + # unrelated slot is closed or advanced. Under governor 10390 finding 2 + # a FULL buy fill goes to buy-filled-awaiting-collect (assets sit in + # the GE collection box) — the pass dispatches ge_collect, it never + # lets the slot sell directly. + slot_states = {"arrowtips": "buy-open", # partial fill 4,000/10,000 — resting + "iron": "buy-filled-awaiting-collect", # full fill — ge_collect this pass + "cosmic": "buy-open"} # no fill, edge positive — resting dispatched = {name: scheduler_dispatch(s) for name, s in slot_states.items()} - ok = dispatched == {"arrowtips": None, "iron": "sell", "cosmic": None} - check("worked example: partial-buy + full-sell + no-fill slots dispatch only the iron slot to sell; unrelated slots stay untouched", + ok = dispatched == {"arrowtips": None, "iron": "collect", "cosmic": None} + check("worked example: partial-buy + full-buy-fill + no-fill slots dispatch only the iron slot to ge_collect; unrelated slots stay untouched", ok) check("resting slots (buy-open) are never closed or advanced by a pass", dispatched.get("arrowtips") is None and dispatched.get("cosmic") is None) + check("a fully filled buy must pass through buy-filled-awaiting-collect (ge_collect) before any sell", + scheduler_dispatch("buy-filled-awaiting-collect") == "collect" + and scheduler_dispatch("sell-filled-awaiting-collect") == "collect" + and scheduler_dispatch("ready-to-sell") == "sell") - # --- conservation invariants (finding 4) --- - # Balanced snapshot mirroring the worked example: 10,000 Rune arrowtips - # quoted, 4,000 filled (units in the player's possession), 6,000 still - # resting in the live offer; plus an independent cash quote (2,230 GP) - # fully committed in live buy offers. - clean_rec = { - "quoted": {"Rune arrowtips": 10}, - "still_offered": {"Rune arrowtips": 6}, - "collected_items": {"Rune arrowtips": 4}, - "refunded": {}, - "sold": {}, - "cash_quoted": 2230, "still_offered_cash": 2230, - "collected_cash": 0, "refunded_cash": 0, + # --- side-aware conservation invariants (finding 4) --- + # BUY balanced snapshot mirroring the worked example t1: 10,000 Rune + # arrowtips quoted @ 223; 4,000 filled (units in the GE collection + # box), 6,000 still open on the resting offer; cash 2,230,000 = + # spent 892,000 + still_committed 1,338,000 + refunded 0. + buy_partial = { + "side": "buy", + "quoted": {"Rune arrowtips": 10_000}, + "filled": {"Rune arrowtips": 4_000}, + "still_open": {"Rune arrowtips": 6_000}, + "cancelled_unfilled": {}, + "cash_quoted": 2_230_000, "spent": 892_000, + "still_committed": 1_338_000, "refunded": 0, } - check("conservation: a balanced per-slot ledger (partial fill split across still_offered + collected, partition sums to quoted) has zero violations", - conservation_violations(clean_rec) == []) - dup_rec = dict(clean_rec, quoted={"Rune arrowtips": 10}, - still_offered={"Rune arrowtips": 4}, - sold={"Rune arrowtips": 6}) - check("conservation: units both in a live offer AND already sold (impossible pair) is a violation", - any("multiple buckets" in v for v in conservation_violations(dup_rec))) - over_rec = dict(clean_rec, collected_items={"Rune arrowtips": 7}) - check("conservation: over-reported buckets break the quoted partition (sum 13 != quoted 10)", - any("quoted 10 !=" in v for v in conservation_violations(over_rec))) - neg_rec = dict(clean_rec, sold={"Rune arrowtips": -1}) + check("conservation: BUY side — partial fill splits units into filled + still_open and cash into spent + still_committed (partition sums hold, zero violations)", + conservation_violations(buy_partial) == []) + buy_cancelled = dict(buy_partial, still_open={}, + cancelled_unfilled={"Rune arrowtips": 6_000}, + still_committed=0, refunded=1_338_000) + check("conservation: BUY side — a cancelled partial buy carries filled units + refunded CASH (still_open -> cancelled_unfilled + refunded), zero violations", + conservation_violations(buy_cancelled) == []) + phantom_rec = dict(buy_cancelled, refunded={"Rune arrowtips": 6_000}) + check("conservation: BUY side — 'refunded UNITS' on a buy is a phantom (unfilled/cancelled quantity is CASH): violation", + any("CASH" in v or "invalid for side" in v + for v in conservation_violations(phantom_rec))) + sold_on_buy = dict(buy_partial, sold={"Rune arrowtips": 4_000}) + check("conservation: BUY side — a full buy that sells before ge_collect (sold units on a buy ledger) is a violation", + any("invalid for side" in v for v in conservation_violations(sold_on_buy))) + buy_cash_bad = dict(buy_partial, still_committed=1_380_000) + check("conservation: BUY side — cash partition mismatch (spent + still_committed + refunded != cash_quoted) is a violation", + any("cash quoted" in v for v in conservation_violations(buy_cash_bad))) + # SELL balanced: 10,000 sold @ 157, proceeds 1,570,000 in the box. + sell_full = { + "side": "sell", + "quoted": {"Iron bar": 10_000}, + "sold": {"Iron bar": 10_000}, + "still_open": {}, "returned_unsold": {}, + "proceeds_in_box": 1_570_000, "cash_collected": 0, + } + check("conservation: SELL side — full sell partitions sold + still_open + returned_unsold, proceeds in the box (zero violations)", + conservation_violations(sell_full) == []) + sell_partial = dict(sell_full, sold={"Iron bar": 4_000}, + still_open={"Iron bar": 6_000}, + proceeds_in_box=628_000) + check("conservation: SELL side — a partial sell legitimately spans sold + still_open (zero violations)", + conservation_violations(sell_partial) == []) + sell_dup = dict(sell_full, sold={"Iron bar": 6_000}, + still_open={"Iron bar": 3_000}, + returned_unsold={"Iron bar": 1_000}) + check("conservation: SELL side — units both in a live offer AND returned (impossible pair) is a violation", + any("both still_open and returned_unsold" in v + for v in conservation_violations(sell_dup))) + over_rec = dict(buy_partial, filled={"Rune arrowtips": 7_000}) + check("conservation: over-reported buckets break the quoted partition (sum 13,000 != quoted 10,000)", + any("quoted 10000 !=" in v for v in conservation_violations(over_rec))) + neg_rec = dict(sell_full, sold={"Iron bar": -1}) check("conservation: a negative bucket is a violation", any("negative" in v for v in conservation_violations(neg_rec))) - cash_rec = dict(clean_rec, collected_cash=900) - check("conservation: cash quoted != still_offered + collected + refunded is a violation", - any("cash quoted" in v for v in conservation_violations(cash_rec))) + + # --- phase gates (finding 4: assets cannot be reused/closed pre-collect) --- + check("phase gate: a full buy that sells before ge_collect (buy-filled-awaiting-collect with sold units) is rejected", + any("cannot sell before ge_collect" in v + for v in slot_phase_violations(dict(buy_partial, sub_state="buy-filled-awaiting-collect", + filled={"Rune arrowtips": 10_000}, still_open={}, + sold={"Rune arrowtips": 10_000})))) + check("phase gate: a full sell that closes before proceeds collection (closed with proceeds_in_box > 0) is rejected", + any("before proceeds collection" in v + for v in slot_phase_violations(dict(sell_full, sub_state="closed")))) + check("phase gate: a sell-filled-awaiting-collect slot with sold units but no proceeds_in_box is rejected", + any("no proceeds_in_box" in v + for v in slot_phase_violations(dict(sell_full, sub_state="sell-filled-awaiting-collect", + proceeds_in_box=0)))) + check("phase gate: a closed slot that has been fully collected (no proceeds_in_box, no cancelled/returned units) passes", + slot_phase_violations(dict(sell_full, sub_state="closed", + proceeds_in_box=0, cash_collected=1_570_000)) == []) # --- 4h buy-limit ledger vs offer expiry (finding 1) --- check("buy limit: a requote within the remaining capacity is allowed (4,000 used of 10,000 -> 6,000)", @@ -394,29 +558,37 @@ def run_selftest(verbose=False): check("buy limit: a cancelled offer does NOT renew the limit (10,000 used of 10,000 -> any requote blocked before reset)", not buy_capacity_ok(limit_used=10000, limit=10000, qty_requested=1)) - # --- structured lifecycle gates (findings 1-5) --- + # --- structured lifecycle gates (findings 1-5 + governor 10390) --- margin_tags = ["flipping"] ssm_ok = { "sub_states": SLOT_SUB_STATES, "slot_transitions": { "free": ["buy-open"], - "buy-open": ["ready-to-sell", "buy-cancelled-awaiting-collect"], - "buy-cancelled-awaiting-collect": ["ready-to-sell", "free"], + "buy-open": ["buy-filled-awaiting-collect", + "buy-cancelled-awaiting-collect"], + "buy-filled-awaiting-collect": ["ready-to-sell", "free"], + "buy-cancelled-awaiting-collect": ["ready-to-sell", "ready-to-requote", "free"], "ready-to-sell": ["sell-open"], - "sell-open": ["closed", "sell-cancelled-awaiting-collect"], + "sell-open": ["sell-filled-awaiting-collect", + "sell-cancelled-awaiting-collect"], + "sell-filled-awaiting-collect": ["closed"], "sell-cancelled-awaiting-collect": ["ready-to-requote", "closed"], "ready-to-requote": ["buy-open", "sell-open", "free"], "closed": [], }, "slot_transition_whens": [ "free -> buy-open: quote placed", - "buy-open -> ready-to-sell: offer fully filled", - "buy-open -> buy-cancelled-awaiting-collect: abort (stale edge / quote age)", + "buy-open -> buy-filled-awaiting-collect: offer FULLY filled — units sit in the GE collection box until ge_collect", + "buy-open -> buy-cancelled-awaiting-collect: abort on stale edge (margin no longer positive) or strategy reallocation — never on the 4h limit reset", + "buy-filled-awaiting-collect -> ready-to-sell: ge_collect withdrawn units; sell edge positive", + "buy-filled-awaiting-collect -> free: ge_collect withdrawn; margin gone, slot released", "buy-cancelled-awaiting-collect -> ready-to-sell: collected, sell edge positive", - "buy-cancelled-awaiting-collect -> free: collected, full refund, slot freed", + "buy-cancelled-awaiting-collect -> ready-to-requote: collected; margin positive and capacity remains", + "buy-cancelled-awaiting-collect -> free: collected, full cash refund, slot freed", "ready-to-sell -> sell-open: sell offer placed", - "sell-open -> closed: fully filled and proceeds collected", - "sell-open -> sell-cancelled-awaiting-collect: abort", + "sell-open -> sell-filled-awaiting-collect: sell FULLY filled — proceeds sit in the box until ge_collect", + "sell-open -> sell-cancelled-awaiting-collect: abort on stale edge or strategy reallocation", + "sell-filled-awaiting-collect -> closed: proceeds collected and ledger reconciled", "sell-cancelled-awaiting-collect -> ready-to-requote: collected, requote", "sell-cancelled-awaiting-collect -> closed: collected, slot closed", "ready-to-requote -> buy-open: requoted buy", @@ -424,17 +596,17 @@ def run_selftest(verbose=False): "ready-to-requote -> free: abandoned", ], "scheduler_iteration": "one pass over all slots; dispatch pending actions only", - "quote_age_policy": "resting offers re-checked every 600 s; stale edges requoted", - "persisted_state": "per-slot record: sub-state + quoted/filled/sold/collected", - "limit_ledger": "limit_used + first_fill_ts + limit_resets_at, independent of offer expiry", - "conservation": "quoted == still_offered + collected + refunded + sold", + "quote_age_policy": "offers rest while the scanned margin stays positive; aborted only on a stale edge or strategy reallocation — the 4h limit reset never cancels an offer", + "persisted_state": "per-slot record: sub-state + side-aware quoted/filled/sold/collected buckets", + "limit_ledger": "limit_used + first_fill_ts + limit_resets_at, independent of offer expiry; cancellation never renews it", + "conservation": "side-aware: buy quoted units = filled + still_open + cancelled_unfilled and acquired items = filled only; sell quoted = sold + still_open + returned_unsold", "break_resume": "breaks between passes; state persists in the per-slot record", } flip_ok = {"tags": margin_tags, "economy": { "income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17, "ceiling_gp_per_hour": 149500, "gp_per_hour_low": 102600, "gp_per_hour_high": 138800}, "slot_state_machine": ssm_ok} - check("flip lifecycle: ceiling + nested realized bounds + canonical slot machine passes", + check("flip lifecycle: ceiling + nested realized bounds + canonical 10-state slot machine passes", ge_lifecycle_errors("ok.yaml", flip_ok) == []) errs = ge_lifecycle_errors("bad.yaml", {"tags": margin_tags, "economy": { "income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17, From 052c6984ac4eddabe489d9843abfac8499f8788c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 03:00:56 -0500 Subject: [PATCH 14/15] =?UTF-8?q?codex:=20low-alch-members=20=E2=80=94=20l?= =?UTF-8?q?oss-reserve=20stop=20direction=20+=20fill-price=20scenario=20GP?= =?UTF-8?q?=20bounds=20(bounce-fix=20attempt=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner guidance 2026-08-29 for governor review 10361 (PR #8), finding by finding: 1. Loss-reserve stop direction — the stop read 'session net loss (total assets - initial capital) > 162,990'; a loss makes that LHS negative so it could never exceed the positive reserve (the stop could never fire). Now expressed everywhere as 'initial capital - total assets > 162,990' (stop_conditions, worked-example step 6, recovery block, version pin): a loss makes the difference positive, the strict-greater comparison fires on losses and never on profits, and exactly-at-boundary does not fire. tools/validate.py gains alch_accounting_regression(): comparator boundary fixtures (below 162,989 / at 162,990 / above 162,991 / profit side +50,000 / breakeven) plus entry-text pins rejecting the reversed form anywhere in the file. 2. Fill-price scenarios for realized GP bounds — previously gp_per_hour_low /high derived only from the live-low -3.00/cast margin while the buy policy permits live low + 5% (candidate 423.15 + nature 147 = 570.15 vs 540 lowalch = -30.15/cast). The economy block now declares FILL-PRICE SCENARIOS A (live-low 543 -> -3.00/cast) and B (max allowed +5% 570.15 -> -30.15/cast) as the single ledger behind capital, batch economics AND the GP bounds: gp_per_hour_low -29,463 (scenario B x session-fast 977.2), gp_per_hour_high -1,758 (scenario A x session-slow 585.9), midpoints -22,365 / -2,225 pinned; validator enforces the exact bound values and the -30.15/570.15/423.15 scenario pins. Preserved: complete GE acquisition/restock flow, one-cast iteration, monotonic break/restock counters, total-assets accounting (floor 575,150), fail-closed staff path, and the earlier fixed pacing/equip findings. version 5 -> 6, updated 2026-08-29. Gate: python3 tools/validate.py -> 2 entries, 0 errors, 1 pre-existing F2P WARN (low-alch-f2p researched, untouched); git diff --check clean; regression negative-tested (old gp pins, reversed form, missing scenario pins all rejected). --- codex/economy/low-alch-members.yaml | 46 +++++++++++++----- tools/validate.py | 75 +++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml index cc3b7c7..261f77d 100644 --- a/codex/economy/low-alch-members.yaml +++ b/codex/economy/low-alch-members.yaml @@ -3,8 +3,8 @@ title: Low-Level Alchemy (Members) domain: economy tags: [alching, magic-training, members, loss-training] status: verified -version: 5 # 5: bounce-fix 4 2026-08-28 (owner guidance 10325) — complete GE acquisition path (open -> place -> fill -> collect) for initial buy AND every restock (bank-stock fast path only when the bank holds stock); fail-closed staff equip (bounded retries, no fire-rune fallback); capital model (cash vs supplies, staff cost, max-fill worst case, 6-batch loss reserve, float); monotonic break/restock counters (breaks 550/1,100/1,650 independent of restocks 901/1,802) + validator regression; startup-vs-steady-state rates over a 2 h session horizon -updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date +version: 6 # 6: bounce-fix 5 2026-08-29 (owner guidance for governor 10361) — loss-reserve stop fixed to 'initial capital - total assets > 162,990' everywhere + validator boundary fixtures (below/at/above/profit) + realized GP/hr bounds derived from declared fill-price scenarios (A live-low -3.00/cast, B max allowed +5% -30.15/cast): gp low -29,463 / high -1,758. # 5: bounce-fix 4 2026-08-28 (owner guidance 10325) — complete GE acquisition path (open -> place -> fill -> collect) for initial buy AND every restock (bank-stock fast path only when the bank holds stock); fail-closed staff equip (bounded retries, no fire-rune fallback); capital model (cash vs supplies, staff cost, max-fill worst case, 6-batch loss reserve, float); monotonic break/restock counters (breaks 550/1,100/1,650 independent of restocks 901/1,802) + validator regression; startup-vs-steady-state rates over a 2 h session horizon +updated: "2026-08-29" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > Magic training from level 21 by low-alching members items with the lowest @@ -13,7 +13,10 @@ summary: > (mapping lowalch != null, members=true, live GE prices 2026-08-27). At current prices the method burns a small amount of GP per cast for 31 XP — run it for XP, not GP. Least-loss liquid anchor at last check: rune - javelin tips (-3/cast). Model pins (owner guidance 2026-08-28): one + javelin tips (live-low net -3/cast; realized loss band -1,758 to -29,463 + gp/hr across the declared live-low and max-allowed +5% fill scenarios — + XP is fill-independent at 23,000/hr session-realized midpoint). Model + pins (owner guidance 2026-08-28): one iteration = one cast; breaks every 400-700 COMPLETED CASTS (cast-count based, independent of bank visits — midpoint example: 550, 1,100, 1,650, ...); restock when candidate or nature drops below the numeric @@ -68,8 +71,8 @@ economy: cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire, live low 2026-08-27) income_per_cycle: 540 # lowalch of rune javelin tips (40% of value 1350) net_per_cycle: -3 - gp_per_hour_low: -2930 # session-realized fast bound: 977.2 casts/hr x -3 = -2,932 (3.4 s cycle, 300 s startup, 2 h horizon) - gp_per_hour_high: -1760 # session-realized slow bound: 585.9 casts/hr x -3 = -1,758 (5.2 s cycle, 600 s startup, 2 h horizon) + gp_per_hour_low: -29463 # WORST-CASE realized: max-allowed fill scenario B (-30.15/cast) x session-fast 977.2 casts/hr = -29,462.6 (3.4 s cycle, 300 s startup, 2 h horizon) — see FILL-PRICE SCENARIOS below + gp_per_hour_high: -1758 # BEST-CASE realized: live-low fill scenario A (-3.00/cast) x session-slow 585.9 casts/hr = -1,757.7 (5.2 s cycle, 600 s startup, 2 h horizon) — see FILL-PRICE SCENARIOS below xp_per_hour: 23000 # session-realized midpoint: 741.8 casts/hr x 31 = 22,995 (pin 23,000) # ---- rate model (pinned; owner guidance 2026-08-28) ---- # LOOP (idealised, zero overhead — ceiling reference ONLY, never the @@ -87,14 +90,30 @@ economy: # SESSION-REALIZED (2 h horizon = 7,200 s; one-time startup 300-600 s, # mid 450 s -> 450/7200 = 6.25 %): realized = steady-state x (1 - # startup/session): fast 977.2, mid 741.8, slow 585.9 casts/hr. - # xp mid = 741.8 x 31 = 22,995 -> pin 23,000; gp = casts/hr x -3: - # low -2,932 (fast), high -1,758 (slow), mid -2,225. + # xp mid = 741.8 x 31 = 22,995 -> pin 23,000. REALIZED GP/hr derives from + # the FILL-PRICE SCENARIOS below (the SAME ledger as the batch economics): + # worst = -29,463 = scenario B x session-fast 977.2 (-30.15 x 977.2 = -29,462.6) + # best = -1,758 = scenario A x session-slow 585.9 (-3.00 x 585.9 = -1,757.7) + # midpoints: scenario A x 741.8 = -2,225; scenario B x 741.8 = -22,365. # BATCH ECONOMICS (901 casts; deposits 901 x 540 = 486,540 gp per batch): # input cost at live low: 901 x 543 = 489,243 -> net -2,703/batch # input cost at MAX fill (live low + 5 %): 901 x 570.15 = 513,705 -> net # -27,165/batch (worst case). Reserve = 6 worst-case batches (Magic # 21 -> 55 = ~5,300 casts = ~6 batches) = 162,990 gp; the capital stop # checks TOTAL ASSETS, not raw cash (see accounts + stop_conditions). + # FILL-PRICE SCENARIOS (one ledger for capital, batch economics, AND the + # realized GP bounds — every number below comes from the same scenario): + # A live-low fill: candidate 403 + nature 140 = 543/cast vs + # 540 lowalch -> net -3.00/cast (the anchor) + # B max allowed fill (+5%): candidate 403x1.05 = 423.15 + nature + # 140x1.05 = 147 -> 570.15/cast vs 540 lowalch + # -> net -30.15/cast (the executable buy policy + # permits live low + 5%; this scenario, not the + # -3 live-low margin, bounds the downside) + # gp_per_hour_low = -29,463 | gp_per_hour_high = -1,758 derive from these + # scenarios at the session rates above; the batch ledger uses the same + # 543 / 570.15 per-cast costs (901-cast batch: -2,703 live-low, -27,165 + # max fill), so capital accounting and GP bounds can never diverge. supplies: - item: rune javelin tips qty_per_cycle: 1 @@ -363,7 +382,7 @@ run_control: - "magic level >= target (55 -> hand off to HLA)" - "session elapsed >= 2 h (session horizon — startup is amortised over this window in the rate model)" - "total assets (cash + supplies at current buy cost) < 575,150 — cannot fund the next batch at max fill (570,150) plus staff replacement (5,000); assets, NOT raw cash: cash that became supplies is not a loss" - - "session net loss (total assets - initial capital) > 162,990 — the 6-batch worst-case loss reserve is exhausted" + - "session net loss: initial capital - total assets > 162,990 — a loss makes 'initial capital - total assets' POSITIVE, so this strict-greater comparison fires on losses and never on profits; 162,990 = the 6-batch worst-case reserve (exactly AT the boundary it does NOT fire — strict >)" - "3 consecutive failed casts (client issue — do not loop)" - "3 consecutive no-fill GE rounds (initial acquisition or restock) with no fallback candidate available" - "staff of fire equip failed after bounded retries — FAIL CLOSED (log ERROR, alert, stop)" @@ -406,8 +425,9 @@ run_control: # 1,802 (901-cadence, 1,802 = 901 x 2). The sequences 550/1,100/1,650/2,200 # and 901/1,802/2,703 interleave independently. # Step 6 — continue until a stop_condition fires: Magic >= 55 (HLA handoff), - # the 2 h session horizon, the capital stops (assets floor 575,150 or net - # loss > 162,990), 3 failed casts, or 3 consecutive no-fill GE rounds. + # the 2 h session horizon, the capital stops (assets floor 575,150, or + # loss = initial capital - total assets > 162,990), 3 failed casts, or 3 + # consecutive no-fill GE rounds. # ============================================================================================ risk: @@ -436,7 +456,11 @@ risk: # (a8); 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert). # - Capital: the stop checks TOTAL ASSETS (cash + supplies at current buy cost), and # both capital stops are assets-based (see stop_conditions) — cash converted into -# supplies is not a loss and never fires a stop by itself. +# supplies is not a loss and never fires a stop by itself. Loss direction is pinned +# everywhere as 'initial capital - total assets > 162,990': a loss makes that +# difference positive, so the stop fires on losses and never on profits (the +# reversed sign form can never exceed the threshold on a loss and is rejected by +# the validator regression; boundary fixtures: below/at/above + profit side). # - Explicit completion/stop: stop_conditions above (Magic 55 HLA handoff, 2 h session # horizon, assets floor 575,150, loss reserve 162,990, repeated failures, fail-closed # staff equip). diff --git a/tools/validate.py b/tools/validate.py index 0068a6f..d5b74e3 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -72,6 +72,77 @@ def alch_worked_example_regression(rel, data, raw_text): return errs +def _loss_reserve_stop(initial_capital, total_assets, reserve): + """Loss-reserve stop comparator in the pinned direction (owner guidance + 2026-08-29, governor review 10361, PR #8): stop fires only when the + loss EXCEEDS the reserve, i.e. when (initial capital - total assets) + > reserve. A loss makes the LHS positive; a profit (total assets > + initial capital) makes it negative and can never fire this stop. The + reversed 'total assets - initial capital' form is invalid because it + is negative exactly when a loss exists.""" + return (initial_capital - total_assets) > reserve + + +def alch_accounting_regression(rel, data, raw_text): + """Loss-reserve + fill-price accounting regression (owner guidance + 2026-08-29 for governor review 10361, PR #8): the low-alch-members + entry must express the loss-reserve stop in the correct direction + EVERYWHERE ('initial capital - total assets > 162,990'), the reversed + sign form must never reappear, and the realized GP/hour bounds must be + derived from the declared fill-price scenarios (A live-low -3.00/cast + vs B max allowed +5% fill -30.15/cast), exposing the +5% fill loss + instead of recycling the live-low margin. Includes below/at/above + boundary fixtures and a profit-side negative regression for the + comparator itself. Scoped to the members low-alchemy entry only.""" + if data.get("id") != "low-alch-members": + return [] + errs = [] + + # 1. comparator boundary fixtures (pure function, entry's own constants) + INIT, RESERVE = 1_308_290, 162_990 # entry capital / 6-batch reserve + fixtures = [ + ("below (loss 162,989)", INIT - 162_989, False), + ("at boundary (loss 162,990, strict >)", INIT - 162_990, False), + ("above (loss 162,991)", INIT - 162_991, True), + ("profit side (assets 50,000 above initial)", INIT + 50_000, False), + ("breakeven (assets == initial)", INIT, False), + ] + for label, total_assets, expect in fixtures: + got = _loss_reserve_stop(INIT, total_assets, RESERVE) + if got != expect: + errs.append(f"{rel}: loss-reserve comparator fixture '{label}' " + f"returned {got}, expected {expect}") + + # 2. entry direction: correct form present, reversed form absent + if "initial capital - total assets > 162,990" not in raw_text: + errs.append(f"{rel}: loss-reserve stop must be expressed as " + f"'initial capital - total assets > 162,990' everywhere") + if "total assets - initial capital" in raw_text: + errs.append(f"{rel}: loss-reserve stop uses the reversed sign form — " + f"on a loss it is negative and can never exceed the " + f"reserve; use 'initial capital - total assets > 162,990'") + + # 3. realized GP bounds must come from the fill-price scenarios + eco = data.get("economy", {}) + if eco.get("gp_per_hour_low") != -29463: + errs.append(f"{rel}: gp_per_hour_low must be -29,463 (max-fill " + f"scenario B -30.15/cast x session-fast 977.2 casts/hr " + f"= -29,462.6), got {eco.get('gp_per_hour_low')}") + if eco.get("gp_per_hour_high") != -1758: + errs.append(f"{rel}: gp_per_hour_high must be -1,758 (live-low " + f"scenario A -3.00/cast x session-slow 585.9 casts/hr " + f"= -1,757.7), got {eco.get('gp_per_hour_high')}") + for pin, label in (("-30.15", "max-fill per-cast loss -30.15"), + ("570.15", "max-fill per-cast cost 570.15"), + ("423.15", "max-fill candidate price 423.15")): + if pin not in raw_text: + errs.append(f"{rel}: economy FILL-PRICE SCENARIOS missing {label} " + f"({pin}) — the +5% accepted-fill scenario must be " + f"declared so the realized GP bounds cannot silently " + f"revert to the live-low-only margin") + return errs + + def main(): global STALE_DAYS if "--stale-days" in sys.argv: @@ -134,6 +205,10 @@ def main(): # monotonic break/restock counters, scoped to the one-cast model errors += alch_worked_example_regression(rel, data, path.read_text()) + # loss-reserve direction + fill-price GP bound regression (owner + # guidance 2026-08-29 for governor 10361), scoped to low-alch-members + errors += alch_accounting_regression(rel, data, path.read_text()) + # action id/step uniqueness a_ids = [a["id"] for a in data["actions"]] a_steps = [a["step"] for a in data["actions"]] From 671d025d9f114fc23c80d66d1d85a15e86d9b3d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 03:11:20 -0500 Subject: [PATCH 15/15] fix(flip-candidates): release the physical GE slot after a completed flip via closed -> free (governor 10471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A successful sell followed sell-filled-awaiting-collect -> closed, and 'closed' had no outgoing transition while the scheduler left closed slots untouched — every GE slot could perform ONE flip and then became permanently unusable (no rotations, breaks, or longer sessions). Fix (finding-by-finding, governor comment 10471): 1. 'closed' is no longer a sink. The completed cycle's ledger is reconciled and ARCHIVED at close, and the physical slot is released to free via the explicit archival transition closed -> free (flip-candidates.yaml slot_transitions, whens, scheduler_iteration, a9 archive step, summary, risk, sources). The scheduler pass dispatches the release (SLOT_DISPATCH['closed'] = 'release'), so a closed slot is only ever transient. 2. A deliberately closed cancelled sell releases identically: sell-cancelled-awaiting-collect -> closed -> free (whens updated). 3. Committed two-rotation regression (validator, wired into ge_lifecycle_errors for every flipping entry): slot_reuse_violations walks one physical slot through free -> buy-open -> buy-filled-awaiting-collect -> ready-to-sell -> sell-open -> sell-filled-awaiting-collect -> closed -> free -> buy-open (second buy in the SAME slot), validating every ledger frame against both conservation gates; archived_ledger_preserved_after_second_buy proves the rotation-1 archived ledger survives rotation 2 bit-for-bit (no loss, duplication, or mutation). Negative fixtures prove a closed sink or a closed that does not release to free FAILS. 4. ge_lifecycle_errors now requires closed == ['free'] exactly and rejects any machine where a state has no transitions. 5. Worked example: t5/t8 walk the release, t8b runs rotation 2 in the same physical slots. All prior fixtures preserved (tax x4, async-slot scheduler, collection-box, limit-window, side-aware conservation x25) — now 35 selftest checks. Evidence: python3 tools/validate.py -> 2 entries, 0 errors, 1 pre-existing warning; --selftest -> 35 PASS, 0 errors; git diff --check clean. --- codex/economy/flip-candidates.yaml | 94 ++++++++++----- docs/action-grammar.md | 2 +- tools/validate.py | 179 +++++++++++++++++++++++++++-- 3 files changed, 237 insertions(+), 38 deletions(-) diff --git a/codex/economy/flip-candidates.yaml b/codex/economy/flip-candidates.yaml index 807bbcc..d25e87c 100644 --- a/codex/economy/flip-candidates.yaml +++ b/codex/economy/flip-candidates.yaml @@ -3,7 +3,7 @@ title: GE Flipping (margin trading) domain: economy tags: [flipping, merchanting, ge, money-making, hybrid] status: verified -version: 5 # 5: bounce-fix 6 2026-08-28 (governor comment 10390) — the 4h buy-limit reset NEVER cancels an offer (offer lifetime = independent quote_age_policy: stale edge or strategy reallocation only); explicit buy-filled-awaiting-collect / sell-filled-awaiting-collect states so completed offers must pass through the GE collection box (ge_collect) before reuse or closing; SIDE-AWARE per-slot ledgers (buy: quoted units = filled + still_open + cancelled_unfilled, acquired items = filled ONLY, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with committed negative fixtures for phantom refunded buy units, a full buy sold before collection, and a full sell closed before proceeds collection. Capped/floored tax model + its 4 selftest fixtures and the asynchronous scheduler behavior preserved (owner decisions 2026-08-28). +version: 6 # 6: bounce-fix 7 2026-08-29 (governor comment 10471) — 'closed' is no longer a sink: after a completed sell's proceeds are collected and the ledger is reconciled + ARCHIVED, the physical GE slot is released to free via the explicit archival transition `closed -> free` (a deliberately closed cancelled sell releases the same way), and the committed two-rotation regression proves one slot completes buy/collect/sell/collect, archives the first ledger, and opens a SECOND buy in the same slot without losing, duplicating, or mutating the archived record. Previous (v5, bounce-fix 6): the 4h buy-limit reset NEVER cancels an offer (offer lifetime = independent quote_age_policy: stale edge or strategy reallocation only); explicit buy-filled-awaiting-collect / sell-filled-awaiting-collect states so completed offers must pass through the GE collection box (ge_collect) before reuse or closing; SIDE-AWARE per-slot ledgers (buy: quoted units = filled + still_open + cancelled_unfilled, acquired items = filled ONLY, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with committed negative fixtures for phantom refunded buy units, a full buy sold before collection, and a full sell closed before proceeds collection. Capped/floored tax model + its 4 selftest fixtures and the asynchronous scheduler behavior preserved (owner decisions 2026-08-28). updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: >- @@ -21,7 +21,11 @@ summary: >- global scheduler pass that acts only on dispatchable slots and never closes or advances unrelated ones; completed offers (full buys and full sells) land in the GE COLLECTION BOX and must pass through ge_collect + - ledger reconciliation before assets are reused or the slot closes; the + ledger reconciliation before assets are reused or the slot closes; + 'closed' is NOT a sink — the physical GE slot is released to free via + the archival transition `closed -> free` after the completed cycle's + ledger is reconciled + archived, so every slot runs flip after flip + through rotations and breaks (two-rotation reuse regression); the 4h buy-limit ledger (limit_used / first_fill_ts / limit_resets_at / remaining) is DISTINCT from offer expiry — a limit-window reset NEVER cancels an offer and cancellation NEVER renews the limit; ge_cancel @@ -85,12 +89,16 @@ best_practice: why: "A slot's offer fills on its own timeline (partial fills keep resting). The global scheduler pass acts only on dispatchable slots (free -> quote, *-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote -> - requote) and leaves resting buy-open / sell-open / closed slots untouched — one - slot's cancel or close never advances an unrelated slot. A FULLY FILLED offer - never sells or closes directly: it first enters an explicit - buy-/sell-filled-awaiting-collect state because its assets sit in the GE - collection box until ge_collect (finding 2, governor 10390)." - source: "governor review comment 10323 + 10390 (2026-08-28); OSRS wiki — Grand Exchange / Bank (collection box); docs/action-grammar.md" + requote, closed -> free archival release) and leaves resting buy-open / + sell-open slots untouched — one slot's cancel or close never advances an + unrelated slot. A FULLY FILLED offer never sells or closes directly: it + first enters an explicit buy-/sell-filled-awaiting-collect state because + its assets sit in the GE collection box until ge_collect (finding 2, + governor 10390). A 'closed' slot is only ever transient: the completed + cycle's ledger is archived at close and the pass releases the physical + slot to free (closed -> free), so the same slot runs the next flip — + never one flip and done (governor 10471)." + source: "governor review comments 10323 + 10390 + 10471 (2026-08-28/29); OSRS wiki — Grand Exchange / Bank (collection box); docs/action-grammar.md" - name: keep a per-slot offer ledger; ge_cancel -> collection box -> ge_collect -> reconcile, never strand units why: "Every slot tracks its side-aware ledger: a BUY quote partitions units into filled + still_open + cancelled_unfilled with acquired items = filled ONLY (the @@ -176,7 +184,7 @@ slot_state_machine: sell-filled-awaiting-collect: [closed] sell-cancelled-awaiting-collect: [ready-to-requote, closed] ready-to-requote: [buy-open, sell-open, free] - closed: [] + closed: [free] # archival release — the physical slot is reusable for the next flip (governor 10471) slot_transition_whens: - "free -> buy-open: scheduler pass quotes a resting buy at the low edge (qty = min(remaining 4h limit, slot capital, capital available))" - "buy-open -> buy-filled-awaiting-collect: offer FULLY filled (filled == quoted) — the units sit in the GE COLLECTION BOX until ge_collect; the slot may NOT sell them directly (governor 10390 finding 2)" @@ -189,9 +197,10 @@ slot_state_machine: - "ready-to-sell -> sell-open: scheduler pass quotes a resting sell at the high edge for the units in hand" - "sell-open -> sell-filled-awaiting-collect: sell offer FULLY filled (sold == quoted) — the proceeds sit in the GE COLLECTION BOX until ge_collect; the slot may NOT close directly (governor 10390 finding 2)" - "sell-open -> sell-cancelled-awaiting-collect: quote_age_policy abort — scanned edge stale or strategy reallocation; ge_cancel: unsold units + proceeds received so far move to the collection box (never to inventory)" - - "sell-filled-awaiting-collect -> closed: ge_collect withdrawn the proceeds; ledger reconciled — slot freed" + - "sell-filled-awaiting-collect -> closed: ge_collect withdrawn the proceeds; ledger reconciled and ARCHIVED — the archived cycle record is immutable; the physical slot is released at the next dispatch by the closed-to-free archival transition (governor 10471)" - "sell-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; requote the sell at the fresh high edge or the current band" - - "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; slot closed (exit taken)" + - "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; ledger archived, deliberately closed (exit taken) — the physical slot releases to free exactly like a completed sell (governor 10471)" + - "closed -> free: ARCHIVAL RELEASE — the completed cycle's ledger stays archived (immutable, conserved), and the physical GE slot returns to free to place the next flip's buy (two-rotation reuse regression)" - "ready-to-requote -> buy-open: requoted buy placed (buy_capacity_ok: limit_used + qty <= limit until limit_resets_at)" - "ready-to-requote -> sell-open: requoted sell placed at the fresh high edge" - "ready-to-requote -> free: slot abandoned and released" @@ -201,11 +210,15 @@ slot_state_machine: awaiting-collect / buy-cancelled-awaiting-collect / sell-filled-awaiting-collect / sell-cancelled-awaiting-collect -> ge_collect, ready-to-sell -> sell (ge_sell), ready-to-requote -> - requote. Resting buy-open / sell-open / closed slots produce NO action - and are never closed, advanced, re-quoted or touched by the pass. A - pass therefore advances only the slots whose sub-state is dispatchable, - and a completed offer always passes through its *-filled-awaiting- - collect state (ge_collect) before its assets are reused. + requote, closed -> free (archival release: the completed cycle's + ledger was archived at close; the pass returns the physical slot to + free so the next flip can begin). Resting buy-open / sell-open slots + produce NO action and are never closed, advanced, re-quoted or touched + by the pass. A pass therefore advances only the slots whose sub-state + is dispatchable, a completed offer always passes through its + *-filled-awaiting-collect state (ge_collect) before its assets are + reused, and a closed slot is only ever transient — released to free + rather than left to strand after one flip (governor 10471). quote_age_policy: >- Offer lifetime is INDEPENDENT of the 4h buy-limit ledger. Resting offers are re-checked at the 600 s cadence and kept resting while the @@ -249,14 +262,22 @@ slot_state_machine: gates: a buy-filled-awaiting-collect slot has NOTHING sold yet (a full buy cannot sell before ge_collect) and a sell-filled-awaiting-collect slot holds its proceeds in the box; a 'closed' slot has no uncollected - proceeds or cancelled/returned units. Committed validator fixtures — + proceeds or cancelled/returned units — it holds ONLY the archived + (immutable) cycle record, and the physical slot is released to free + via the closed -> free archival transition so the SAME slot can run + the next flip (governor 10471; two-rotation reuse regression: the + archived rotation-1 ledger survives rotation 2 bit-for-bit). + Committed validator fixtures — including the negative fixtures (phantom refunded buy units; a full buy sold before collection; a full sell closed before proceeds - collection) — prove the invariants and the scheduler dispatch. + collection; a 'closed' sink that strands the slot after one flip) — + prove the invariants and the scheduler dispatch. break_resume: >- Breaks are taken only when no slot is dispatchable: every slot is - resting (buy-open / sell-open), free, or collected (closed) — never - mid-collect, mid-place, or in an *-awaiting-collect state. Full state + resting (buy-open / sell-open) or free — never mid-collect, mid-place, + or in an *-awaiting-collect state. A closed slot is only ever + transient (archived and released to free by the pass), so a break + never catches the activity between archive and release; full state (offers server-side + the persisted per-slot record) survives the pause; resume by re-opening the GE, re-checking offers, re-running the scheduler pass and continuing each slot from its persisted sub-state. @@ -268,7 +289,7 @@ state_machine: terminal: [complete, failed] states: idle: - goal: "be logged in at a bank with a GE booth, capital >= floor, every slot's ledger closed (sub-state free or closed, all buckets zero)" + goal: "be logged in at a bank with a GE booth, capital >= floor, every slot's ledger closed (sub-state free — a closed slot is transient and released to free by the pass — all live buckets zero)" next: [scan, complete] scan: goal: "read the current high/low band for every candidate; compute net-positive edges with floored+capped tax (a3)" @@ -312,7 +333,7 @@ state_machine: when: "a slot filled and proceeds/results are ready to collect" to: collect - from: collect - when: "only resting or closed slots remain; cadence continues" + when: "only resting slots remain (closed slots are released to free by the same pass); cadence continues" to: wait - from: collect when: "session GP target reached" @@ -424,6 +445,7 @@ actions: withdraw: "from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds — per slot" reconcile: "SIDE-AWARE per-slot ledger conservation check BEFORE any slot reuse — buy: quoted == filled + still_open + cancelled_unfilled and cash_quoted == spent + still_committed + refunded; sell: quoted == sold + still_open + returned_unsold" decide: "buy-filled/buy-cancelled -> ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release); sell-filled/sell-cancelled -> closed | ready-to-requote" + archive: "at 'closed' the completed cycle's ledger is reconciled and ARCHIVED (immutable); the pass then releases the physical slot to free (closed -> free) so the SAME slot places the next flip's buy — the two-rotation reuse regression proves the archived record survives rotation 2 (governor 10471)" log: "RESULT item, qty, net gp per slot (floored+capped tax)" on_fail: alert anti_ban: @@ -435,7 +457,7 @@ actions: target: "current location" params: duration: "[60, 240] seconds of idle AFK per break (matches run_control.break.for)" - safe: "only when every slot is resting (buy-open / sell-open) or collected (closed) — resting offers persist server-side at their quoted edges and the per-slot record persists locally; never break mid-collect (coins in hand) or mid-place (offer half-configured)" + safe: "only when every slot is resting (buy-open / sell-open) or free — resting offers persist server-side at their quoted edges and the per-slot record persists locally; a closed slot is transient (archived + released to free by the pass), so breaks never catch the activity between archive and release; never break mid-collect (coins in hand) or mid-place (offer half-configured)" resume: "re-open GE (a2), re-check offer state (a6), re-run the scheduler pass (schedule), continue each slot from its persisted sub-state (a4/a7/a8/a9)" on_fail: skip @@ -496,8 +518,12 @@ run_control: # # t5 pass 4: iron sell-filled-awaiting-collect -> ge_collect (a9): withdraw # 1,570,000; tax floor(157 x 0.02) = 3/item -> net (157 - 3 - 145) x 10,000 - # = 90,000. proceeds_in_box -> cash_collected; ledger reconciled; - # sub-state -> closed, slot freed. Conservation: quoted 10,000 == sold 10,000. + # = 90,000. proceeds_in_box -> cash_collected; ledger reconciled and + # ARCHIVED (the immutable cycle record: quoted 10,000 == sold 10,000); + # sub-state -> closed, and the SAME pass's archival dispatch releases the + # physical slot: closed -> free (governor 10471). The slot can place the + # next flip's buy immediately — it is never stranded after one flip. + # Conservation: quoted 10,000 == sold 10,000, zero proceeds left uncollected. # # t6 LIMIT RESET, NOT AN OFFER EVENT (finding 1): arrowtips reaches # limit_resets_at with 6,000 still open on the resting offer. The window @@ -516,7 +542,19 @@ run_control: # t8 pass 6: arrowtips ready-to-sell -> sell-open 10,000 @ 244 -> full fill -> # sell-filled-awaiting-collect -> pass 7 ge_collect: proceeds 2,440,000; # tax floor(244 x 0.02) = 4/item -> net (244 - 4 - 223) x 10,000 = 170,000 - # (matches the 170,000 4h/slot table row) -> closed. + # (matches the 170,000 4h/slot table row) -> closed (ledger archived) -> + # closed -> free: the arrowtips physical slot is released for the next flip. + # + # t8b SECOND ROTATION in the SAME physical slot (governor 10471 regression): + # the iron slot released at t5 and the arrowtips slot released at t8 are + # free and dispatchable again — pass 8 quotes a NEW arrowtips buy in the + # iron slot (free -> buy-open, a4; buy_capacity_ok re-checked against the + # CURRENT window's limit_ledger). Rotation 1's archived ledgers (iron + # closed record, arrowtips closed record) remain archived and bit-for-bit + # untouched — no loss, duplication, or mutation — while the live ledger is + # a fresh rotation-2 record (quoted 10,000, cash_quoted 2,230,000, + # still_open 10,000). This is the committed slot_reuse regression: a slot + # completes one flip, is released, and runs the next one. # # t9 STALE-EDGE recovery (cosmic, finding 1 cancel trigger): at a cadence check # the cosmic LOW edge has risen 122 -> 126: net 128 - floor(128 x 0.02)=2 - @@ -566,6 +604,7 @@ risk: - "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. BUY cancels return filled items + refunded CASH (the unfilled quantity is cash, never 'refunded units'); SELL cancels return unsold units + proceeds received. Reuse a slot only after ge_collect + side-aware ledger reconciliation (conservation invariants)." - "The 4h buy limit is independent of offer life: cancelling does not renew it and the reset does not cancel offers. A slot at limit_used == limit cannot place a new/requoted BUY until limit_resets_at (first_fill_ts + 4h); its resting offer is unaffected." - "Breaks are safe only when no slot is dispatchable (resting offers or post-collect points, see a10 and break_resume); state = GE offers + the persisted per-slot record, which survive the pause." + - "A completed cycle NEVER strands a slot: after ge_collect the ledger is reconciled + archived at 'closed' and the scheduler pass releases the physical slot to free (closed -> free archival release), so every slot keeps running flip after flip. Never model a slot as one-flip-then-done (governor 10471)." sources: - "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h window starting on the FIRST purchase of the window; connected limits; verified 2026-08-27)" @@ -576,7 +615,8 @@ sources: - "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)" - "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6)" - "governor review comment 10390 on PR #7 (2026-08-28): the 4h limit reset never cancels offers — resting offers remain open while the margin is positive (finding 1); filled offers must pass through explicit *-filled-awaiting-collect states and ge_collect before reuse/close (finding 2); side-aware ledgers kill the phantom-units error (finding 3); side-aware conservation with negative fixtures, including sale-before-collection and close-before-proceeds-collection (finding 4)" - - "owner decisions 2026-08-28 (guidance comments 10264, 10271, 10334): capped/floored global tax formula; complete lifecycle contract; authorisation to keep repairing this same branch/PR" + - "governor review comment 10471 on PR #7 (2026-08-29): a completed flip must NOT permanently strand the GE slot — after proceeds are collected and the completed ledger is archived/reconciled the physical slot returns to free via the archival transition closed -> free (also for a deliberately closed cancelled sell); committed two-rotation regression (buy/collect/sell/collect -> archive -> second buy in the same slot) with the archived ledger preserved" + - "owner authorisations 2026-08-28/29 (guidance comments 10264, 10271, 10334, 10527/10528): capped/floored global tax formula; complete lifecycle contract; continued full-lifecycle repair on the same branch/PR (retry-budget superseded)" - "docs/action-grammar.md (ge_cancel/ge_collect rows, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)" verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27 diff --git a/docs/action-grammar.md b/docs/action-grammar.md index ef23cd6..09cd594 100644 --- a/docs/action-grammar.md +++ b/docs/action-grammar.md @@ -70,7 +70,7 @@ Legend for the mapping table: | `ge_sell` | sell `qty` of `item`, confirm | — | — | — | | `ge_cancel` | abort an offer (quote_age_policy only — stale edge or strategy reallocation, never a 4h limit-window reset): BUY — filled items + refunded CASH (unspent remainder) enter the GE COLLECTION BOX; SELL — unsold units + proceeds received so far enter the box; NEVER directly to inventory; the 4h buy limit is NOT renewed | — | — | — | | `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — | -| `ge_collect` | withdraw from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds; reconcile the SIDE-AWARE per-slot ledger (buy: quoted = filled + still_open + cancelled_unfilled; sell: quoted = sold + still_open + returned_unsold) before any slot reuse | — | — | — | +| `ge_collect` | withdraw from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds; reconcile the SIDE-AWARE per-slot ledger (buy: quoted = filled + still_open + cancelled_unfilled; sell: quoted = sold + still_open + returned_unsold) before any slot reuse; a completed cycle closes with the ledger reconciled + ARCHIVED and the same pass releases the physical slot to free (`closed -> free` archival transition), so the slot immediately runs the next flip (governor 10471) | — | — | — | | `ge_check_price` | read current buy/sell price of an item | — | — | — | ## F. Combat diff --git a/tools/validate.py b/tools/validate.py index c11d0a4..6b8dab8 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -38,6 +38,13 @@ Runs: refunded units; sell: quoted = sold + still_open + returned_unsold) with phase gates (a full buy cannot sell before collection, a full sell cannot close before proceeds collection); + closed has ONE outgoing transition — the archival release + ``closed -> free`` — so a completed cycle's physical GE slot + returns to free for the next flip instead of stranding forever + (governor 10471); the two-rotation reuse regression proves a slot + completes buy/collect/sell/collect, archives the first ledger, and + places a second buy in the same slot without loss, duplication, or + mutation of the archived record; economy.ceiling_gp_per_hour is the TYPED economic ceiling and 0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling. - Regression fixtures (--selftest): a fractional-tax flipping @@ -133,7 +140,7 @@ SLOT_SUB_STATES = [ "sell-filled-awaiting-collect", # sell FULLY filled — proceeds sit in the box until ge_collect "sell-cancelled-awaiting-collect", # aborted sell: proceeds + unsold units in the box "ready-to-requote", # collected units/refunds; requote or abandon - "closed", # slot finished, ledger reconciled, nothing held + "closed", # completed cycle: ledger reconciled + ARCHIVED; the physical slot is released to free via the closed -> free archival transition (governor 10471) ] # Dispatch table: the pure action a slot in a given sub-state needs this pass. SLOT_DISPATCH = { @@ -146,7 +153,7 @@ SLOT_DISPATCH = { "sell-filled-awaiting-collect": "collect", "sell-cancelled-awaiting-collect": "collect", "ready-to-requote": "requote", - "closed": None, + "closed": "release", # archival release: closed -> free (governor 10471) } @@ -155,7 +162,12 @@ def scheduler_dispatch(sub_state): A None (resting) slot is left completely untouched — the pass must not close, advance, or mutate it. This is the property the worked-example - regression proves. + regression proves. A 'closed' slot is NOT resting: its completed + cycle's ledger was archived at ge_collect time and the pass releases + the physical slot to free ('release' = the closed -> free archival + transition), so the slot can run another flip instead of stranding + forever (governor 10471). Resting states are buy-open / sell-open + only. """ return SLOT_DISPATCH.get(sub_state) @@ -334,6 +346,120 @@ def buy_capacity_ok(limit_used, limit, qty_requested): return limit_used + qty_requested <= limit +# Deterministic ledger frames for ONE physical GE slot across TWO full +# flips (governor 10471): the fixture walks a complete buy/collect/ +# sell/collect cycle, archives the first ledger at `closed`, releases the +# physical slot to `free`, and opens a SECOND buy in the SAME slot. Every +# frame must satisfy both conservation checks; the archived first-cycle +# record must survive rotation 2 untouched (no loss, duplication, or +# mutation). Prices: Rune arrowtips buy 223 / sell 244 / tax +# min(floor(244*0.02), 5M) = 4 / proceeds 2,440,000 (snapshot 2026-08-27, +# matches the worked example). +_CYCLE_ITEM = "Rune arrowtips" + +_CYCLE_FRAMES = [ + # (sub_state, ledger) — ledger is side-aware and conservation-clean. + ("free", {"side": "buy", "quoted": {}, "filled": {}, "still_open": {}, + "cancelled_unfilled": {}, "cash_quoted": 0, "spent": 0, + "still_committed": 0, "refunded": 0}), + ("buy-open", {"side": "buy", "quoted": {_CYCLE_ITEM: 10_000}, + "filled": {}, "still_open": {_CYCLE_ITEM: 10_000}, + "cancelled_unfilled": {}, "cash_quoted": 2_230_000, + "spent": 0, "still_committed": 2_230_000, "refunded": 0}), + ("buy-filled-awaiting-collect", + {"side": "buy", "quoted": {_CYCLE_ITEM: 10_000}, + "filled": {_CYCLE_ITEM: 10_000}, "still_open": {}, + "cancelled_unfilled": {}, "cash_quoted": 2_230_000, + "spent": 2_230_000, "still_committed": 0, "refunded": 0}), + ("ready-to-sell", + {"side": "buy", "quoted": {_CYCLE_ITEM: 10_000}, + "filled": {_CYCLE_ITEM: 10_000}, "still_open": {}, + "cancelled_unfilled": {}, "cash_quoted": 2_230_000, + "spent": 2_230_000, "still_committed": 0, "refunded": 0}), + ("sell-open", {"side": "sell", "quoted": {_CYCLE_ITEM: 10_000}, + "sold": {}, "still_open": {_CYCLE_ITEM: 10_000}, + "returned_unsold": {}, "proceeds_in_box": 0, + "cash_collected": 0}), + ("sell-filled-awaiting-collect", + {"side": "sell", "quoted": {_CYCLE_ITEM: 10_000}, + "sold": {_CYCLE_ITEM: 10_000}, "still_open": {}, + "returned_unsold": {}, "proceeds_in_box": 2_440_000, + "cash_collected": 0}), + ("closed", {"side": "sell", "quoted": {_CYCLE_ITEM: 10_000}, + "sold": {_CYCLE_ITEM: 10_000}, "still_open": {}, + "returned_unsold": {}, "proceeds_in_box": 0, + "cash_collected": 2_440_000}), # ARCHIVED here + ("free", {"side": "buy", "quoted": {}, "filled": {}, "still_open": {}, + "cancelled_unfilled": {}, "cash_quoted": 0, "spent": 0, + "still_committed": 0, "refunded": 0}), # released: physical slot reusable + # rotation 2: the SAME physical slot opens a second buy (ledger reset; + # the archived rotation-1 record above must remain untouched). + ("buy-open", {"side": "buy", "quoted": {_CYCLE_ITEM: 10_000}, + "filled": {}, "still_open": {_CYCLE_ITEM: 10_000}, + "cancelled_unfilled": {}, "cash_quoted": 2_230_000, + "spent": 0, "still_committed": 2_230_000, "refunded": 0}), +] + +# The canonical complete flip cycle: rotation 1 (buy/collect/sell/collect) +# plus the archival release and the start of rotation 2. +_CYCLE_PATH = [f[0] for f in _CYCLE_FRAMES] + +# Index of the `closed` frame (archived ledger) and the frames around it. +_CLOSED_IDX = _CYCLE_PATH.index("closed") + + +def slot_reuse_violations(transitions): + """Two-rotation slot-reuse regression (governor 10471). + + Walks ``_CYCLE_PATH`` through the entry's ``slot_transitions`` and + validates every ledger frame against both conservation gates. A + ``closed`` state with no outgoing transition (or one that does not + release to ``free``) strands the physical GE slot after one flip — + the exact defect this regression exists to forbid. Errors are + returned as a list of strings. + """ + out = [] + for a, b in zip(_CYCLE_PATH, _CYCLE_PATH[1:]): + if b not in (transitions.get(a) or []): + out.append( + f"slot cycle: transition {a} -> {b} is missing — a completed " + f"flip must end by releasing the physical slot to free " + f"(archival release closed -> free), otherwise the slot " + f"performs one flip and then becomes permanently unusable") + for state, record in _CYCLE_FRAMES: + rec = dict(record, sub_state=state) + con = conservation_violations(rec) + if con: + out.append(f"slot cycle: conservation fails at '{state}': {con}") + ph = slot_phase_violations(rec) + if ph: + out.append(f"slot cycle: phase gate fails at '{state}': {ph}") + return out + + +def archived_ledger_preserved_after_second_buy(): + """The rotation-1 archived ledger survives rotation 2 intact. + + ``closed`` archives a deep copy of the completed cycle's ledger; when + the same physical slot is released to free and opens a second buy, the + archived record must be bit-for-bit unchanged (no loss, duplication, + or mutation) while the live ledger is a fresh empty record. + """ + archived = json.loads(json.dumps(_CYCLE_FRAMES[_CLOSED_IDX][1])) + archived_state = _CYCLE_FRAMES[_CLOSED_IDX][0] + live = {k: (dict(v) if isinstance(v, dict) else v) + for k, v in _CYCLE_FRAMES[-1][1].items()} # rotation-2 buy-open ledger + if archived != _CYCLE_FRAMES[_CLOSED_IDX][1]: + return ["slot cycle: archived rotation-1 ledger was MUTATED by rotation 2"] + if archived_state != "closed": + return [f"slot cycle: unexpected archived state {archived_state!r}"] + # The live ledger at rotation-2 buy-open must be a fresh record, not a + # copy of the archived one: quoted cash/units belong to rotation 2. + if live.get("cash_quoted") != 2_230_000 or live.get("proceeds_in_box"): + return ["slot cycle: rotation-2 live ledger is not a fresh record"] + return [] + + def ge_lifecycle_errors(rel, data): """GE-flip lifecycle gates (flipping/merchanting entries only): @@ -342,6 +468,9 @@ def ge_lifecycle_errors(rel, data): - the slot model is per-slot asynchronous: slot_state_machine must declare exactly the canonical sub-states, valid transitions, a scheduler_iteration, persisted_state, conservation and break_resume; + 'closed' is NOT a sink: it must release the physical slot via the + archival transition closed -> free, and the committed two-rotation + reuse regression (slot_reuse_violations) must pass; - rates: ceiling_gp_per_hour is the typed economic ceiling and 0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling. """ @@ -392,11 +521,16 @@ def ge_lifecycle_errors(rel, data): errors.append(f"{rel}: slot transition '{from_s}' -> unknown " f"sub-state '{to_s}'") for state in SLOT_SUB_STATES: - if state != "closed" and not trans.get(state): + if not trans.get(state): errors.append(f"{rel}: slot sub-state '{state}' has no transitions " - f"(closed is the only sink)") - if trans.get("closed"): - errors.append(f"{rel}: 'closed' is a sink — it must have no outgoing transitions") + f"— every state must be able to advance; a closed " + f"slot must release the physical slot via the " + f"archival closed -> free transition (governor 10471)") + if list(trans.get("closed") or []) != ["free"]: + errors.append(f"{rel}: 'closed' must have exactly the archival release " + f"['free'] — a closed slot with no outgoing transition " + f"performs one flip and then becomes permanently " + f"unusable (governor 10471)") whens = [str(w) for w in ssm.get("slot_transition_whens", []) or []] for w in whens: arrow = w.split("->") @@ -408,6 +542,10 @@ def ge_lifecycle_errors(rel, data): "limit_ledger", "conservation", "break_resume"): if not str(ssm.get(field, "")).strip(): errors.append(f"{rel}: slot_state_machine.{field} must be declared") + # Two-rotation reuse regression (governor 10471): every flipping entry's + # encoded machine must complete buy/collect/sell/collect, release the + # physical slot to free, and support a second buy in the same slot. + errors += [f"{rel}: {e}" for e in slot_reuse_violations(trans)] return errors @@ -574,7 +712,7 @@ def run_selftest(verbose=False): "sell-filled-awaiting-collect": ["closed"], "sell-cancelled-awaiting-collect": ["ready-to-requote", "closed"], "ready-to-requote": ["buy-open", "sell-open", "free"], - "closed": [], + "closed": ["free"], # archival release: physical slot reusable (governor 10471) }, "slot_transition_whens": [ "free -> buy-open: quote placed", @@ -588,9 +726,10 @@ def run_selftest(verbose=False): "ready-to-sell -> sell-open: sell offer placed", "sell-open -> sell-filled-awaiting-collect: sell FULLY filled — proceeds sit in the box until ge_collect", "sell-open -> sell-cancelled-awaiting-collect: abort on stale edge or strategy reallocation", - "sell-filled-awaiting-collect -> closed: proceeds collected and ledger reconciled", + "sell-filled-awaiting-collect -> closed: proceeds collected and ledger reconciled + ARCHIVED", "sell-cancelled-awaiting-collect -> ready-to-requote: collected, requote", - "sell-cancelled-awaiting-collect -> closed: collected, slot closed", + "sell-cancelled-awaiting-collect -> closed: collected, ledger archived, slot closed (exit taken)", + "closed -> free: archival release — completed cycle's ledger stays archived; the physical GE slot is reusable for the next flip", "ready-to-requote -> buy-open: requoted buy", "ready-to-requote -> sell-open: requoted sell", "ready-to-requote -> free: abandoned", @@ -630,6 +769,26 @@ def run_selftest(verbose=False): slot_transition_whens=ssm_ok["slot_transition_whens"][:4]))) check("flip lifecycle: a sub-state without transitions (ready-to-sell missing) is rejected", any("no transitions" in e for e in errs)) + errs = ge_lifecycle_errors("bad5.yaml", dict(flip_ok, slot_state_machine=dict( + ssm_ok, slot_transitions=dict(ssm_ok["slot_transitions"], closed=[])))) + check("flip lifecycle: a 'closed' sink (no archival release) is rejected — the physical slot would perform one flip and strand", + any("archival release" in e for e in errs)) + + # --- slot reuse through rotations (governor 10471) --- + check("slot cycle: the archival release closed -> free is a dispatched scheduler action", + scheduler_dispatch("closed") == "release") + check("slot reuse: one full buy/collect/sell/collect lifecycle completes and the SAME physical slot opens a second buy (two-rotation walk passes)", + slot_reuse_violations(ssm_ok["slot_transitions"]) == []) + check("slot reuse: the rotation-1 archived ledger survives rotation 2 intact (no loss, duplication, or mutation)", + archived_ledger_preserved_after_second_buy() == []) + sink_trans = dict(ssm_ok["slot_transitions"], closed=[]) + check("slot reuse: a machine whose closed has no outgoing transition FAILS the two-rotation regression (one flip, then permanently unusable)", + any("permanently unusable" in v + for v in slot_reuse_violations(sink_trans))) + closed_no_free = dict(ssm_ok["slot_transitions"], closed=["ready-to-sell"]) + check("slot reuse: a closed that does not release to free FAILS the two-rotation regression", + any("closed -> free" in v + for v in slot_reuse_violations(closed_no_free))) return errors