RuneScape’s Grand Exchange is a constrained trading environment that mirrors real exchange plumbing without regulatory friction or capital barriers. The API enforces rate limits, the order book is opaque, and historical tick data does not exist. A developer built a market-making bot that navigates these constraints using machine learning to rank offers by forecasted profitability. The architecture exposes patterns that apply to any agent operating under partial observability and strict API quotas.
Why This Matters
The Grand Exchange API is documented, accessible, and strict. It enforces a four-hour buy limit per item (13,000 coal ore, for example), applies a 1% tax on executed sell offers above 100 gold (capped at 5 million per offer), and updates prices every five minutes via the OSRS Wiki real-time stream. You cannot short-sell. You cannot see the order book depth. You cannot replay historical trades at sub-five-minute granularity.
These constraints force the agent to solve the same problems real market-making systems face: inventory risk management, price discovery under latency, and order prioritization when throughput is capped. The difference is you can prototype the entire stack in a weekend and test it against a live market without a brokerage account.
Architecture
The bot splits into three services:
- JavaScript API client: polls the OSRS Wiki real-time price stream every five minutes, writes spread, volume, and buy limit data to a time-series store.
- Java client: controls character actions in-game, places buy and sell offers, monitors execution status, handles session persistence.
- Python ranking API: trains ML models on historical spread and volume data, returns a ranked list of offers by forecasted profitability.
The Java client is the orchestration layer. It queries the Python API for the top N offers, submits them to the Grand Exchange via the game client, and polls execution status. When an offer fills, it updates inventory state and re-queries the ranking API to decide whether to flip the item or hold.
The Python API does not run inference in real time. It pre-computes rankings every five minutes when new price data arrives, caches the results, and serves them via HTTP. This decouples model training (which can take seconds per item) from the latency-sensitive order placement loop.
State Management
The agent persists three categories of state:
- Inventory snapshot: current holdings by item, quantity, and average acquisition cost. Updated on every fill.
- Offer history: submitted offers, execution timestamps, fill prices, and tax paid. Used to calculate realized P&L and detect stale offers.
- Rate limit counters: per-item buy limits reset every four hours. The agent tracks the last reset timestamp and cumulative quantity purchased to avoid API rejections.
The Java client writes this state to a local SQLite database. On restart, it rehydrates inventory and rate limit counters, cancels any stale offers still open in the Grand Exchange, and resumes polling the ranking API.
The OSRS Wiki API does not provide a session token or persistent connection. Every poll is stateless. The agent must infer execution from the difference between submitted offers and the current inventory snapshot. If an offer disappears from the Grand Exchange UI but inventory has not changed, the offer was cancelled or expired. If inventory increased, the offer filled.
Price Discovery Without Tick Data
The OSRS Wiki API publishes a five-minute moving average of trade prices, not individual ticks. The agent cannot see bid-ask spreads in real time. It cannot detect flash crashes or liquidity gaps until the next five-minute window closes.
The ranking API compensates by modeling spread volatility over the past hour. It trains a simple regression model per item that predicts the next five-minute spread given the last twelve observations (one hour of history). Items with stable spreads and high volume rank higher. Items with erratic spreads or low volume rank lower, even if the current spread looks attractive.
This approach avoids the trap of chasing wide spreads that collapse before the next order fills. The agent prioritizes consistency over peak profitability.
Inventory Risk and Position Limits
The agent cannot short-sell. It must buy before it can sell. This introduces inventory risk: if the price drops after purchase, the agent holds a losing position until the market recovers or it exits at a loss.
The ranking API enforces a position limit per item as a percentage of total capital. For high-volume items like coal ore, the limit is 20%. For low-volume items like rare armor, the limit is 5%. This prevents the agent from concentrating risk in illiquid positions that take hours to unwind.
The agent also tracks holding time. If an item sits in inventory for more than 30 minutes without a profitable exit, the ranking API deprioritizes that item class for future purchases. This feedback loop prevents the agent from repeatedly buying items with structural bid-ask compression.
Rate Limit Queuing Strategy
The Grand Exchange enforces a four-hour buy limit per item. The agent must decide which items to prioritize when the ranking API returns more offers than the rate limit allows.
The Java client implements a simple priority queue. It sorts offers by forecasted profitability, then filters out any item where the cumulative quantity purchased in the last four hours exceeds the buy limit. It submits the top N offers that pass the filter, where N is the number of open offer slots (the Grand Exchange caps active offers at eight per account).
When an offer fills, the client immediately queries the ranking API for the next best offer and submits it. This keeps the offer slots saturated and maximizes throughput within the rate limit.
The client does not retry rejected offers. If the Grand Exchange rejects an offer due to rate limit exhaustion, the client logs the rejection, updates the local rate limit counter to reflect the actual server-side state, and moves to the next item in the priority queue. Retrying wastes API calls and delays other profitable offers. The client also handles partial fills by updating the inventory snapshot with the filled quantity and resubmitting the unfilled portion only if the ranking API still considers that item profitable in the next polling cycle. This prevents the agent from chasing stale opportunities after market conditions change.
Tax Optimization
The 1% tax on sell offers above 100 gold changes the profitability calculation. The agent must account for tax when ranking offers, not just the raw spread.
The Python API computes net profit using logic similar to this (representative pseudocode based on the source article’s tax description):
# Pseudocode representing tax calculation logic from the source article
def net_profit(buy_price, sell_price, quantity, tax_rate=0.01, tax_cap=5_000_000):
gross_revenue = sell_price * quantity
tax_per_item = max(0, int(sell_price * tax_rate))
total_tax = min(tax_per_item * quantity, tax_cap)
net_revenue = gross_revenue - total_tax
cost = buy_price * quantity
return net_revenue - cost
Items with high unit prices hit the tax cap quickly. For example, selling 1,000 items at 10,000 gold each incurs a tax of 100,000 gold (1% of 10 million), well below the 5 million cap. Selling 10,000 items at 10,000 gold each incurs a tax of 1 million gold (1% of 100 million, capped at 5 million). The agent prefers high-volume, low-price items where the tax is a smaller percentage of gross profit.
Comparison to Real Exchange APIs
| Dimension | Grand Exchange | Typical REST API | Typical WebSocket |
|---|---|---|---|
| Order book visibility | Opaque | Partial (top N levels) | Full depth |
| Historical data | 5-min aggregates | Tick-by-tick available | Real-time stream |
| Rate limits | Per-item, 4-hour window | Per-second, per-endpoint | Connection-based |
| Order types | Market only | Limit, stop, conditional | Limit, stop, conditional |
| Fill reporting | Inferred from inventory | Immediate via callback | Immediate via message |
| Short selling | Not allowed | Allowed with margin | Allowed with margin |
Note: Typical REST API and WebSocket columns are representative examples, not specific products.
The Grand Exchange API is closer to a rate-limited REST API than a WebSocket feed. The agent must poll for state changes and infer execution from side effects. This increases latency and reduces the agent’s ability to react to fast-moving markets.
Failure Modes
The agent fails in predictable ways:
-
Stale price data: if the OSRS Wiki API lags or the agent’s polling loop stalls, the ranking API uses outdated spreads. The author observed this during network instability, causing the agent to submit offers that were no longer profitable. Mitigation: the agent now checks the timestamp of the latest price update and skips ranking if data is older than 10 minutes.
-
Inventory lock: if the Java client crashes mid-trade, the agent may hold inventory without a corresponding offer history entry. The author encountered this during early testing. On restart, the agent could not calculate the acquisition cost. Mitigation: the client now writes offer history to disk before submitting each order, allowing recovery on restart.
-
Rate limit desync: if the agent’s rate limit counter drifts from the Grand Exchange’s internal counter (due to clock skew or missed updates), it submits offers that are rejected. The author observed this when the system clock was adjusted during a test run. Mitigation: the agent now syncs its rate limit counters by parsing rejection messages from the Grand Exchange API and resetting counters accordingly.
-
Model drift (hypothetical): the ML models assume spread distributions are stationary over short windows. If the game developer changes tax rates, buy limits, or item drop rates, the models would produce garbage rankings until retrained. The author has not observed this in practice but considers it a risk for long-running deployments.
The agent does not implement circuit breakers or anomaly detection beyond the mitigations above. It assumes the environment is stable and the API is reliable. In production, you would add health checks, fallback logic, and alerting.
When to Use This Pattern
Use this pattern when you need to build a market-making agent under strict API constraints and partial observability. The three-service split (data ingestion, orchestration, ranking) is reusable. The state management approach (inventory snapshot, offer history, rate limit counters) generalizes to any exchange with opaque order books and rate limits.
Avoid this pattern if you need sub-second latency or full order book depth. The five-minute polling loop and opaque execution reporting make the agent too slow for high-frequency strategies. You also cannot hedge or short, so the agent is exposed to directional risk.
The Grand Exchange is a useful sandbox for testing agent logic without capital risk. The constraints are artificial but representative. If your agent works here, the orchestration and state management will translate to real exchanges with minimal refactoring.
Technical Verdict
The three-service split is sound for latency-insensitive markets, but the opaque order book forces the agent to trade speed for robustness. The author reports consistent profitability across multiple item classes, with the agent successfully navigating rate limits and tax optimization. The most transferable insight is the state persistence model: inventory snapshots, offer history, and rate limit counters are necessary when fill reporting is delayed or inferred. This pattern breaks when you need sub-second execution or short selling. For learning market-making mechanics without capital risk, the Grand Exchange is an ideal testbed. The constraints are real enough to expose failure modes but forgiving enough to iterate quickly.
Further reading: Adventures in Algorithmic Trading on the Runescape Grand Exchange and Hacker News discussion.