The framework has four modules. Core, Chain, Feed, Tracker. Four ways to interact with uncertainty.The Vision Bot Python framework (vision-bot/) does the tedious work — RPC calls, bitmap encoding, position tracking — so you can focus on the only part that matters: the predict() method. One function. A list of markets in, a list of UP/DOWN strings out. Everything else is plumbing around that single act of hubris.
The core module provides three things: bitmap helpers, the Strategy base class, and RiskCheck. The alphabet of prediction — encoding, commitment, and the illusion of control.Bitmap encoding converts a list of "UP" / "DOWN" strings into packed bytes. Each prediction is one bit (1 = UP, 0 = DOWN), big-endian order:
The Executor class wraps all contract calls behind a clean interface. One wallet, two contracts — Vision and USDC. The bridge between your intentions and the immutable record of their consequences.
Vision runs on L3 where USDC uses 18 decimals, not 6. A 10 USDC deposit is 10 * 10**18, not 10 * 10**6. The framework handles this with DECIMALS = 18. Get it wrong and you deposit dust — twelve orders of magnitude between intention and reality.
Key chain functions beyond the Executor:
Function
Purpose
fetch_batches(api_url)
Get active batches (oracle API -> vision-batches.json fallback -> on-chain scan)
VisionFeed maintains a background WebSocket connection to the data-node, receiving live market prices. The world, streaming in as numbers. It auto-reconnects with exponential backoff and falls back to HTTP — because the connection to reality is always intermittent.
The Tracker monitors all active positions each poll cycle. It fetches balances, computes PnL, auto-claims rewards, auto-withdraws depleted positions. It is the accountant — the one who tallies what the strategist refuses to see.
Tracker.check_all() (called every poll cycle) | | For each active position: | +---> GET /vision/balance/{batch_id}/{player} | from oracle API | +---> Update balance + PnL | +---> PnL > claim_above? ----YES----> GET BLS signature | | from oracle | NO | | | +---> claimRewards() on-chain | v +---> balance < withdraw_below? --YES--> GET BLS signature | | from oracle | NO | | | +---> withdraw() on-chain | v | +---> Save to pnl.json +---> Remove from active
Poll. Predict. Join. Monitor. Claim. Sleep. Repeat. The main loop in bot.py calls run_cycle() on a fixed interval. The lifecycle of a bot is the lifecycle of hope — endlessly renewed, endlessly tested:
The moment of truth — or rather, the moment of opinion. For each eligible batch, the framework fetches the market list, subscribes to the WebSocket feed for live prices, and calls strategy.predict(markets).The returned ["UP", "DOWN", ...] list is encoded into a bitmap and hashed. Your theory about the future, compressed into bits.
Joining requires four transactions — the bureaucracy of commitment, all handled automatically:
USDC.approve(Vision, amount) | v Vision.depositBalance(amount) <-- credits your Vision balance | v Vision.joinBatch( <-- locks deposit + records bitmap hash batchId, configHash, depositAmount, stakePerTick, bitmapHash ) | v POST /vision/bitmap <-- reveals actual bitmap to oracles to all oracle nodes (with retry + backoff)
# strategies/mean_reversion.pyfrom framework.core import Strategyclass MeanReversionStrategy(Strategy): name = "mean-reversion" def predict(self, markets: list[dict]) -> list[str]: """Bet against recent momentum -- contrarian approach.""" bets = [] for m in markets: change = m.get("change") or 0 # If it went up recently, bet DOWN (expect reversion) if change > 0: bets.append("DOWN") else: bets.append("UP") return bets
Set strategy = "mean-reversion" in your config.toml (or STRATEGY env var).
That is all. The loader in core.py scans every module in strategies/, finds the class whose name matches, and instantiates it. Your conviction, summoned by a string.
Your predict() method receives a list of market dicts and must return a list of "UP" or "DOWN" strings, one per market, in the same order. Binary. No nuance, no hedging, no “perhaps.” The market demands a verdict.Input — each market dict:
{ "id": "bitcoin", # asset identifier "price": 67234.50, # current price (float) "change": 2.1, # percent change (float or None) "volume": 1234567890.0, # 24h volume (float or None) "market_cap": None, # market cap (float or None)}
Output — list of strings:
["UP", "DOWN", "UP", "UP", "DOWN", "UP", ...]
The framework handles everything after predict() returns: encoding, hashing, on-chain submission, bitmap reveal. You provide the opinion. The machine provides the consequences.
For strategies that need historical data, the VisionFeed is available in the bot’s main loop. To access it from your strategy, you can accept it as a constructor argument:
class HistoryStrategy(Strategy): name = "history-aware" def __init__(self, feed=None): self.feed = feed def predict(self, markets): bets = [] for m in markets: if self.feed: hist = self.feed.history("current_batch", m["id"]) if len(hist) > 10: avg = sum(p["price"] for p in hist[-10:]) / 10 bets.append("UP" if m["price"] < avg else "DOWN") continue bets.append("UP") return bets
The markets.json file maps sources to their market configurations. It is consumed by the deployment scripts and data-node, not directly by the bot. The bot fetches market lists dynamically via fetch_batch_config().
Beyond RiskCheck, the main loop applies several more guards before joining a batch:
New batch candidate | v +-- batch_id < min_batch_id? -----> SKIP | +-- batch_id in tracker.active? --> SKIP (already tracking) | +-- batch_ids set and batch_id | not in allowlist? ------------> SKIP | +-- batch.paused? ----------------> SKIP | +-- tracker.active >= max_batches?-> SKIP (at capacity) | +-- Already joined on-chain? -----> SKIP (previous run) | +-- risk.can_join(deposit)? ------> SKIP (exposure limit) | +-- usdc_balance < deposit? ------> SKIP (insufficient funds) | v PROCEED TO JOIN
Capital is at risk. Every USDC deposited is staked against other players. If your predictions are consistently wrong, your balance trends to zero. The auto_withdraw feature exits positions before they are fully depleted, but losses are real, irreversible, and indifferent to your intentions.
# Set required env varexport BOT_PRIVATE_KEY="0xYOUR_PRIVATE_KEY"# Run with default config.tomlcd vision-botpython bot.py# Run with a specific config filepython bot.py --config path/to/config.toml# Run a single cycle (useful for testing / cron jobs)python bot.py --once