codex: rate_model lifecycle derivation + executable HLA tier selection (bounce-fix attempt 5, owner guidance 10331, issue #5)

This commit is contained in:
Claude 2026-08-28 04:04:03 -05:00
parent 34f36d2275
commit 589767b195
4 changed files with 1019 additions and 318 deletions

View File

@ -3,32 +3,39 @@ title: Low-Level Alchemy (F2P)
domain: economy
tags: [alching, magic-training, f2p, loss-training]
status: verified
version: 2 # 2: bounce-fix 3 2026-08-28 (owner guidance) — realized rates incl. mandatory breaks + banking, break.every in CAST counts, numeric carried_batch/restock_threshold, pinned worked example
version: 3 # 3: bounce-fix 4 2026-08-28 (owner guidance 10331, PR #10) — explicit rate_model (session-realized rates incl. startup over a 2h horizon: xp 23,000, gp -109,500..-65,600), complete GE acquisition path (open -> fill -> collect) for the initial buy AND every restock (bank-stock fast path), fail-closed staff equip, monotonic break example (550/1,100/1,650 independent of 901-cast restocks), assets-based capital stops
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 F2P items with the highest
lowalch-value among affordable, high-limit items. At current prices it is
a LOSS activity (money in exchange for Magic XP); run it only for XP.
Model pin (owner guidance 2026-08-28): one iteration = one cast; breaks
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 — worked
example pinned below; all rates are REALIZED (delays + breaks + banking).
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; all rates are SESSION-REALIZED under the
declared rate_model (startup + restock + breaks included; see economy).
accounts:
membership: f2p
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)"
- "A bank with GE access within reach (alch spot = the bank booth; restock trip 60-120s)"
- "Working capital >= 292,000 gp for one carried_batch (1,000 x 148 gp gold necklaces + 1,000 x 144 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). Equip failure is FAIL-CLOSED: bounded retries (3), then log ERROR + alert + stop — there is no fire-rune fallback in this model"
- "A bank with GE access within reach (alch spot = the bank booth; restock trip [60, 120] s)"
- "Starting capital >= ~1.30M gp CASH (staff hard cap 5,000 + first batch 1,000/1,000 at MAX allowed fill 306,600 + session loss reserve 684,400 = 6 worst-case 901-cast batches at live-low+5% fills (covers Magic 21 -> 54) + resting-offer float 306,600). The capital stop checks TOTAL ASSETS (cash + supplies at current buy cost), never raw cash — cash legitimately converts into supplies mid-run"
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: no requirements row in the infobox, none in the prose — contrast Battlestaff 'requires 30 Attack and 30 Magic'); Low Level Alchemy (Cost)"
techniques:
- name: cast on noted (stacked) items
@ -41,31 +48,40 @@ best_practice:
economy:
# Verified 2026-08-27 against prices.runescape.wiki (live 'latest')
# + OSRS wiki Low_Level_Alchemy infobox.
# Bounce-fix 3 (2026-08-28, owner guidance): all rate fields are REALIZED
# figures — mandatory breaks + banking included, not just the cast loop.
# Bounce-fix 4 (2026-08-28, owner guidance 10331): all rate fields are
# SESSION-REALIZED — startup + restock + mandatory breaks included over
# the declared 2 h session horizon (rate_model).
item: Gold necklace # example anchor; rank by cheapest candidate live
item_id: 1654
cost_per_cycle: 292 # necklace buy 148 + nature rune 144 (staff of fire)
cost_per_cycle: 292 # necklace buy 148 + nature rune 144 (staff of fire, live low)
income_per_cycle: 180 # lowalch of gold necklace (40% of value 450)
net_per_cycle: -112
gp_per_hour_low: -110500 # realized (incl. restock + breaks), fastest cycle: 986.4 casts/hr x -112 = -110,477
gp_per_hour_high: -74000 # realized (incl. restock + breaks), slowest cycle: 660.6 casts/hr x -112 = -73,987
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) ----
# Cast: 1.8s (3 ticks x 0.6s) + anti_ban.delay [1.6, 3.4]s ADDITIVE after the
# action (docs/action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
gp_per_hour_low: -109500 # session-realized fast bound: 977.1 casts/hr x -112 = -109,435 (3.4 s cycle, 300 s startup, 2 h horizon)
gp_per_hour_high: -65600 # session-realized slow bound: 585.9 casts/hr x -112 = -65,621 (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 (derived; enforced by tools/validate.py rate_model_errors) ----
# Cast: 1.8s (3 ticks x 0.6s, rate_model.cast_ticks) + anti_ban.delay
# [1.6, 3.4]s ADDITIVE (action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
# BATCH model: carried_batch 1,000 candidate + 1,000 natures; restock_threshold
# 100 -> 901-cast restock cadence (first restock after cast 901). Overhead per
# 901-cast batch: 90s restock trip (60-120s, GE-adjacent booth) + 135s breaks
# (every [400, 700] casts, mid 550, for [45, 120]s -> 901/550 x 82.5s) = 225s.
# 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).
# gp_per_hour = casts/hr x -112: fast -110,477 (low), slow -73,987 (high),
# midpoint -88,626.
# Superseded figures (fail the realized gate on purpose): loop-only 837
# casts/hr (25,953 xp/hr) ignored breaks + banking; the 8-14-cast break
# regime (~305 casts/hr) and the 1,800-cast regime ignored the additive delay.
# 100 -> 901-cast restock cadence. Overhead per 901-cast batch: restock trip
# [60, 120]s (mid 90) + breaks (every [400, 700] casts, mid 550, for
# [45, 120]s, mid 82.5s -> 82.5 x 901/550 = 135.2s).
# STEADY-STATE casts/hr = 901 x 3600 / (901 x cycle + trip + break_share x 901):
# fast 3.4s -> 1,019.7; midpoint 4.3s -> 791.3; slow 5.2s -> 639.2.
# SESSION-REALIZED (2 h horizon = 7,200 s; startup 300-600 s, mid 450 s ->
# 450/7200 = 6.25 % amortised): 977.1 / 741.8 / 585.9 casts/hr.
# xp_per_hour = 741.8 x 31 = 22,995 (pin 23,000);
# gp_per_hour = casts/hr x -112: low -109,435, high -65,621 (mid -83,082).
# BATCH ECONOMICS (901 casts; deposits 901 x 180 = 162,180 gp per batch):
# input cost at live low: 901 x 292 = 263,092 -> net -100,912/batch.
# input cost at MAX fill (live low + 5 %: 1,000 x 155.4 + 1,000 x 151.2 =
# 306,600 per batch): 901 x 306.6 = 276,247 -> net -114,067/batch (worst
# case). Reserve = 6 worst-case batches (Magic 21 -> 54) = 684,400 gp;
# the capital stops check TOTAL ASSETS, not raw cash (accounts + stop_conditions).
# Superseded figures (all fail the derived gate on purpose): loop-only 837
# casts/hr (25,953 xp/hr); the 8-14-cast-break regime (~305 casts/hr); the
# 1,800-cast regime; the pre-startup realized 24,500 (791.3 x 31) — startup
# amortisation is part of the model.
supplies:
- item: gold necklace
qty_per_cycle: 1
@ -79,54 +95,86 @@ economy:
profit_drivers:
- "NONE at current prices — XP is the product, not GP"
- "Pick the candidate with the least-negative net each run (live rank)"
- "Batch-scale economics (worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = 901 x -112 = -100,912 gp, +27,931 xp"
- "Batch-scale economics (worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = 901 x -112 = -100,912 gp at live-low fills (-114,067 at MAX allowed fills), +27,931 xp, deposits 162,180 gp"
# Explicit activity/rate discriminator (owner guidance 2026-08-28, bounce 4):
# this entry OPTS IN to the derived-rate gate; every rate derives from the
# entry's own declared lifecycle and the validator re-derives it.
rate_model:
anchor_tier: la # xp_per_hour pins the LA tier's realized rate
cast_ticks: 3 # LA is a 3-tick cast -> 1.8 s (0.6 s/tick)
restock_trip_s: [60, 120] # GE-adjacent booth trip per restock
session_horizon_s: 7200 # 2 h session; startup is amortised over it
startup_s: [300, 600] # one-time GE acquisition + loadout
alch_carry_batch: 1000 # stacked candidates + natures
alch_restock_threshold: 100 # -> 901-cast LA restock cadence
state_machine:
initial: idle
terminal: [complete, failed]
states:
idle:
goal: "reach the bank, 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 (a4/a5); 3 consecutive no-fill rounds -> failed. An offer still unfilled at session end is left resting by design — its coins are tied up (float reserve) and return when it fills or the operator aborts it (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 — deposit overflow, withdraw only the shortfall)"
next: [casting, failed]
casting:
goal: "cast Low Level Alchemy on the current candidate item — each cast consumes 1 candidate + 1 nature and pays 180 gp (lowalch), +31 xp, net -112 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts)"
goal: "cast Low Level Alchemy on the current candidate item (a8) — each cast consumes 1 candidate + 1 nature and pays 180 gp (lowalch), +31 xp, net -112 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts, via a12); the counter is NEVER reset by restocking"
next: [bank_supplies, complete, failed]
bank_supplies:
goal: "deposit coins, 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 (a9 — bank-stock fast path when the bank already holds enough); for the remaining deficit, open GE (a2), buy the deficit (a10, live low + 5%), collect it (a11) — 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 54, hand off to HLA at 55)"
goal: "reached the run's XP/level target (default 54, hand off to HLA at 55) or the 2 h session horizon"
failed:
goal: "hard failure — stop, log ERROR sentinel, alert"
transitions:
# Handoff (verified 2026-08-27 from the OSRS wiki): the LA page states
# "Players with level 55 Magic are generally recommended to use High
# Level Alchemy instead" (65 vs 31 xp/cast, 60% vs 40% of item value).
# Superheat Item unlocks at 43 (53 xp/cast) — an optional window if
# ore-smelting margins justify it. Handoffs are soft efficiency choices,
# not hard caps (all spells stay castable); see
# codex/training/magic-alch-handoff.yaml.
- 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 (default 54, hand off to HLA at 55)"
when: "magic level >= target (54, hand off to HLA at 55) at session start"
to: complete
- from: idle
when: "3 consecutive purchase/equip failures with no fallback available (recovery block)"
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 (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 (default 54, hand off to HLA at 55)"
when: "magic level >= target (54, hand off to HLA at 55)"
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)"
when: "restocked to carried_batch (1,000 candidate + 1,000 nature) — 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
actions:
- id: a1
@ -141,18 +189,76 @@ 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/a10) or collects (a6/a11) 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)"
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: [1200, 3000]
- id: a4
step: buy-supplies
verb: ge_buy
target: grand-exchange
params:
item: "current least-loss live candidate (default: gold necklace)"
qty: "carried_batch = 1,000 (fits the 4h GE limit window of 18,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]
- id: a3
- 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 %"
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 '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: cast-alch
verb: use_item_on
target: "candidate item in inventory"
@ -163,102 +269,144 @@ actions:
consumption: "1 candidate + 1 nature per cast (staff of fire default); pays 180 gp (lowalch) + 31 xp, net -112 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 = 3.4-5.2s, mid 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 (rate_model)
- id: a4
step: bank-loop
- id: a9
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 (a10) and collected (a11)"
on_fail: alert
anti_ban:
delay: [500, 1100]
- id: a5
- id: a10
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 a9 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: a11
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: a12
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:
iteration: "one cast" # schema.md: break.every counts iterations, so every
# value below is in completed casts, NOT bank trips
# (owner decision 2026-08-28)
max_iterations: null
iteration: "one cast" # = one Low Level Alchemy cast (+31 xp, -112 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: [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
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 (default 54, hand off to HLA at 55)"
- "gp under floor (working capital below one carried_batch = 292,000 gp)"
- "magic level >= target (54 -> hand off to HLA at 55)"
- "session elapsed >= 2 h (session horizon — startup is amortised over this window in the rate model)"
- "total assets (cash + supplies at current buy cost) < 311,600 — cannot fund the next batch at max fill (306,600) plus staff replacement (5,000); assets, NOT raw cash: cash that became supplies is not a loss"
- "session net loss (total assets - initial capital) > 684,400 — 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).
# 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.
#
# Step 0 — before the first inventory: account holds >= 292,000 gp working capital
# (1,000 x 148 + 1,000 x 144). At the GE bank: GE-buy staff of fire, Wield it
# (no documented requirement), GE-buy 1,000 gold necklaces (noted, stacked) +
# 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 (-292 gp) and
# pays 180 gp (lowalch) + 31 xp -> net -112 gp. Cast counter 0 -> 550 (~39 min at
# midpoint cadence).
# Step 2 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every [400, 700]):
# 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: 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 180 = 162,180 gp; batch net -100,912 gp;
# batch xp 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 (the 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 54+ -> hand off to HLA at 55; 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.30M
# gp cash (staff cap 5,000 + first batch max 306,600 + reserve 684,400 +
# float 306,600). At the GE bank: open GE (a2); place offers — staff (a3),
# 1,000 gold necklaces (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 (-292 gp)
# and pays 180 gp (lowalch) + 31 xp -> net -112 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 (a12). 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 180 = 162,180 gp (a9); 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 (a10, live low + 5 %), collect (a11) ->
# inventory back to 1,000/1,000. Batch net: -100,912 gp (live-low fills) or
# -114,067 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 54+ (HLA handoff at
# 55), the 2 h session horizon, the capital stops (assets floor 311,600 or
# net loss > 684,400), 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 (18,000 necklaces / 18,000 natures)."
- "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-120s AFK) and independent of bank visits — never treat a restock trip as a break, and never reset the break counter on restock (gcd(550, 901) = 1)."
- "GE buy bursts: pace offers, respect the 4h limit window (18,000 necklaces / 18,000 natures); offers persist until filled or aborted and a 4h limit reset is NOT an offer expiry."
# --- 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 a4 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 (a2);
# 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: fall back to fire runes (3 fire + 1 nature = +15 gp
# per cast rune cost), log WARN, continue; never loop on equip attempts.
# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout (a2/a4);
# 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert).
# - Explicit completion/stop: stop_conditions above (Magic 54+ HLA handoff, gp floor,
# repeated failures).
# bank_supplies at candidate OR nature < 100) catches both mid-run depletions; the
# restock phase (a9 -> a2 -> a10 -> a11) 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/a11) and the remainder is re-placed at
# the current edge (a3/a4/a5/a10); 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 54+ HLA handoff at 55,
# 2 h session horizon, assets floor 311,600, loss reserve 684,400, repeated
# failures, fail-closed staff equip).
sources:
- "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, F2P; infobox 2026-08-27)"
- "prices.runescape.wiki mapping (id 1654 gold necklace: members=false, limit 18000, lowalch 180, highalch 270, value 450; id 561 nature rune; id 554 fire rune)"
- "prices.runescape.wiki latest 2026-08-27 (necklace buy 148, nature buy 144, fire buy 5)"
- "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 — 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 + lifecycle model; owner guidance 2026-08-28 (realized rates incl. breaks + banking, worked example)"
- "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) — the cadence + break + GE + lifecycle model; owner guidance 2026-08-28 (rate_model, fail-closed staff, monotonic counters)"
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
related:

View File

@ -3,7 +3,7 @@ title: Magic training via alchemy (LA → SII → HLA handoff)
domain: training
tags: [magic-training, alching, f2p, xp-training]
status: verified
version: 2 # 2: bounce-fix 3 2026-08-28 (owner guidance) — dynamic recipe selection (highest-unlocked, target_input, fallback), realized rates incl. mandatory breaks + banking, full-lifecycle actions, worked example pinned
version: 3 # 3: bounce-fix 4 2026-08-28 (owner guidance 10331) — explicit rate_model discriminator (derived per-entry lifecycle rates), per-recipe inventory capacity floor(27/qty) with derived min/mid/max rates pinned in recipes.rates, complete GE acquisition path (open -> fill -> collect) for initial buy AND every restock (bank-stock fast path), fail-closed staff equip (bounded retries, no fire-rune fallback), configurable session target (55 = LA/SII->HLA handoff completion; >55 = executable HLA tier to the target; LA stays the 43-54 fallback), capital model (cash vs supplies, max-fill costs, reserve, assets-based stops)
updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
summary: >
@ -12,44 +12,59 @@ summary: >
ore into bars), then High Level Alchemy from 55 (65 xp/cast, the wiki's
recommended end-state). All three spells are F2P and stay castable after
their unlock levels, so the handoffs are efficiency recommendations, not
hard caps. SII model (owner guidance 2026-08-28): the recipe table is
dynamic and governed by the account's Smithing level — the highest UNLOCKED
recipe with a positive complete-cost margin is selected, its primary ore
(target_input, never coal) is the spell target, and fallback is to the next
lower unlocked recipe, else skip SII and stay on LA. Rates are REALIZED:
mandatory additive delays PLUS breaks (every 400-700 completed casts) PLUS
banking (901-cast alch batches; 24-cast SII bank cycles) — pinned below.
hard caps. The SESSION TARGET is configurable: the default 55 completes at
the LA/SII -> HLA handoff; a target above 55 transitions into the
executable HLA tier and continues until the target — HLA is never
declared-but-unreachable, and Low Alchemy remains the executable fallback
throughout Magic 43-54 whenever SII is skipped. SII model (owner guidance
2026-08-28): a dynamic recipe table governed by the account's Smithing
level — the highest UNLOCKED recipe whose complete recipe cost (ALL inputs
at ALLOWED fill prices, live low + 5%) plus one nature rune stays under the
bar's live GE price is selected; its primary ore (target_input, never coal)
is the spell target; materials unobtainable -> next lower unlocked recipe,
else skip SII and stay on LA. SII inventory cycles are CAPACITY-DERIVED:
floor(27 / total input qty) casts per bank cycle (bronze 13, iron/silver/
gold 27, steel 9, mithril 5, adamantite 3, runite 3) — never a fixed pack.
Rates are SESSION-REALIZED under the declared rate_model: cast cycle +
mandatory additive delays + breaks + banking + startup over a 2 h horizon
(anchors: LA 23,000 / SII steel 12,400 / HLA 48,200 Magic XP/hr; per-recipe
min/mid/max rates pinned in the recipes table and enforced by the
validator). Every purchase follows the complete GE path open -> place ->
fill/wait -> collect, for the initial acquisition and every restock (bank
fast path only when the bank holds stock). Staff-of-fire equip failure is
FAIL-CLOSED (bounded retries, then log ERROR + alert + stop).
accounts:
membership: f2p # LA, SII and HLA are all members=No spells
min_level: 21 # Magic 21 — Low Level Alchemy unlock
requires_quest: []
requires_other:
- "Staff of Fire equipped (drops LA/SII/HLA rune cost to 1 nature rune per cast; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources)"
- "Staff of Fire equipped (drops LA/SII/HLA rune cost to 1 nature rune per cast; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources). Equip failure is FAIL-CLOSED: bounded retries (3), then log ERROR + alert + stop — there is no fire-rune fallback in this model"
- "SII tier requires the selected recipe's Smithing level (recipes table: bronze 1 / iron 15 / silver 20 / steel 30 / gold 40 / mithril 50 / adamantite 70 / runite 85) — the highest UNLOCKED recipe is selected and an unmet gate is NEVER selected (selection.mode)"
- "Working capital >= 150,000 gp for 1,000 nature runes plus the selected recipe's exact inputs (live GE buy; steel example: 1 iron ore 100 + 2 coal 150 each per cast at the 2026-08-27 snapshot)"
- "A bank with GE access within reach (alch spot = the bank booth; restock trips 60-120s)"
- "Starting capital >= ~1.31M gp CASH for the LA/HLA tier path (staff hard cap 5,000 + first batch 1,000/1,000 at MAX allowed fill 307,650 + session loss reserve 690,075 = 6 worst-case 901-cast batches at live-low+5% fills (covers Magic 21 -> 55) + resting-offer float 307,650). The SII window uses capacity-sized packs (steel example: 9 x (1 iron 105 + 2 coal 157.5 + 1 nature 152.25) = 5,150 gp per pack at max fill). The capital stop checks TOTAL ASSETS (cash + supplies at current buy cost), never raw cash — cash legitimately converts into supplies"
- "Session target Magic level configured at session start: default 55 (completes at the LA/SII -> HLA handoff); a target above 55 runs the HLA tier until the target; LA remains the executable fallback throughout 43-54 when SII is skipped"
- "A bank with GE access within reach (alch spot = the bank booth; restock trips [60, 120] s)"
best_practice:
equipment:
- item: staff of fire
why: "unlimited fire runes — the rune cost of every alchemy spell drops to a single nature rune per cast; equip requirement: none documented on the OSRS wiki (verified 2026-08-28)"
why: "unlimited fire runes — the rune cost of every alchemy spell drops to a single nature rune per cast; 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 — High Level Alchemy (rune cost table); OSRS wiki — Staff of fire (id 1387: no requirements row in the infobox, none in the prose — contrast Battlestaff 'requires 30 Attack and 30 Magic')"
techniques:
- name: hand off LA -> HLA at level 55
why: "LA page: 'Players with level 55 Magic are generally recommended to use High Level Alchemy instead, as it provides more coins and experience per cast' — 65 vs 31 xp/cast, 60% vs 40% of item value"
- name: hand off LA -> HLA at level 55 (configurable target)
why: "LA page: 'Players with level 55 Magic are generally recommended to use High Level Alchemy instead, as it provides more coins and experience per cast' — 65 vs 31 xp/cast, 60% vs 40% of item value. The default session target is 55; a target above 55 keeps casting HLA (65 xp/cast) until the configured level — the HLA tier is never declared but unreachable"
source: "OSRS wiki — Low Level Alchemy (Comparison with High Level Alchemy); High Level Alchemy (infobox: level 55, 65 xp)"
- name: optional Superheat Item window 43-55
why: "SII unlocks at 43 (53 xp/cast) and smelts ore into bars without a furnace — a training window that can profit when ore/bar margins are positive; it cannot be cast on noted ore, so it needs the exact recipe inputs in inventory rather than a note stack"
why: "SII unlocks at 43 (53 xp/cast) and smelts ore into bars without a furnace — a training window that can profit when ore/bar margins are positive; it cannot be cast on noted ore, so it needs the exact recipe inputs in inventory rather than a note stack. The inventory holds floor(27 / total input qty) casts per bank cycle (steel = 9) — never a fixed 24-cast pack"
source: "OSRS wiki — Superheat Item (43 Magic, 53 xp, F2P; 'smelt ore into bars without a furnace'; 'cannot be cast on noted ore')"
- name: select the highest UNLOCKED SII recipe, never an unmet gate
why: "the recipe table is dynamic: at each cast the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost (ALL inputs at live GE buy + one nature rune) stays under the bar's GE price; the spell targets that recipe's declared target_input (the primary ore — never coal); when the selected recipe's materials cannot be obtained, fall back to the next lower unlocked recipe; when none has a positive margin, skip SII and stay on LA"
source: "OSRS wiki — Superheat Item (requires the Smithing level and materials of the bar); Smithing (bar smelting table); prices.runescape.wiki (live GE prices); owner guidance 2026-08-28 (selection.mode)"
why: "at each cast the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost (ALL inputs at ALLOWED fill prices = live low + 5%) + one nature rune at allowed fill stays under the bar's live GE price; the spell targets that recipe's declared target_input (the primary ore — never coal); when the selected recipe's materials cannot be obtained, fall back to the next lower unlocked recipe; when none has a positive margin, skip SII and stay on LA (LA is executable throughout 43-54)"
source: "OSRS wiki — Superheat Item (requires the Smithing level and materials of the bar); Smithing (bar smelting table); prices.runescape.wiki (live GE prices); owner guidance 2026-08-28 (selection.mode, allowed-fill eligibility)"
- name: buy GE items below alch value, cast on noted items at a bank
why: "HLA page: 'Players who train this way can break even on the cost of runes, or even turn a profit, by buying items on the Grand Exchange for a lower cost than the item's High Alchemy value'; all alchemy spells work on noted items, saving bank withdrawals per cast"
source: "OSRS wiki — High Level Alchemy (GE buying for break-even/profit; noted-item casting)"
- name: treat the handoffs as soft efficiency choices
why: "all three spells remain castable past 21/43/55 and stay on the standard F2P spellbook, so the tier switches optimise xp/cast and cost rather than gate the path"
why: "all three spells remain castable past 21/43/55 and stay on the standard F2P spellbook, so the tier switches optimise xp/cast and cost rather than gate the path; LA is the executable fallback whenever the SII window is skipped"
source: "OSRS wiki — Low Level Alchemy; Superheat Item; High Level Alchemy (level + experience infoboxes, members=No)"
economy:
@ -57,34 +72,37 @@ economy:
# staff of fire -> 1 nature rune per cast. Nature rune id 561 buy 145
# (mapping limit 18000, cross-checked against Module:Exchange/Nature_rune,
# members=false, lowalch 72 = 40% of value 180 — consistency assertion).
# Income/net per cast depend on the alched item, so they live in the
# per-activity entries (low-alch-f2p, f2p-alch-catalog): this entry only
# prices the shared consumable. SII recipe economics live in the recipes
# table below.
# Income/net per cast depend on the alched item or the recipe bar, so they
# live in the per-activity entries and in the recipes table (this entry
# prices the shared consumable + the rate model).
# xp_per_hour pins the rate_model ANCHOR tier (sii, steel-bar): the entry's
# own declared lifecycle — no hard-coded per-activity constants.
item: nature rune
item_id: 561
cost_per_cycle: 145 # one nature rune per cast (staff of fire), buy price 2026-08-27
xp_per_hour: 51400 # HLA tier, REALIZED (breaks + banking included), pinned below
# ---- realized-rate derivation (owner guidance 2026-08-28: rates must
# include the mandatory breaks and banking, not just the cast loop) ----
# Cast: 1.8s (3 ticks x 0.6s) + anti_ban.delay [1.6, 3.4]s ADDITIVE after
# the action (docs/action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
xp_per_hour: 12400 # SII anchor, SESSION-REALIZED: steel mid 249.1 casts/hr x 53 x startup factor 0.9375 = 12,379 (pin 12,400)
# ---- rate model (derived from rate_model + actions + run_control + recipes;
# enforced by the validator's rate_model_errors gate) ----
# Cast: 1.8s (3 ticks x 0.6s, rate_model.cast_ticks) + anti_ban.delay
# [1.6, 3.4]s ADDITIVE (action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
# Restock trip [60, 120]s (rate_model.restock_trip_s); breaks every
# [400, 700] casts for [45, 120]s (run_control.break) -> amortised break
# share 45/700 fast, 82.5/550 mid, 120/400 slow.
# ALCH tiers (LA/HLA): 901-cast restock batch (carried_batch 1,000 -
# restock_threshold 100) + 225s overhead per batch (90s restock trip at a
# GE-adjacent booth + 135s breaks every [400, 700] casts, mid 550, for
# [45, 120]s): realized casts/hr = 901 / ((901 x 4.3 + 225) / 3600) = 791.3.
# LA 24,500 xp/hr = 31 x 791.3 (pin 24,530 -> 24,500)
# HLA 51,400 xp/hr = 65 x 791.3 (pin 51,435 -> 51,400)
# SII tier: bank cycle is INVENTORY-bound (bars are non-stackable): restock
# when bars_in_inventory >= 24, i.e. 24-cast cycles with a ~60s trip. Per
# 550-cast break window: 550 x 4.3 = 2,365s casting + 22.9 trips x 60s +
# 82.5s break = 3,822.5s -> realized casts/hr = 550 / (3,822.5/3600) = 518.0.
# SII 27,500 xp/hr = 53 x 518.0 (pin 27,454 -> 27,500; range 24,300 by
# slow cycle 458.6/hr .. 31,500 by fast cycle 595.0/hr)
# Superseded figures (fail the realized gate on purpose): loop-only 837
# casts/hr (54,419 HLA) ignored breaks+banking; the 8-14-cast break regime
# (~305 casts/hr, 19,830 HLA) and the 1,400/1,800-cast regimes ignored the
# additive delay.
# restock_threshold 100), steady-state 1,019.7 / 791.3 / 639.2 casts/hr;
# SESSION-REALIZED over the 2 h horizon with startup 300-600s (factor
# 0.9583/0.9375/0.9167): 977.1 / 741.8 / 585.9 casts/hr ->
# LA 23,000 xp/hr = 31 x 741.8 (22,995)
# HLA 48,200 xp/hr = 65 x 741.8 (48,217)
# SII tiers: per-recipe capacity = floor(27 / total input qty) (steel 9,
# bronze 13, iron/silver/gold 27, mithril 5, adamantite/runite 3); full
# derived min/mid/max rates live in each recipe's rates block (validator-
# checked): steel steady mid 249.1 casts/hr = 13,204 Magic XP/hr; the
# xp_per_hour anchor adds the startup factor (0.9375 mid) -> 12,400.
# Superseded figures (all fail the derived gate on purpose): loop-only
# 837 casts/hr; the 8-14-cast-break regime (~305 casts/hr); the 1,400/
# 1,800-cast regimes; the impossible 24-cast SII pack (518.0 casts/hr) —
# capacity is floor(27/qty), never a fixed pack.
supplies:
- item: nature rune
qty_per_cycle: 1
@ -93,15 +111,18 @@ economy:
profit_drivers:
- "XP per cast by tier (wiki infoboxes): LA 31 (level 21) / SII 53 (level 43) / HLA 65 (level 55); SII also grants the bar's Smithing XP (recipes table)"
- "HLA can break even or profit on GE-bought items priced below their High Alchemy value (HLA page); LA is a loss at current prices (see low-alch-f2p)"
- "SII turns a profit only when the complete recipe cost (ALL inputs + one nature rune) stays under the resulting bar's GE price — re-check margins per window and select the highest UNLOCKED recipe with a positive margin (selection.mode); never a steel-only default"
- "SII turns a profit only when the complete recipe cost (ALL inputs at ALLOWED fill prices + one nature rune at allowed fill) stays under the resulting bar's GE price — re-check margins per window and select the highest UNLOCKED recipe with a positive margin; never a steel-only default, never an unmet Smithing gate"
# Selection model (owner guidance 2026-08-28): the Superheat recipe is chosen
# at runtime from the recipes table — never hardcoded to one bar, never above
# the account's Smithing level, with a deterministic fallback chain.
# the account's Smithing level, with a deterministic fallback chain. The
# validator proves eligibility (smithing gate + complete-recipe margin),
# target_input (declared input, never coal), unique gates, and the fallback
# path via its fixtures.
selection:
mode: highest-unlocked
eligible_rule: "at each cast, the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing level whose complete input cost (ALL inputs at live GE buy) + one nature rune 145 < the bar's live GE price"
fallback: "when the selected recipe's materials cannot be obtained (3 consecutive failed GE purchases / unfillable offers), drop to the next lower unlocked recipe and re-check its margin; when no unlocked recipe has a positive margin, skip SII for this window and stay on LA — never select a recipe whose Smithing requirement is unmet, never cast on coal"
eligible_rule: "at each cast, the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing level whose complete input cost at ALLOWED fill prices (every input at live GE low + 5%, the a4 max_price cap) + one nature rune at allowed fill < the bar's live GE price — the full actual/allowed recipe cost, not a headline ore price"
fallback: "when the selected recipe's materials cannot be obtained (3 consecutive failed GE purchases / unfillable offers), drop to the next lower unlocked recipe and re-check its allowed-fill margin; when no unlocked recipe has a positive margin, skip SII for this window and stay on LA throughout 43-54 — LA remains executable (a8 handles level < 43 AND the 43-54 SII-skip window); never select a recipe whose Smithing requirement is unmet, never cast on coal"
# Concrete F2P bar recipes for Superheat Item. SII requires the bar's
# Smithing level and its exact materials, so every selectable bar is modelled
@ -110,8 +131,14 @@ selection:
# smelting table; Superheat Item grants the bar's Smithing XP per cast).
# target_input = the PRIMARY ORE the spell is cast on (a declared input,
# never coal — the validator enforces both).
# econ 2026-08-27: bar margin = bar GE price - (all inputs at live GE buy +
# one nature rune 145); the client selects via selection.mode.
# rates (validator-checked, derived from the entry's rate_model): capacity =
# floor(27 / total input qty) casts per bank cycle (28-slot inventory minus
# the nature stack); casts_per_hour and magic_xp_per_hour are the STEADY-
# STATE min/mid/max (cycle endpoints 3.4/4.3/5.2s, trip 60/90/120s,
# break share 45/700, 82.5/550, 120/400). The economy.xp_per_hour anchor
# adds the startup factor (see rate_model).
# econ 2026-08-27: bar margin = bar GE price - (all inputs at allowed fill +
# one nature rune at allowed fill); the client selects via selection.mode.
recipes:
bronze-bar:
bar: Bronze bar
@ -128,6 +155,10 @@ recipes:
item_id: 438
qty: 1
source: ge
rates:
capacity: 13
casts_per_hour: {min: 445.6, mid: 316.5, max: 244.4}
magic_xp_per_hour: {min: 23617, mid: 16776, max: 12952}
iron-bar:
bar: Iron bar
bar_item_id: 2351
@ -139,6 +170,10 @@ recipes:
item_id: 440
qty: 1
source: ge
rates:
capacity: 27
casts_per_hour: {min: 633.1, mid: 462.5, max: 362.0}
magic_xp_per_hour: {min: 33554, mid: 24514, max: 19187}
silver-bar:
bar: Silver bar
bar_item_id: 2355
@ -150,6 +185,10 @@ recipes:
item_id: 442
qty: 1
source: ge
rates:
capacity: 27
casts_per_hour: {min: 633.1, mid: 462.5, max: 362.0}
magic_xp_per_hour: {min: 33554, mid: 24514, max: 19187}
steel-bar:
bar: Steel bar
bar_item_id: 2353
@ -165,6 +204,10 @@ recipes:
item_id: 453
qty: 2
source: ge
rates:
capacity: 9
casts_per_hour: {min: 355.3, mid: 249.1, max: 191.2}
magic_xp_per_hour: {min: 18833, mid: 13204, max: 10131}
gold-bar:
bar: Gold bar
bar_item_id: 2357
@ -176,6 +219,10 @@ recipes:
item_id: 444
qty: 1
source: ge
rates:
capacity: 27
casts_per_hour: {min: 633.1, mid: 462.5, max: 362.0}
magic_xp_per_hour: {min: 33554, mid: 24514, max: 19187}
mithril-bar:
bar: Mithril bar
bar_item_id: 2359
@ -191,6 +238,10 @@ recipes:
item_id: 453
qty: 4
source: ge
rates:
capacity: 5
casts_per_hour: {min: 232.8, mid: 160.4, max: 122.0}
magic_xp_per_hour: {min: 12338, mid: 8499, max: 6468}
adamantite-bar:
bar: Adamantite bar
bar_item_id: 2361
@ -206,6 +257,10 @@ recipes:
item_id: 453
qty: 6
source: ge
rates:
capacity: 3
casts_per_hour: {min: 153.4, mid: 104.5, max: 79.1}
magic_xp_per_hour: {min: 8131, mid: 5538, max: 4194}
runite-bar:
bar: Runite bar
bar_item_id: 2363
@ -221,42 +276,78 @@ recipes:
item_id: 453
qty: 8
source: ge
rates:
capacity: 3
casts_per_hour: {min: 153.4, mid: 104.5, max: 79.1}
magic_xp_per_hour: {min: 8131, mid: 5538, max: 4194}
# Explicit activity/rate discriminator (owner guidance 2026-08-28, bounce 4):
# this entry OPTS IN to the derived-rate gate; every rate below derives from
# the entry's own declared lifecycle, and the validator re-derives it.
rate_model:
anchor_tier: sii # xp_per_hour pins the SII tier's realized rate
anchor_recipe: steel-bar # the anchor recipe (must be a declared recipe)
cast_ticks: 3 # LA/SII/HLA are 3-tick casts -> 1.8 s (0.6 s/tick)
restock_trip_s: [60, 120] # GE-adjacent booth trip per restock
session_horizon_s: 7200 # 2 h session; startup is amortised over it
startup_s: [300, 600] # one-time GE acquisition + loadout
alch_carry_batch: 1000 # LA/HLA tier: stacked candidates + natures
alch_restock_threshold: 100 # -> 901-cast LA/HLA restock cadence
sii_capacity_slots: 27 # 28-slot inventory minus the nature stack
state_machine:
initial: idle
terminal: [complete, failed]
states:
idle:
goal: "acquire + equip the staff of fire, GE-buy nature runes and the tier supplies (alch candidates, or the selected recipe's exact inputs), build the starting inventory to its carried_batch"
next: [cast, complete, failed]
goal: "session start at the GE bank booth; choose the session Magic target (default 55 — completes at the LA/SII -> HLA handoff; above 55 the HLA tier runs to the target). Open GE (a2), place the initial offers (staff a3, tier supplies a4), 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 (a5 — never leave purchases in the box). PARTIAL FILLS: collect the filled portion, re-place the remainder at the current edge (a3/a4); 3 consecutive no-fill rounds -> failed. An offer still unfilled at session end is left resting by design — its coins are tied up (float reserve) and return when it fills or the operator aborts it (the single operator-touch point)"
next: [ready, wait_offers, failed]
ready:
goal: "equip the staff of fire (a6 — bounded retries, then FAIL CLOSED) and build the loadout to the tier's carried amounts (a7; purchases from a5 are already in the inventory — deposit overflow, withdraw only the shortfall)"
next: [cast, failed]
cast:
goal: "cast the active tier spell with the tier-correct target — LA (< 43) or HLA (>= 55) on a noted alch candidate; SII (43-55) on the selected recipe's declared target_input (primary ore, never coal; exact inputs withdrawn, margin positive); a break fires on the completed-cast counter, NOT on bank visits"
goal: "cast the active tier spell with the tier-correct target — LA (< 43, or 43-54 when SII is skipped) or HLA (>= 55, only when the session target is above 55) on a noted alch candidate (a8); SII (43-55, selection.mode) on the selected recipe's declared target_input (primary ore, never coal) with its exact allowed-fill inputs withdrawn (a9). A break fires on the completed-cast counter (a13), NEVER on bank visits, and restocking never resets it"
next: [cast, bank_supplies, complete, failed]
bank_supplies:
goal: "deposit coins and smelted bars; restock to the tier's carried_batch (LA/HLA: candidate + natures to 1,000/1,000; SII: one 24-cast input pack of the selected recipe) and resume casting"
next: [cast, failed]
goal: "restock to the tier's carried amounts: deposit coins + smelted bars and withdraw ALL tier supplies the bank holds (a10 — bank-stock fast path only when the bank actually 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 purchased. Restocking never resets the cast or break counters"
next: [cast, bank_supplies, failed]
complete:
goal: "magic level target reached (default 55 -> HLA is the end-state)"
goal: "magic level >= the configured session target (default 55 — completes at the LA/SII -> HLA handoff; a target above 55 finishes only after the executable HLA tier reaches it)"
failed:
goal: "hard failure — stop, log ERROR sentinel, alert"
transitions:
- from: idle
when: "staff equipped AND tier supplies at carried_batch (LA/HLA: 1,000 candidate + 1,000 natures; SII: 24 iron-or-primary + 48 coal + 24 natures for the selected recipe)"
to: cast
when: "staff + tier supply offers all placed (a3/a4)"
to: wait_offers
- from: idle
when: "magic level >= target (55 -> HLA end-state)"
when: "magic level >= session target at session start"
to: complete
- from: idle
when: "3 consecutive purchase/equip failures with no fallback available (recovery block)"
when: "3 consecutive offer-place failures at session start (no fallback candidate/recipe)"
to: failed
- from: wait_offers
when: "ge_offers_done AND all purchases collected (a5)"
to: ready
- from: wait_offers
when: "partial fill: filled portion collected (a5), remainder re-placed at the current edge (a3/a4) — 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 (a6) AND loadout at the tier's carried amounts (a7)"
to: cast
- from: ready
when: "staff equip failed after bounded retries — FAIL CLOSED"
to: failed
- from: cast
when: "LA/HLA tier: nature count < restock_threshold (100) OR candidate count < restock_threshold (100); SII tier: bars_in_inventory >= 24 (non-stackable bars fill the inventory) OR any selected-recipe input count < 1 — deterministic restock triggers, pinned numbers"
to: bank_supplies
- from: cast
when: "level < 43 and LA tier active (cast on the cheapest alch candidate)"
when: "level < 43, or level 43-55 with SII skipped — LA tier: cast on the cheapest alch candidate (LA is executable throughout the 43-54 window when no recipe has a positive margin)"
to: cast
- from: cast
when: "level 43-55 and the SII window is chosen — selection.mode picks the recipes-table entry with the highest smithing_level <= current Smithing and a positive complete-cost margin; cast on its target_input"
when: "level 43-55 and the SII window is chosen — selection.mode picks the recipes-table entry with the highest smithing_level <= current Smithing and a positive allowed-fill complete-cost margin; cast on its target_input"
to: cast
- from: cast
when: "level 43-55 — SII fallback: the selected recipe's materials cannot be obtained, so drop to the next lower unlocked recipe (never an unmet Smithing gate) and continue SII if its margin is positive"
@ -265,17 +356,26 @@ state_machine:
when: "level 43-55 and no unlocked recipe has a positive margin this window — skip SII and stay on LA"
to: cast
- from: cast
when: "level >= 55 and HLA tier active (cast on the cheapest alch candidate)"
when: "level >= 55 and session target > 55 — the HLA tier executes (a8) until magic reaches the target"
to: cast
- from: cast
when: "magic level >= target (55 -> HLA end-state)"
when: "LA/HLA tier: candidate count < restock_threshold (100) OR nature count < restock_threshold (100); SII tier: bars_in_inventory >= the selected recipe's capacity (floor(27 / total input qty): steel 9, bronze 13, mithril 5, ...) OR any selected-recipe input < 1 — deterministic restock triggers, pinned numbers"
to: bank_supplies
- from: cast
when: "magic level >= session target (default 55 — completes at the LA/SII -> HLA handoff; targets above 55 require the executable HLA tier)"
to: complete
- from: cast
when: "3 consecutive failed casts (client issue — do not loop)"
to: failed
- from: bank_supplies
when: "restocked to the tier's carried_batch (see bank_supplies goal)"
when: "restocked to the tier's carried amounts (bank fast path or re-bought deficit collected)"
to: cast
- 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
actions:
- id: a1
@ -290,81 +390,140 @@ 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/a11) or collects (a5/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 '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 fire runes per cast (LA/HLA: 3 fire + 1 nature = 155 gp vs 140; SII: 4 fire + 1 nature = 160 gp vs 140 — the recipe margin rule compares against the FULL rune cost), 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 (a5) 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
verb: ge_buy
target: grand-exchange
params:
item: "nature rune + the tier's supplies — LA/HLA: the current least-loss alch candidate; SII: the exact unnoted inputs of the SELECTED recipe (every input and quantity from the recipes table, e.g. steel-bar = 1 iron ore + 2 coal), only while its complete-cost margin is positive"
qty: "LA/HLA: carried_batch 1,000 (4h GE limits: natures 18,000, candidates per mapping); SII: one 24-cast input pack (24 x each input quantity + 24 natures) per bank cycle"
max_price: "live low + 5%"
on_fail: retry # re-place at live low +5% with paced offers; 3 consecutive failures -> fallback to next unlocked recipe (SII) or -> failed (recovery block)
item: "nature runes + the tier's supplies — LA/HLA: the current least-loss alch candidate; SII: the exact unnoted inputs of the SELECTED recipe (every input and quantity from the recipes table, e.g. steel-bar = 1 iron ore + 2 coal), only while the allowed-fill complete-cost margin is positive"
qty: "LA/HLA: carried_batch 1,000 (4h GE limits: natures 18,000, candidates per mapping); SII: one capacity-sized pack of the selected recipe — capacity = floor(27 / total input qty) casts (steel = 9, so 9 x each input quantity + 9 natures per pack)"
max_price: "live low + 5 %"
note: "offer rests until filled or the run ends; partial fills are collected (a5) and the remainder re-placed at the current edge; 3 consecutive no-fills -> SII recipe fallback, else failed"
on_fail: retry
anti_ban:
delay: [1200, 3000]
- id: a5
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, candidates/natures, or the recipe's exact inputs (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: a6
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 '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: a7
step: build-loadout
verb: bank_inventory
target: bank
params:
deposit: [overflow products]
withdraw: "to the tier's carried amounts (only the shortfall — collected purchases from a5 are already in the inventory): LA/HLA 1,000 candidate + 1,000 nature; SII one capacity-sized pack of the selected recipe (steel: 9 iron + 18 coal + 9 natures)"
on_fail: retry
anti_ban:
delay: [500, 1100]
- id: a8
step: cast-alch
verb: use_item_on
target: "noted alch candidate in inventory (LA and HLA tiers)"
params:
spell: "Low Level Alchemy (level < 43) | High Level Alchemy (level >= 55)"
spell: "Low Level Alchemy (level < 43, or 43-54 when SII is skipped) | High Level Alchemy (level >= 55 — only when the session target is above 55; HLA is never declared-but-unreachable)"
item: "cheapest live alch candidate in inventory — LA/HLA are alchemy tiers, so the selection rule is the alch-candidate rule"
on: "itself"
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 = 3.4-5.2s, mid 4.3s -> realized 791.3 casts/hr incl. breaks + banking (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 = 3.4-5.2s, mid 4.3s -> LA/HLA session-realized ~742 casts/hr incl. startup + restock + breaks (rate_model)
- id: a6
- id: a9
step: cast-superheat
verb: use_item_on
target: "the selected recipe's declared target_input in inventory (the primary ore — never coal; SII tier only, 43-55)"
params:
spell: "Superheat Item"
selected_recipe: "runtime selection per selection.mode: the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost (ALL inputs, live GE buy) + one nature rune < the bar's live GE price; fall back to the next lower unlocked recipe when its materials cannot be obtained"
selected_recipe: "runtime selection per selection.mode: the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost at ALLOWED fill prices + one nature rune at allowed fill < the bar's live GE price; fall back to the next lower unlocked recipe when its materials cannot be obtained"
target_input: "the selected recipe's declared target_input field (primary ore; coal can never be the spell target)"
on: "itself"
consumption: "1 x each recipe input (e.g. steel = 1 iron ore + 2 coal) + 1 nature per cast -> 1 bar, +53 Magic xp + the bar's Smithing xp; the pack holds capacity = floor(27 / total input qty) casts (steel = 9)"
on_fail: skip # no unlocked positive-margin recipe -> skip SII, stay on LA (selection.fallback)
anti_ban:
delay: [1600, 3400] # post-action sleep (additive): same 3.4-5.2s/cast as a5; SII realized 518.0 casts/hr incl. 24-cast bank cycles + breaks (pinned in economy)
delay: [1600, 3400] # post-action sleep (additive): same 3.4-5.2s/cast as a8; per-recipe realized rates in recipes.rates (steel mid 249.1 casts/hr steady, session anchor 12,400 xp/hr)
- id: a7
step: bank-loop
- id: a10
step: restock-deposit
verb: bank_inventory
target: bank
params:
deposit: [all coins, all products] # products incl. smelted bars during SII
withdraw: "LA/HLA: candidate + nature runes up to carried_batch (1,000/1,000); SII: one 24-cast input pack of the selected recipe (24 x each input quantity + 24 natures)"
withdraw: "ALL tier supplies currently in the bank, up to the carried amounts (LA/HLA 1,000/1,000; SII one capacity-sized pack of the selected recipe) — the bank-stock FAST PATH applies only when the bank actually holds enough; the remaining deficit is bought at the GE (a11) and collected (a12)"
on_fail: alert
anti_ban:
delay: [500, 1100]
- id: a8
- id: a11
step: restock-buy
verb: ge_buy
target: grand-exchange
params:
item: "the tier's supplies to top up to the carried amounts"
qty: "deficit = carried amount - inventory count after the a10 fast path (LA/HLA: 901 candidate + 901 natures after a 901-cast batch with an empty bank; SII: the selected recipe's capacity-sized pack minus whatever the bank held)"
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 — carried amounts are restored 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 [400, 700] casts — 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 [400, 700] casts — 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 alch / capacity-sized SII restock cadences)"
on_fail: skip
run_control:
@ -378,79 +537,105 @@ run_control:
# reset this counter (owner decision 2026-08-28)
for: [45, 120] # seconds of idle AFK
stop_conditions:
- "magic level >= target (default 55 -> HLA is the end-state)"
- "gp under floor (below one full carried_batch of natures + tier supplies)"
- "magic level >= session target (default 55: completes at the LA/SII -> HLA handoff; when the configured target is above 55, the HLA tier executes from 55 until the target — HLA is never declared-but-unreachable)"
- "session elapsed >= 2 h (session horizon — the rate model amortises startup over it)"
- "total assets (cash + supplies at current buy cost) < the next tier batch at max fill plus staff replacement (LA/HLA: 312,650 = 307,650 + 5,000; SII steel: 10,150 = 5,150 + 5,000) — assets, NOT raw cash: cash that became supplies is not a loss"
- "session net loss (total assets - initial capital) > 690,075 — the 6-batch worst-case LA reserve (Magic 21 -> 55 at max fills) is exhausted"
- "3 consecutive failed casts (client issue — do not loop)"
- "3 consecutive failed GE purchases with no fallback recipe/candidate available"
- "3 consecutive no-fill GE rounds (initial acquisition or restock) with no fallback recipe/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) =================
# SII tier, steel-bar path (Magic 43-54, Smithing 30). Constants: break.every
# [400, 700] casts (midpoint 550), break.for [45, 120] s; SII bank cycle = 24
# casts (bars_in_inventory >= 24 or any input < 1 -> bank_supplies); LA/HLA use
# the 901-cast batch model pinned in low-alch-f2p (carried_batch 1,000/1,000,
# restock_threshold 100).
# SII tier, steel-bar path (Magic 43-54, Smithing 30, session target 55).
# Constants: break.every [400, 700] casts (the example pins the midpoint
# 550), break.for [45, 120] s; steel capacity = floor(27 / (1 iron + 2 coal))
# = 9 casts per bank cycle — restocks every 9 casts; the counter sequences
# are MONOTONIC and INDEPENDENT (restocking never resets the break counter).
#
# Step 0 — prep, before the first inventory: account has 30 Smithing + 43-54
# Magic; GE-buy staff of fire and Wield it (no documented requirement); buy
# 1,000 nature runes and steel inputs (1 iron ore + 2 coal per cast, live GE).
# Build the starting inventory: 24 iron ore + 48 coal + 24 natures (one
# 24-cast pack). Cast counter := 0. Steel is the highest UNLOCKED recipe
# (Smithing 30); bronze (1), iron (15) and silver (20) are unlocked too but
# LOWER, so steel is selected. Smithing 20 accounts select silver-bar and
# steel stays locked (never an unmet gate).
# Step 1 — casts #1..#24: each cast consumes 1 iron ore + 2 coal + 1 nature
# and produces 1 steel bar (+53 Magic xp, +17.5 Smithing xp). After cast #24
# the inventory holds 24 bars -> FIRST RESTOCK/BANK EVENT at cast 24: deposit
# the 24 bars, withdraw the next 24 iron + 48 coal + 24 natures.
# Step 2 — resume at cast #25; the 24-cast cycle repeats: restocks at casts
# 48, 72, 96, ... (every 24 casts, deterministic).
# Step 3 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every
# [400, 700]): the 23rd cycle (casts 529-552) is mid-flight — take-break for
# 45-120 s, resume at cast #551, finish the cycle (restock at 552).
# The break fired on the CAST COUNTER while a bank cycle was still running:
# breaks and banking are independent by construction.
# Step 4 — break #2 at cast 1,100 (550 after #1); restocks continue every 24
# casts; the SII window ends at Magic 55 -> HLA tier (901-cast batch model,
# 65 xp/cast). Stop at the target level, gp floor, or 3 failed casts.
# Step 0 — prep (one-time startup ~300-600 s): account has 30 Smithing +
# 43-54 Magic and the LA/HLA-tier capital (>= ~1.31M gp). At the GE bank:
# choose the session target (55); open GE (a2); place offers — staff (a3),
# 1,000 nature runes + LA candidate OR - for the SII window - the selected
# recipe's first pack (steel: 9 iron ore + 18 coal + 9 natures, a4); wait
# for fills (ge_offers_done) and collect everything (a5); equip the staff
# (a6); loadout = 9 iron + 18 coal + 9 natures (a7). Cast counter := 0.
# Steel is the highest UNLOCKED recipe (Smithing 30); bronze (1), iron (15)
# and silver (20) are unlocked but LOWER, so steel is selected. Smithing 20
# accounts select silver-bar (capacity 27) and steel stays locked (never an
# unmet gate).
# Step 1 — casts #1..#9: each cast consumes 1 iron ore + 2 coal + 1 nature
# and produces 1 steel bar (+53 Magic xp, +17.5 Smithing xp). After cast #9
# the inventory holds 9 bars (capacity reached) -> FIRST RESTOCK at cast 9:
# deposit the 9 bars (a10); the bank holds 0 steel inputs (all sales were
# GE buys), so the fast path yields nothing -> open GE (a2), RE-BUY the
# pack deficit 9 iron + 18 coal + 9 natures (a11, live low + 5 %), collect
# (a12) -> inventory rebuilt. Restocks repeat every 9 casts (9, 18, 27, ...,
# and at 558 after the first break below) — capacity-derived, deterministic.
# Step 2 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every
# [400, 700]): the 61st cycle is mid-flight — take-break 45-120 s (a13),
# resume at cast 551, finish the cycle (restock at 558). The break fired on
# the CAST COUNTER while a bank cycle was still running: breaks and banking
# are independent by construction, and restocking did NOT shift the break.
# Step 3 — break #2 at cast 1,100 (exactly 550 after #1 — monotonic,
# unaffected by the 9-cast restock cadence); break #3 at 1,650; restocks
# continue at 1,107, 1,116, ... (every 9). The SII window ends at Magic 55:
# with the default target 55 the session completes at the LA/SII -> HLA
# handoff; with a target above 55 the HLA tier (a8, 65 xp/cast, 901-cast
# batch model) executes until the target.
# Fallback chain pinned: steel inputs unfillable at the GE (3 failed offers)
# -> drop to silver-bar (20, next lower unlocked) -> if silver is also
# unfillable or margin-negative -> skip SII this window and stay on LA.
# unfillable or margin-negative at allowed fills -> skip SII this window and
# stay on LA throughout 43-54 (a8 handles the SII-skip window explicitly).
# =====================================================================================================
risk:
ban_level: low
notes:
- "3-tick cast cadence is the main detection vector; keep anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; realized ~791 casts/hr alch tiers and ~518 casts/hr SII incl. breaks + banking)."
- "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."
- "3-tick cast cadence is the main detection vector; keep anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; session-realized ~742 casts/hr on the alch tiers and ~234 casts/hr on the steel SII anchor, incl. startup + restock + breaks over a 2 h session — loop-only 837 is a ceiling reference, never the output)."
- "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, and never reset the break counter on restock (break interval 550 and restock cadences 901/9 are coprime by construction)."
- "The valuable-item warning fires on item GE value (not alch value) — the script must handle the warning widget when alching high-value items (HLA page)."
- "SII: withdraw the exact recipe inputs (unnoted, per the recipes table) and cast on the declared target_input (primary ore) — never on coal, never on a noted stack; bank the produced bars when bars_in_inventory >= 24."
- "SII ore-buy bursts are pace-sensitive like any GE buy; respect the 4h limit window; re-check the complete-cost margin per window because ore/bar prices move independently."
- "SII: withdraw the EXACT recipe inputs (unnoted, per the recipes table) sized to the derived capacity (floor(27 / input qty)) and cast on the declared target_input (primary ore) — never on coal, never on a noted stack; bank the produced bars when bars_in_inventory >= capacity."
- "SII ore-buy bursts are pace-sensitive like any GE buy; respect the 4h limit window; re-check the allowed-fill complete-cost margin per window because ore/bar prices move independently."
# --- recovery block (owner guidance 2026-08-28: every activity must cover its complete runnable lifecycle) ---
# - Missing materials / depleted supplies: LA/HLA restock at candidate OR nature
# < 100 (carried_batch 1,000/1,000); SII restocks at bars >= 24 or any selected-
# recipe input < 1 — both are deterministic threshold transitions to bank_supplies.
# - Failed GE purchases (offer unfilled): re-place at live low + 5% with paced
# offers; after 3 consecutive failures in the SII window, fall back to the next
# lower unlocked recipe (selection.fallback); with no fallback recipe left,
# skip SII and stay on LA — never sit in an unfillable offer loop.
# - Staff of fire cannot be equipped (a3): fall back to fire runes (LA/HLA: +15
# gp/cast; SII: +20 gp/cast rune cost), log WARN, continue; never loop on equip
# attempts.
# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout
# (a4/a7); 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert).
# - Explicit completion/stop: stop_conditions above (Magic 55 HLA end-state, gp
# floor, repeated failures).
# < 100 (carried_batch 1,000/1,000); SII restocks at bars >= the selected
# recipe's capacity (floor(27 / input qty)) or any selected-recipe input < 1 —
# deterministic threshold transitions to bank_supplies; the restock phase
# (a10 -> a2 -> a11 -> a12) RE-BUYS the deficit at the GE — never withdraw a
# batch that was never purchased.
# - GE offers: partial fills are collected (a5/a12) and the remainder re-placed
# at the current edge (a3/a4/a11); after 3 consecutive no-fill rounds (initial
# or restock) the SII recipe falls back to the next lower unlocked recipe;
# with no fallback recipe left, SII is skipped and LA takes over; with no
# fallback candidate, the run stops (failed). 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 (a6): 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 (a7); 3 consecutive failed casts -> failed (stop, log ERROR
# sentinel, alert).
# - Capital: both capital stops are assets-based (total assets = cash +
# supplies at current buy cost) — cash converted into supplies is not a loss
# and never fires a stop by itself (see stop_conditions).
# - Explicit completion/stop: stop_conditions above (session target 55 or the
# >55 HLA tier, 2 h session horizon, assets floor 312,650 / 10,150, loss
# reserve 690,075, repeated failures, fail-closed staff equip).
sources:
- "OSRS wiki — Low Level Alchemy (21 Magic, 31 xp/cast, members=No, 3-tick, 40% of item value; 'recommended to use High Level Alchemy instead' at 55 Magic)"
- "OSRS wiki — Superheat Item (43 Magic, 53 xp/cast, members=No; smelts ore into bars without a furnace; cannot be cast on noted ore; grants the bar's Smithing XP; requires the bar's Smithing level and materials)"
- "OSRS wiki — High Level Alchemy (55 Magic, 65 xp/cast, members=No, 60% of item value; GE buying for break-even/profit; noted-item casting; valuable-item warning)"
- "OSRS wiki — Smithing (bar smelting table: smithing levels, per-bar XP and ore/coal quantities for bronze/iron/silver/steel/gold/mithril/adamantite/runite)"
- "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"
- "prices.runescape.wiki/api/v1/osrs/mapping (id 561 nature rune: members=false, limit 18000, value 180, lowalch 72; limit cross-checked against Module:Exchange/Nature_rune — agree)"
- "prices.runescape.wiki/api/v1/osrs/latest (nature rune buy 145, snapshot 2026-08-27; steel-bar fixture prices: iron ore 100, coal 150, steel bar 600)"
- "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 + lifecycle model; owner guidance 2026-08-28 (dynamic recipe selection, realized rates, worked example)"
- "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) — the cadence + break + GE + lifecycle model; owner guidance 2026-08-28 (capacity-derived SII, rate_model, configurable target, fail-closed staff)"
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
related:

View File

@ -101,11 +101,66 @@
"source": { "enum": ["ge", "p2p", "drops", "craft", "bank", "npc-shop"] }
}
}
},
"rates": {
"type": "object",
"required": ["capacity", "casts_per_hour", "magic_xp_per_hour"],
"additionalProperties": false,
"properties": {
"capacity": { "type": "integer", "minimum": 1 },
"casts_per_hour": {
"type": "object",
"required": ["min", "mid", "max"],
"additionalProperties": false,
"properties": {
"min": { "type": "number", "minimum": 0 },
"mid": { "type": "number", "minimum": 0 },
"max": { "type": "number", "minimum": 0 }
}
},
"magic_xp_per_hour": {
"type": "object",
"required": ["min", "mid", "max"],
"additionalProperties": false,
"properties": {
"min": { "type": "number", "minimum": 0 },
"mid": { "type": "number", "minimum": 0 },
"max": { "type": "number", "minimum": 0 }
}
}
}
}
}
}
},
"rate_model": {
"type": "object",
"required": ["anchor_tier", "cast_ticks", "restock_trip_s", "session_horizon_s"],
"additionalProperties": false,
"properties": {
"anchor_tier": { "enum": ["la", "sii", "hla"] },
"anchor_recipe": { "type": "string" },
"cast_ticks": { "type": "integer", "minimum": 1 },
"restock_trip_s": {
"type": "array",
"items": { "type": "number", "minimum": 0 },
"minItems": 2,
"maxItems": 2
},
"session_horizon_s": { "type": "integer", "minimum": 1 },
"startup_s": {
"type": "array",
"items": { "type": "number", "minimum": 0 },
"minItems": 2,
"maxItems": 2
},
"alch_carry_batch": { "type": "integer", "minimum": 1 },
"alch_restock_threshold": { "type": "integer", "minimum": 0 },
"sii_capacity_slots": { "type": "integer", "minimum": 1 }
}
},
"best_practice": {
"type": "object",
"required": ["equipment"],

View File

@ -23,10 +23,17 @@ Runs:
`target_input` that is one of its declared inputs and never coal
(the spell targets the primary ore), and smithing_level gates must
be unique so "highest unlocked" selection is deterministic.
- Realized-rate consistency: xp_per_hour must derive from the pinned
realized cadence (mandatory additive delays PLUS breaks PLUS
banking), not from the loop-only 837 casts/hr or the superseded
8-14-cast-break regime (~305 casts/hr).
- Realized-rate consistency (owner guidance 2026-08-28, bounce 4):
entries that DECLARE a `rate_model` must have xp_per_hour derived
from their OWN declared lifecycle — cast ticks + the casting
action's mandatory anti_ban.delay (additive), run_control breaks,
per-recipe inventory capacity (floor(slots / total input qty)),
restock trips, and startup amortised over the session horizon.
The gate is behind an explicit discriminator: entries without
`rate_model` (e.g. mining) are never treated as alchemy. Every
declared SII recipe must pin `rates` (capacity + min/mid/max
casts/hr + Magic xp/hr) matching the derived model, and capacity
must fit a 28-slot inventory (1 nature stack -> 27 input slots).
3. Global id uniqueness across the whole codex tree.
Malformed/scalar YAML documents (an empty doc, a bare string, a list)
@ -42,6 +49,7 @@ Exit codes: 0 = clean (warnings ok), 1 = validation errors.
Usage: python3 tools/validate.py [--stale-days 30] [--selftest]
--selftest: run the regression fixtures only
"""
import copy
import datetime
import json
import re
@ -143,21 +151,240 @@ def recipe_margin(recipe, bar_price, input_prices, nature_price):
return bar_price - (cost + nature_price)
# --- realized-cadence constants (pinned model; owner guidance 2026-08-28) ---
# docs/action-grammar.md:135: anti_ban.delay is an ADDITIVE post-action sleep,
# so a cast = 1.8s (3 ticks x 0.6s) + [1.6, 3.4]s = 3.4-5.2s (mid 4.3s).
# LA/HLA realized casts/hr: 901-cast restock batch (carried_batch 1,000 -
# restock_threshold 100) with 225s overhead per batch (90s restock trip +
# 135s breaks at every [400,700] casts, mid 550) -> 791.3 casts/hr midpoint.
# SII realized casts/hr: 24-cast bank cycle (non-stackable bars fill the
# inventory) -> 518.0 casts/hr midpoint (550-cast break window: 2,365s
# casting + 22.9 trips x 60s + 82.5s break).
REALIZED_CastsPerHr_ALCH = 791.3
REALIZED_CastsPerHr_SII = 518.0
# --- rate-model constants (owner guidance 2026-08-28, bounce 4) ---
# The cadence is DERIVED from each entry's declared rate_model + actions +
# run_control + recipes — nothing is hard-coded per activity class. These
# are the only universal facts:
# * one game tick = 0.6 s (docs/action-grammar.md);
# * anti_ban.delay is an ADDITIVE post-action sleep
# (docs/action-grammar.md:135), so cycle = cast_ticks*0.6 + delay;
# * LA/SII/HLA Magic XP per cast (wiki infoboxes): 31 / 53 / 65.
XP_LA, XP_SII, XP_HLA = 31, 53, 65
RATE_TOLERANCE = 0.02 # pinned values may round to 3 significant figures
def _mean(pair):
return (pair[0] + pair[1]) / 2.0 if pair else 0.0
def _casting_delay(data):
"""The mandatory post-action delay of the entry's casting action(s),
converted from ms (anti_ban.delay units) to seconds.
Derived from the entry's OWN actions: the first action whose step
starts with 'cast' and carries an anti_ban.delay pair. Returns
(delay_min_s, delay_max_s) or None.
"""
for a in data.get("actions", []):
if not isinstance(a, dict):
continue
if not str(a.get("step", "")).startswith("cast"):
continue
delay = (a.get("anti_ban") or {}).get("delay")
if isinstance(delay, list) and len(delay) == 2:
return float(delay[0]) / 1000.0, float(delay[1]) / 1000.0
return None
def _break_amort(data):
"""Break time per cast from the entry's OWN run_control.break.
break.every counts completed casts (one-cast iteration model), so the
amortised break share = mean(break.for) / mean(break.every). Fast/slow
ends use for_min/every_max and for_max/every_min respectively.
"""
brk = (data.get("run_control") or {}).get("break") or {}
every, for_ = brk.get("every"), brk.get("for")
if not (isinstance(every, list) and len(every) == 2
and isinstance(for_, list) and len(for_) == 2):
return None
e_min, e_max = float(every[0]), float(every[1])
f_min, f_max = float(for_[0]), float(for_[1])
return {
"fast": f_min / e_max, "mid": _mean(for_) / _mean(every),
"slow": f_max / e_min}
def _session_factor(rm, pace):
"""Startup amortisation factor = 1 - startup_s / session_horizon_s.
pace selects the matching end of the startup range: fast cast pace
pairs with the fast (short) startup, slow with the slow one, mid with
the midpoint. Returns None when the horizon is missing or startup
exceeds the horizon (impossible session)."""
startup = rm.get("startup_s") or [300, 600]
horizon = rm.get("session_horizon_s")
if not isinstance(horizon, (int, float)) or horizon <= 0:
return None
if not (isinstance(startup, list) and len(startup) == 2):
return None
s_min, s_max = float(startup[0]), float(startup[1])
pick = {"fast": s_min, "mid": _mean(startup), "slow": s_max}[pace]
if pick >= horizon:
return None
return 1.0 - pick / horizon
def recipe_capacity(recipe, slots):
"""Inventory capacity of a recipe: floor(slots / total input qty).
SII bars are non-stackable and one slot holds the nature rune stack,
so a 28-slot inventory leaves `slots` (=27) for recipe inputs. A
recipe whose inputs exceed the slots cannot exist (capacity 0)."""
qty = recipe_total_input_qty(recipe)
if qty <= 0:
return 0
return int(slots // qty)
def _alch_pace_casts_hr(cycle, trip, break_share, batch_casts):
"""Steady-state realized casts/hr for an alch tier (LA/HLA), derived
from the batch model (carried_batch - restock_threshold casts per
restock trip) plus breaks."""
batch_s = batch_casts * cycle + trip + break_share * batch_casts
if batch_s <= 0:
return 0.0
return batch_casts * 3600.0 / batch_s
def rate_model_errors(rel, data):
"""xp_per_hour and per-recipe rates must derive from the entry's OWN
declared lifecycle (owner guidance 2026-08-28, bounce 4).
The gate is OPT-IN behind an explicit `rate_model` discriminator:
entries without one (e.g. a mining entry) are never treated as
alchemy. For entries that declare it, the model is derived from the
entry's own fields — cast_ticks + the casting action's mandatory
anti_ban.delay, run_control.break, restock trips, the recipes table
(capacity = floor(slots / total input qty)), and startup amortised
over session_horizon_s. Regression coverage (--selftest):
non-alchemy pass, loop-only/old-regime failures, cadence-mutation
failure (one-hour breaks), per-recipe capacity and rate pins."""
def err(msg):
return f"{rel}: {msg}"
rm = data.get("rate_model")
if not isinstance(rm, dict):
return [] # explicit discriminator — unrelated entries pass
errors = []
tick_s = 0.6
cast_ticks = rm.get("cast_ticks")
if not isinstance(cast_ticks, (int, float)) or cast_ticks < 1:
errors.append(err("rate_model.cast_ticks must be >= 1"))
return errors
cast_s = cast_ticks * tick_s
delay = _casting_delay(data)
if delay is None:
errors.append(err("rate_model requires a casting action (step 'cast-*') "
"with anti_ban.delay [min, max] — the rate derives from "
"the entry's own mandatory pacing"))
return errors
cycle = {"fast": cast_s + delay[0], "mid": cast_s + _mean(delay),
"slow": cast_s + delay[1]}
break_amort = _break_amort(data)
if break_amort is None:
errors.append(err("rate_model requires run_control.break with every + for "
"ranges (cast-count iteration model)"))
return errors
trip = rm.get("restock_trip_s")
if not (isinstance(trip, list) and len(trip) == 2):
errors.append(err("rate_model.restock_trip_s must be [min, max] seconds"))
return errors
trip = {"fast": float(trip[0]), "mid": _mean(trip), "slow": float(trip[1])}
slots = rm.get("sii_capacity_slots", 27)
# --- per-recipe SII rates (capacity + min/mid/max), fully derived ---
recipes = data.get("recipes")
if isinstance(recipes, dict):
for rid, recipe in recipes.items():
if not isinstance(recipe, dict):
continue
cap = recipe_capacity(recipe, slots)
rates = recipe.get("rates")
if not isinstance(rates, dict):
errors.append(err(f"recipe '{rid}' must pin a rates block "
f"(capacity, casts_per_hour, magic_xp_per_hour)"))
continue
if cap < 1:
errors.append(err(f"recipe '{rid}' cannot fit the inventory: "
f"total input qty {recipe_total_input_qty(recipe)} "
f"> {slots} input slots (capacity 0)"))
continue
pinned_cap = rates.get("capacity")
if pinned_cap != cap:
errors.append(err(f"recipe '{rid}' rates.capacity {pinned_cap} != "
f"derived floor({slots} / {recipe_total_input_qty(recipe)}) = {cap}"))
casts = {}
for pace in ("min", "mid", "max"):
p = {"min": "fast", "mid": "mid", "max": "slow"}[pace]
per_cast = cycle[p] + trip[p] / cap + break_amort[p]
casts[pace] = 3600.0 / per_cast if per_cast > 0 else 0.0
pin_cph = (rates.get("casts_per_hour") or {})
pin_xp = (rates.get("magic_xp_per_hour") or {})
for pace, computed in casts.items():
expect_xp = computed * XP_SII
pc, px = pin_cph.get(pace), pin_xp.get(pace)
for label, pinned, expected in (("casts_per_hour", pc, computed),
("magic_xp_per_hour", px, expect_xp)):
if not isinstance(pinned, (int, float)):
errors.append(err(f"recipe '{rid}' rates.{label}.{pace} missing"))
elif abs(pinned - expected) / expected > RATE_TOLERANCE:
errors.append(err(
f"recipe '{rid}' rates.{label}.{pace} {pinned} is not the "
f"derived rate ({expected:.1f} from cycle {cycle[p]:.2f}s + "
f"trip {trip[p]:.1f}s/{cap} capacity + break {break_amort[p]:.3f}s; "
f"tolerance {RATE_TOLERANCE:.0%})"))
# --- xp_per_hour anchor: the declared anchor tier, same lifecycle ---
eco = data.get("economy")
xph = eco.get("xp_per_hour") if isinstance(eco, dict) else None
if not isinstance(xph, (int, float)):
errors.append(err("rate_model requires economy.xp_per_hour to pin the "
"anchor tier's realized rate"))
return errors
anchor = rm.get("anchor_tier")
per_cast = {"la": XP_LA, "sii": XP_SII, "hla": XP_HLA}.get(anchor)
if per_cast is None:
errors.append(err("rate_model.anchor_tier must be la, sii or hla"))
return errors
if anchor == "sii":
rid = rm.get("anchor_recipe")
recipe = (recipes or {}).get(rid) if isinstance(recipes, dict) else None
if not isinstance(recipe, dict):
errors.append(err(f"rate_model.anchor_recipe '{rid}' is not a declared "
f"recipe — the anchor must be one of the recipes"))
return errors
cap = recipe_capacity(recipe, slots)
if cap < 1:
return errors # already reported as capacity 0
casts_mid = 3600.0 / (cycle["mid"] + trip["mid"] / cap + break_amort["mid"])
else:
batch = rm.get("alch_carry_batch")
threshold = rm.get("alch_restock_threshold")
if not (isinstance(batch, (int, float)) and batch > 1
and isinstance(threshold, (int, float)) and 0 <= threshold < batch):
errors.append(err("rate_model.alch_carry_batch and alch_restock_threshold "
"are required for la/hla anchors (threshold < batch)"))
return errors
batch_casts = batch - threshold
casts_mid = _alch_pace_casts_hr(cycle["mid"], trip["mid"],
break_amort["mid"], batch_casts)
factor = _session_factor(rm, "mid")
if factor is None:
errors.append(err("rate_model startup_s must be < session_horizon_s "
"(startup cannot exceed the session)"))
return errors
expected = per_cast * casts_mid * factor
if abs(xph - expected) / expected > RATE_TOLERANCE:
errors.append(err(
f"xp_per_hour {xph} is not the anchor tier's derived realized rate "
f"(expected ~{expected:.0f} = {per_cast} xp/cast x {casts_mid:.1f} "
f"steady-state casts/hr x startup factor {factor:.3f} over "
f"{rm.get('session_horizon_s')}s; tolerance {RATE_TOLERANCE:.0%})"))
return errors
def highest_unlocked_recipe(recipes, smithing_level):
"""Deterministic Superheat selection: the declared recipe with the
highest smithing_level <= the account's Smithing level, else None.
@ -174,6 +401,37 @@ def highest_unlocked_recipe(recipes, smithing_level):
return best
def select_recipe(recipes, smithing_level, bar_prices, input_prices,
nature_price, fill_factor=1.0):
"""Deterministic full selection (owner guidance 2026-08-28, bounce 4):
among the recipes whose Smithing gate is unlocked, pick the HIGHEST-gate
one whose complete-recipe margin is positive at the given prices.
fill_factor scales every input and the nature rune price to the ALLOWED
fill cost (live low + 5% -> 1.05), so eligibility uses the FULL
actual/allowed recipe cost. Returns None when no unlocked recipe has a
positive margin — the caller then skips SII and stays on LA (the
fallback chain: this helper already drops to the next lower unlocked
recipe whenever the highest one is margin-negative)."""
unlocked = []
for rid, recipe in (recipes or {}).items():
if not isinstance(recipe, dict):
continue
lvl = recipe.get("smithing_level")
if isinstance(lvl, int) and lvl <= smithing_level:
cost = sum(
int(inp.get("qty", 0)) * int(input_prices.get(inp.get("item"), 0))
for inp in recipe.get("inputs", []))
margin = (bar_prices.get(recipe.get("bar"), 0)
- (cost * fill_factor + nature_price * fill_factor))
unlocked.append((lvl, rid, margin))
unlocked.sort(key=lambda t: t[0], reverse=True)
for _lvl, rid, margin in unlocked:
if margin > 0:
return rid
return None
def recipe_model_errors(rel, data):
"""SII recipe-model + selection gates (owner guidance 2026-08-28).
@ -261,37 +519,6 @@ def recipe_model_errors(rel, data):
return errors
def rate_errors(rel, data):
"""xp_per_hour must derive from the REALIZED cadence: mandatory
additive delays PLUS the pinned mandatory breaks PLUS banking.
Regression guard: the loop-only figure (837 casts/hr) and the
superseded 8-14-cast-break regime (~305 casts/hr) both sit outside
the tolerance band around the realized rate.
"""
errors = []
eco = data.get("economy")
if not isinstance(eco, dict):
return errors
xph = eco.get("xp_per_hour")
if not isinstance(xph, (int, float)):
return errors
has_sii = any(
"superheat" in (str(a.get("params") or {}).lower()
+ str(a.get("target") or "").lower()
+ str(a.get("step") or "").lower())
for a in data.get("actions", []))
per_cast = XP_HLA if has_sii else XP_LA
expected = per_cast * REALIZED_CastsPerHr_ALCH
if abs(xph - expected) / expected > RATE_TOLERANCE:
errors.append(
f"{rel}: xp_per_hour {xph} is not the realized cadence rate "
f"(expected ~{expected:.0f} = {per_cast} xp/cast x "
f"{REALIZED_CastsPerHr_ALCH} casts/hr including mandatory breaks "
f"and banking; tolerance {RATE_TOLERANCE:.0%})")
return errors
def run_selftest(verbose=False):
"""Regression fixtures for the cross-field gates; error strings."""
errors = []
@ -387,6 +614,27 @@ def run_selftest(verbose=False):
highest_unlocked_recipe(f2p_bars, 85) == "runite-bar")
check("smithing 0 -> None (no recipe selected below the lowest gate)",
highest_unlocked_recipe(f2p_bars, 0) is None)
# Full selection: Smithing gate + complete-recipe margin at actual and
# ALLOWED fill prices, falling back to the next lower unlocked recipe
bar_prices = {"Bronze bar": 200, "Iron bar": 380, "Silver bar": 450,
"Steel bar": 600, "Runite bar": 12500}
input_prices = {"Copper ore": 50, "Tin ore": 30, "Iron ore": 100,
"Silver ore": 150, "Coal": 150, "Runite ore": 11000}
nature = 145
check("full selection at live prices: smithing 30 + steel margin +55 -> steel-bar",
select_recipe(f2p_bars, 30, bar_prices, input_prices, nature) == "steel-bar")
check("allowed-fill eligibility (x1.05): steel margin +27.75 -> steel-bar still selected",
select_recipe(f2p_bars, 30, bar_prices, input_prices, nature,
fill_factor=1.05) == "steel-bar")
check("material availability: steel margin-negative at allowed fills -> next lower unlocked (silver-bar)",
select_recipe(f2p_bars, 30, {**bar_prices, "Steel bar": 570}, input_prices,
nature, fill_factor=1.05) == "silver-bar")
check("no unlocked recipe with a positive margin -> None (skip SII, stay on LA)",
select_recipe(f2p_bars, 30, {"Bronze bar": 10, "Iron bar": 10,
"Silver bar": 10, "Steel bar": 10},
input_prices, nature) is None)
check("an unmet Smithing gate is never selected (smithing 20 -> silver-bar, steel stays locked)",
select_recipe(f2p_bars, 20, bar_prices, input_prices, nature) == "silver-bar")
superheat_action = {
"id": "a6", "step": "cast-superheat", "verb": "use_item_on",
@ -445,22 +693,87 @@ def run_selftest(verbose=False):
check("Superheat entry without an SII fallback transition is rejected",
any("SII fallback" in e for e in no_fb))
# --- realized-rate fixtures: breaks + banking included ---
check("LA-only xp_per_hour 24,500 matches the realized cadence (31 x 791.3)",
rate_errors("ok.yaml", {"economy": {"xp_per_hour": 24500}}) == [])
check("loop-only LA xp_per_hour 25,953 fails the realized-cadence gate",
any("realized cadence" in e
for e in rate_errors("bad.yaml", {"economy": {"xp_per_hour": 25953}})))
check("8-14-cast-break regime LA rate (31 x ~305 = 9,455) fails the realized-cadence gate",
any("realized cadence" in e
for e in rate_errors("bad.yaml", {"economy": {"xp_per_hour": 9455}})))
check("SII entry xp_per_hour 51,400 matches the HLA-tier realized cadence (65 x 791.3)",
rate_errors("ok2.yaml", {"actions": [superheat_action],
"economy": {"xp_per_hour": 51400}}) == [])
check("SII entry with the old loop-only HLA rate 54,419 fails the realized-cadence gate",
any("realized cadence" in e
for e in rate_errors("bad2.yaml", {"actions": [superheat_action],
"economy": {"xp_per_hour": 54419}})))
# --- rate_model fixtures (owner guidance 2026-08-28, bounce 4) ---
# The gate is OPT-IN: entries without rate_model are never treated as
# alchemy, and the rate is DERIVED from the entry's own declared
# lifecycle (cast action delay, run_control.break, recipes, startup).
cast_action = {"id": "a3", "step": "cast-alch", "verb": "use_item_on",
"params": {"spell": "Low Level Alchemy"},
"on_fail": "retry",
"anti_ban": {"delay": [1600, 3400]}}
base = {
"rate_model": {"anchor_tier": "la", "cast_ticks": 3,
"restock_trip_s": [60, 120], "session_horizon_s": 7200,
"startup_s": [300, 600],
"alch_carry_batch": 1000, "alch_restock_threshold": 100},
"actions": [cast_action],
"run_control": {"break": {"every": [400, 700], "for": [45, 120]}},
"economy": {"xp_per_hour": 23000}}
check("non-alchemy entry without rate_model passes (mining xp 52,500 untouched)",
rate_model_errors("mining.yaml", {"economy": {"xp_per_hour": 52500}}) == [])
check("LA entry declaring rate_model with the derived 23,000 xp/hr passes",
rate_model_errors("ok.yaml", dict(base)) == [])
check("loop-only LA 25,953 fails the derived realized-rate gate",
any("derived realized rate" in e
for e in rate_model_errors("bad.yaml", dict(base, **{"economy": {"xp_per_hour": 25953}}))))
check("pre-startup realized 24,500 fails too (startup amortisation is part of the model)",
any("derived realized rate" in e
for e in rate_model_errors("bad.yaml", dict(base, **{"economy": {"xp_per_hour": 24500}}))))
check("8-14-cast-break regime 9,455 fails the derived rate gate",
any("derived realized rate" in e
for e in rate_model_errors("bad.yaml", dict(base, **{"economy": {"xp_per_hour": 9455}}))))
mutated = copy.deepcopy(base)
mutated["run_control"]["break"]["for"] = [3600, 3600] # one-hour break per cast
check("cadence mutation caught: one-hour break per cast fails the derived rate gate",
any("derived realized rate" in e
for e in rate_model_errors("bad.yaml", mutated)))
# SII: per-recipe capacity (floor(27 / total input qty)) + derived rates
def recipe(rid, bar, bar_id, lvl, ti, inputs, cap, cph, xp):
return {"bar": bar, "bar_item_id": bar_id, "smithing_level": lvl,
"target_input": ti,
"inputs": [{"item": it, "item_id": iid, "qty": q, "source": "ge"}
for it, iid, q in inputs],
"rates": {"capacity": cap,
"casts_per_hour": {"min": cph[0], "mid": cph[1], "max": cph[2]},
"magic_xp_per_hour": {"min": xp[0], "mid": xp[1], "max": xp[2]}}}
steel_recipe = recipe("steel-bar", "Steel bar", 2353, 30, "Iron ore",
[("Iron ore", 440, 1), ("Coal", 453, 2)],
9, (355.3, 249.1, 191.2), (18833, 13204, 10131))
bronze_recipe = recipe("bronze-bar", "Bronze bar", 2349, 1, "Copper ore",
[("Copper ore", 436, 1), ("Tin ore", 438, 1)],
13, (445.6, 316.5, 244.4), (23617, 16776, 12952))
runite_recipe = recipe("runite-bar", "Runite bar", 2363, 85, "Runite ore",
[("Runite ore", 451, 1), ("Coal", 453, 8)],
3, (153.4, 104.5, 79.1), (8131, 5538, 4194))
sii_base = dict(base)
sii_base["rate_model"] = {"anchor_tier": "sii", "anchor_recipe": "steel-bar",
"cast_ticks": 3, "restock_trip_s": [60, 120],
"session_horizon_s": 7200, "startup_s": [300, 600]}
sii_base["economy"] = {"xp_per_hour": 12400}
sii_base["recipes"] = {"steel-bar": steel_recipe, "bronze-bar": bronze_recipe,
"runite-bar": runite_recipe}
check("SII entry passes: capacities floor(27/qty) = steel 9, bronze 13, runite 3, all rate pins derived",
rate_model_errors("ok.yaml", dict(sii_base)) == [])
check("steel anchor xp 12,400 (249.1 x 53 x startup 0.9375) passes the anchor gate",
rate_model_errors("ok2.yaml", dict(sii_base)) == [])
check("old 24-pack SII rate 27,500 fails the derived per-recipe rate",
any("derived realized rate" in e
for e in rate_model_errors("bad.yaml", dict(sii_base, **{"economy": {"xp_per_hour": 27500}}))))
bad_cap = copy.deepcopy(sii_base)
bad_cap["recipes"]["steel-bar"]["rates"]["capacity"] = 8
check("capacity pin mismatch (8 vs floor(27/3)=9) is rejected",
any("rates.capacity 8" in e for e in rate_model_errors("bad.yaml", bad_cap)))
bad_xp = copy.deepcopy(sii_base)
bad_xp["recipes"]["steel-bar"]["rates"]["magic_xp_per_hour"]["mid"] = 20000
check("recipe magic_xp pin far from the derived 13,204 is rejected",
any("magic_xp_per_hour.mid" in e for e in rate_model_errors("bad.yaml", bad_xp)))
oversized = copy.deepcopy(sii_base)
oversized["recipes"]["oversized-bar"] = recipe("oversized-bar", "Oversized bar", 9999, 90,
"Iron ore", [("Iron ore", 440, 28)],
1, (1, 1, 1), (53, 53, 53))
check("recipe needing more input slots than the inventory (qty 28 > 27) is rejected",
any("cannot fit the inventory" in e
for e in rate_model_errors("bad.yaml", oversized)))
return errors
@ -572,7 +885,7 @@ def main():
# SII recipe-model + selection gates + realized-rate consistency
errors += recipe_model_errors(rel, data)
errors += rate_errors(rel, data)
errors += rate_model_errors(rel, data)
# 3. staleness / research TODOs
if data.get("status") == "verified":