- Parses the uploaded file with ast.parse(). Syntax errors fail immediately at upload with 422.
- Checks blocked imports, blocked function calls, and blocked attribute access.
- Verifies exactly one Strategy subclass exists.
- Checks for write-mode open() calls.
- Detects Tier A or Tier B automatically. Tier C is assigned at upload for zip archives.
SDK Documentation
Complete reference for the rpsa_sdk package, organized around the decisions you make while building and validating a strategy.
Strategy Contract
Subclass Strategy, implement next_move(), and return one legal move every round.
Economy Context
Opt in with uses_context to read wallets, auctions, perks, and live match state.
Validation Ready
Understand AST scans, smoke tests, context checks, and the most common blockers.
Strategy Base Class
The required shape of every uploaded strategy file.
Import Strategy from rpsa_sdk. Your class must subclass it and implement next_move(context: Context) -> str.
Context API Reference
Live match data and economy actions available when uses_context is enabled.
When uses_context = True, the runtime injects a Context object before each call to next_move(). Read methods reflect current match state. Mutating methods run inside a DB savepoint and return a result dict; they never raise.
| Name | Description |
|---|---|
| context.credits() | Your current wallet balance as Decimal. Refreshed from the database each call. |
| context.opponent_wallet | Opponent wallet balance as a turn-start snapshot. |
| context.round | Current round number, 1-indexed. Round 0 is the pre-match bid window. |
| context.consecutive_wins() | Number of consecutive wins at the start of this turn. Returns 0 on a loss streak. |
| context.market_state() | Full market snapshot with total_rounds, posted_price_items, active_auctions, active_perks, and purchases. |
Place Auction Bid
context.bid(stock_id: str, amount: Decimal)
Returns { ok: True, bid_id: str } on success or { ok: False, error: str } on failure. Common errors include WINDOW_CLOSED, INSUFFICIENT_FUNDS, and INVALID_BID.
Activate Owned Perk
context.activate(perk_code: str, t_start: int, **params)
Returns activation metadata on success or an error result on failure. t_start must be at least context.round + 1. WindowWager and WindowHedge require a Decimal stake param.
notify(own_move, opp_move) to track it yourself between rounds.Import Allowlist
The sandbox validates imports before a strategy can run.
The sandbox enforces a strict allowlist at process startup. Importing anything outside these lists fails validation with an AST scan error.
Standard Library - Allowed
abc, builtins, collections, copy,
dataclasses, decimal, enum, functools,
heapq, itertools, json, math,
operator, pathlib, pickle, queue,
random, re, statistics, string,
struct, time, typing, uuidML Packages - Tier B/C
numpy, scipy, sklearn,
torch, torchvision,
tensorflow, keras,
xgboost, lightgbm,
pandas (read-only ops)os, sys, subprocess, socket, ctypes,
importlib, _socket, _subprocess, _ctypes,
# plus calls to eval, exec, compile, __import__
# and attributes: __class__, __subclasses__,
# __globals__, __builtins__
# and open(..., "w") or open(..., "a")Private C extension modules with names starting with _ are allowed unless explicitly blocked above.
Validation Pipeline
Every upload must pass three checks before season enrollment.
Every upload is queued for validation. All stages must pass before the strategy can be enrolled in a season. Validation typically completes in under 2 minutes.
- Runs your strategy for 100 rounds against AlwaysRock in a sandboxed subprocess.
- Uses a 2-minute total time limit and a per-turn budget.
- Records peak memory and fails memory violations.
- Passes when the strategy completes all 100 rounds without crash, timeout, or memory violation.
- Invalid move returns are noted in the report, but they do not fail the smoke test.
- Runs 50 rounds with a mock context whose credits cycle through 0, 10, 30, 100, and 500.
- Verifies the strategy can receive and use the context object without raising.
- Records bid() and activate() calls for economy diagnostics.
- Passes when the strategy completes all 50 rounds without exception.
random is allowed, but os.urandom is not. Calls like time.sleep() can time out, large state can exceed memory limits, and re-uploading resets validation to pending.Execution Tiers
Choose the smallest tier that fits your strategy.
Tier A
Pure PythonNo ML libraries. Uses only the standard library allowlist and rpsa_sdk. Fastest execution with the lowest sandbox overhead.
Simple Strategy
from rpsa_sdk import Strategy
class RockAlways(Strategy):
def next_move(self, context) -> str:
return "rock"Tier B
ML ImportsMay import NumPy, PyTorch, scikit-learn, and other ML packages from the allowlist. Detected by the AST scanner.
NumPy-Based
import numpy as np
from rpsa_sdk import Strategy
class RandomBot(Strategy):
def next_move(self, context) -> str:
return np.random.choice(["rock", "paper", "scissors"])Tier C
Model FileA zip archive containing strategy.py and a trained model file. Use this when your strategy loads a model artifact.
Loads Model From File
import pickle
from rpsa_sdk import Strategy
class ModelBot(Strategy):
def __init__(self):
with open(self.model_path, "rb") as f:
self.model = pickle.load(f)
def next_move(self, context) -> str:
return self.model.predict()Tier C ZIP Archive Guide
Ship a trained model alongside strategy.py.
Tier C uploads must be a .zip archive with this exact structure:
archive.zip
|-- strategy.py # required: must contain your Strategy subclass
|-- model.<ext> # required: pkl, pt, h5, joblib, onnx, ...
|-- requirements.txt # optional: extra allowed packagesAccessing the model path
Before the first call to next_move(), the runtime sets self.model_path to an absolute path for your extracted model file.
def __init__(self):
import pickle
with open(self.model_path, "rb") as f:
self.clf = pickle.load(f)Requirements and limits
- Use only packages from the ML allowlist.
- Avoid exact version pins unless necessary.
- Model file size: 200 MB or less.
- Total archive size: 210 MB or less.
- No unexpected files outside strategy.py, model, and requirements.txt.
Strategy Patterns
Useful building blocks for stronger entries.
These patterns separate a competitive entry from AlwaysRock. Combine them in a single strategy for better expected value.
Pattern 1 - Opponent Frequency Tracking
Track opponent move history through notify(), predict the next move from recent frequency, and counter it.
from collections import Counter
from rpsa_sdk import Strategy
class FrequencyCounter(Strategy):
uses_context = True
def __init__(self):
self._opp_history = []
self._beats = {"rock": "paper", "paper": "scissors", "scissors": "rock"}
def next_move(self, context) -> str:
if len(self._opp_history) < 10:
return "rock"
counts = Counter(self._opp_history[-20:])
predicted = counts.most_common(1)[0][0]
return self._beats[predicted]
def notify(self, own_move: str, opp_move: str) -> None:
self._opp_history.append(opp_move)Pattern 2 - Wallet-Adaptive Aggression
Read both wallets from context and shift aggression based on the capital gap.
from rpsa_sdk import Strategy
class WalletAdaptive(Strategy):
uses_context = True
def next_move(self, context) -> str:
my_wallet = context.credits()
opp_wallet = context.opponent_wallet
# Behind by more than 50 cr: switch to aggressive rock spam
if opp_wallet - my_wallet > 50:
return "rock"
# Comfortably ahead: play safe scissors to avoid paper traps
if my_wallet - opp_wallet > 30:
return "scissors"
return "paper"Pattern 3 - Auction-Aware Bidding
Read market_state(), bid during open auction windows, and activate owned perks at strategic rounds.
from decimal import Decimal
from rpsa_sdk import Strategy
class AuctionAware(Strategy):
uses_context = True
_bid_placed = False
def next_move(self, context) -> str:
market = context.market_state()
# Bid on LossCap in the pre-match Vickrey if we have not yet
if not self._bid_placed and context.round <= 1:
for auction in market["active_auctions"]:
if "LossCap" in auction.get("perk_id", ""):
result = context.bid(auction["stock_id"], Decimal("15"))
if result["ok"]:
self._bid_placed = True
# Activate LossCap once we own it; schedule it for the next round
owned = {p["perk_code"] for p in market.get("purchases", [])}
activated = {p["perk_code"] for p in market.get("active_perks", [])}
if "LossCap" in owned and "LossCap" not in activated and context.round == 5:
context.activate("LossCap", t_start=context.round + 1)
return "rock"market_state()["active_auctions"] every round. A stock with window_phase == "open" accepts bids; closed means the auction has cleared.Keep the docs close while iterating.
Jump between the SDK, the rules, and the participation guide when you are tuning a strategy for ranked play.