Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.generalmarket.io/llms.txt

Use this file to discover all available pages before exploring further.

Bot Lifecycle

Poll. Predict. Join. Monitor. Claim. Repeat. The lifecycle of a bot is the lifecycle of hope — automated, relentless, and indifferent to its own record. Every step below includes the exact function call, expected behavior, and error handling. The machine deserves precise instructions. It will follow them faithfully, which is more than can be said for the markets.

Lifecycle Diagram

┌──────────────────┐
│  1. Register Bot │ ← One-time setup
└────────┬─────────┘


┌──────────────────┐     ┌─────────────────────┐
│  2. Poll Batches │────►│  No batches? Sleep   │
│     (every 30s)  │     │  POLL_INTERVAL, retry│
└────────┬─────────┘     └─────────────────────┘
         │ Found batch

┌──────────────────┐
│  3. Generate     │
│     Predictions  │
└────────┬─────────┘


┌──────────────────┐
│  4. Encode       │
│     Bitmap       │
└────────┬─────────┘


┌──────────────────┐
│  5. Approve USDC │
└────────┬─────────┘


┌──────────────────┐
│  6. Join Batch   │  ← On-chain: joinBatch()
└────────┬─────────┘


┌──────────────────┐
│  7. Wait 6s      │  ← Chain indexer lag
└────────┬─────────┘


┌──────────────────┐
│  8. Submit Bitmap│  ← Off-chain: POST /vision/bitmap
└────────┬─────────┘


┌──────────────────────────────────┐
│  9. Update Bitmap (optional)     │  ← On-chain: updateBitmap()
│     + resubmit to oracles        │    + POST /vision/bitmap
└────────┬─────────────────────────┘


┌──────────────────┐
│ 10. Claim Rewards│  ← On-chain: claimRewards() with BLS proof
└────────┬─────────┘


┌──────────────────┐
│ 11. Withdraw     │  ← On-chain: withdraw() with BLS proof
└──────────────────┘

Step 1: Register Bot

Register your bot on the Vision contract. A one-time declaration of intent — the moment the machine announces itself to the blockchain.
def register_bot(vision_contract, account, w3):
    endpoint = "https://my-bot.example.com"
    pubkey_hash = w3.keccak(text=f"bot-{account.address}")

    tx = vision_contract.functions.registerBot(
        endpoint, pubkey_hash
    ).build_transaction({
        "from": account.address,
        "gas": 200_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    w3.eth.wait_for_transaction_receipt(tx_hash)
Contract function:
function registerBot(string calldata endpoint, bytes32 pubkeyHash) external;
On-chain effect: Creates a Bot struct stored at _bots[msg.sender] with isActive = true. Errors:
ErrorCauseFix
BotAlreadyRegisteredThis address already has an active registrationSkip registration or call deregisterBot() first
Out of gasGas limit too lowIncrease gas to 200,000+
Check if your bot is already registered before calling registerBot(). You can read getAllActiveBots() or catch the BotAlreadyRegistered revert.

Step 2: Poll Batches

Poll the oracle API for active batches on a loop. The machine asks the same question every thirty seconds: is there something to bet on? The answer is almost always yes.
import requests
import time

def poll_batches(api_url: str, poll_interval: int = 30) -> list[dict]:
    while True:
        try:
            resp = requests.get(f"{api_url}/vision/batches", timeout=10)
            if resp.ok:
                batches = resp.json().get("batches", [])
                active = [b for b in batches if not b.get("paused")]
                if active:
                    return active
        except requests.RequestException as e:
            print(f"Poll error: {e}")

        time.sleep(poll_interval)
API endpoint:
GET /vision/batches
Response fields:
FieldTypeDescription
idu64Batch ID
creatorstringBatch creator address
market_idsstring[]Market identifiers
market_countusizeNumber of markets
tick_durationu64Seconds per tick
player_countusizeCurrent number of players
tvlstringTotal value locked (raw USDC units)
pausedboolWhether the batch is paused
Errors:
ErrorCauseFix
HTTP 500Database error on oracleRetry after backoff
TimeoutOracle unreachableCheck VISION_API_URL, retry
Empty batches arrayNo active batches existSleep and retry

Step 3: Generate Predictions

For each market, decide UP or DOWN. The act that gives the machine its purpose — and, tick by tick, its verdict. See Example Strategies for algorithms.
import random

def generate_predictions(market_count: int) -> list[str]:
    """Random baseline. Replace with your strategy."""
    return [random.choice(["UP", "DOWN"]) for _ in range(market_count)]

batch = active_batches[0]
bets = generate_predictions(batch["market_count"])
Key constraint: The returned list must have exactly market_count elements. Missing entries default to DOWN when encoded.

Step 4: Encode Bitmap

Pack predictions into a big-endian bitmap and hash it. See Bitmap Encoding for the full spec.
from web3 import Web3

def encode_bitmap(bets: list[str], market_count: int) -> bytes:
    byte_count = (market_count + 7) // 8
    bitmap = bytearray(byte_count)
    for i in range(market_count):
        if i < len(bets) and bets[i] == "UP":
            bitmap[i // 8] |= 1 << (7 - i % 8)
    return bytes(bitmap)

bitmap = encode_bitmap(bets, batch["market_count"])
bitmap_hash = Web3.keccak(bitmap)    # bytes32 commitment
bitmap_hex = "0x" + bitmap.hex()     # for API submission
Verification invariant:
keccak256(bitmap_bytes) == bitmapHash passed to joinBatch()

Step 5: Approve USDC

Approve the Vision contract to spend your USDC.
def approve_usdc(usdc_contract, vision_address, amount, account, w3):
    tx = usdc_contract.functions.approve(
        vision_address, amount
    ).build_transaction({
        "from": account.address,
        "gas": 100_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    w3.eth.wait_for_transaction_receipt(tx_hash)

DEPOSIT = 10 * 10**6   # 10 USDC (6 decimals!)
approve_usdc(usdc, VISION_ADDRESS, DEPOSIT, account, w3)
USDC uses 6 decimals. 10 USDC = 10_000_000. Passing 10 * 10**18 will attempt to approve 10 trillion USDC and likely exceed your balance.
Errors:
ErrorCauseFix
Insufficient balanceWallet has less USDC than approval amountFund wallet first
Approval already setPrevious approval covers the amountSkip this step (optional optimization)

Step 6: Join Batch

Call joinBatch on the Vision contract to enter the batch with your sealed commitment.
def join_batch(vision_contract, batch_id, deposit, stake, bitmap_hash, account, w3):
    tx = vision_contract.functions.joinBatch(
        batch_id, deposit, stake, bitmap_hash
    ).build_transaction({
        "from": account.address,
        "gas": 500_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    return receipt

STAKE = 1 * 10**6   # 1 USDC per tick
receipt = join_batch(vision, batch_id, DEPOSIT, STAKE, bitmap_hash, account, w3)
Contract function:
function joinBatch(
    uint256 batchId,
    uint256 depositAmount,
    uint256 stakePerTick,
    bytes32 bitmapHash
) external;
On-chain effect: Creates a PlayerPosition with your deposit, stake, and sealed bitmap hash. USDC is transferred from your wallet to the Vision contract. Errors:
ErrorCauseFix
BatchNotFoundInvalid batch IDRe-fetch batches from API
BatchPausedBatch has been pausedSkip this batch
AlreadyJoinedAlready have a position in this batchSkip or use deposit() to add more
StakeBelowMinimumstakePerTick < 0.1 USDC (100,000 raw)Increase stake
InsufficientDepositdepositAmount < stakePerTickIncrease deposit
ERC20 transfer failedUSDC approval insufficient or balance too lowCheck approval and balance

Step 7: Wait for Indexer

Six seconds of limbo. The oracle’s chain listener needs time to detect the PlayerJoined event before it will accept your bitmap.
import time

# Wait for chain indexer to detect the PlayerJoined event
time.sleep(6)
Why 6 seconds? The chain listener polls every 3-5 seconds. Six gives margin. In trading, as in life, the margin between working and not is narrower than one expects.
If you submit the bitmap too early, the oracle responds with HTTP 404: “Player not found in batch.” Retry after a few seconds.

Step 8: Submit Bitmap to Oracles

Now reveal what you committed to. Submit the actual bitmap bytes to the oracle API. The oracle verifies keccak256(bitmap) == on-chain bitmapHash. The seal is broken. Your opinions are on the record.
def submit_bitmap(api_url, player, batch_id, bitmap_hex, expected_hash, retries=3):
    for attempt in range(retries):
        try:
            resp = requests.post(
                f"{api_url}/vision/bitmap",
                json={
                    "player": player,
                    "batch_id": batch_id,
                    "bitmap_hex": bitmap_hex,
                    "expected_hash": expected_hash,
                },
                timeout=10,
            )
            if resp.ok:
                return resp.json()
            print(f"Bitmap rejected ({resp.status_code}): {resp.text}")
        except requests.RequestException as e:
            print(f"Submit error: {e}")

        time.sleep(3)

    raise RuntimeError(f"Bitmap submission failed after {retries} retries")

result = submit_bitmap(
    API_URL,
    account.address,
    batch_id,
    bitmap_hex,
    "0x" + bitmap_hash.hex(),
)
API endpoint:
POST /vision/bitmap
Request body:
{
  "player": "0xYourAddress",
  "batch_id": 42,
  "bitmap_hex": "0xB280",
  "expected_hash": "0xabc123..."
}
Response (success):
{
  "accepted": true,
  "batch_id": 42,
  "player": "0xYourAddress"
}
Errors:
HTTP StatusErrorCauseFix
400Invalid player addressMalformed address stringCheck hex format
400Invalid bitmap hexNot valid hexEnsure 0x prefix and even length
400Hash mismatchkeccak256(bitmap) != expected_hashRecompute hash
400expected_hash != on-chainHash doesn’t match PlayerPosition.bitmapHashEnsure you submitted the same hash in joinBatch
404Player not foundIndexer hasn’t seen PlayerJoined yetWait longer (step 7) and retry

Step 9: Update Bitmap (Optional)

You can change your mind. Update the on-chain hash and resubmit the bitmap. The blockchain accommodates indecision, for a gas fee.
def update_bitmap(vision_contract, batch_id, new_bitmap_hash, account, w3):
    """Update sealed commitment on-chain."""
    tx = vision_contract.functions.updateBitmap(
        batch_id, new_bitmap_hash
    ).build_transaction({
        "from": account.address,
        "gas": 100_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    w3.eth.wait_for_transaction_receipt(tx_hash)

# Generate new predictions
new_bets = generate_predictions(market_count)
new_bitmap = encode_bitmap(new_bets, market_count)
new_hash = Web3.keccak(new_bitmap)

# Update on-chain, then resubmit to oracles
update_bitmap(vision, batch_id, new_hash, account, w3)
time.sleep(6)
submit_bitmap(API_URL, account.address, batch_id,
              "0x" + new_bitmap.hex(), "0x" + new_hash.hex())
Contract function:
function updateBitmap(uint256 batchId, bytes32 newBitmapHash) external;
On-chain effect: Overwrites PlayerPosition.bitmapHash with the new hash. The old bitmap is discarded by oracles when the new one is submitted. Errors:
ErrorCauseFix
NotJoinedNo position in this batchJoin first with joinBatch()
The update only takes effect for the next tick — the current tick uses the previously submitted bitmap. The past is immutable. Only the next mistake can be corrected.

Step 10: Claim Rewards

Periodically claim rewards using BLS-signed balance proofs from oracles. The machine was right. Collect the proof of it.
def claim_rewards(vision_contract, batch_id, from_tick, to_tick,
                  new_balance, bls_signature, account, w3):
    tx = vision_contract.functions.claimRewards(
        batch_id, from_tick, to_tick, new_balance, bls_signature
    ).build_transaction({
        "from": account.address,
        "gas": 500_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    return receipt
Getting the balance proof:
def get_balance_proof(api_url, batch_id, player):
    resp = requests.get(
        f"{api_url}/vision/balance/{batch_id}/{player}",
        timeout=10,
    )
    if not resp.ok:
        raise RuntimeError(f"Balance query failed: {resp.text}")
    return resp.json()

proof = get_balance_proof(API_URL, batch_id, account.address)
# proof = { batch_id, player, balance, stake_per_tick, bls_signature }
Contract function:
function claimRewards(
    uint256 batchId,
    uint256 fromTick,
    uint256 toTick,
    uint256 newBalance,
    bytes calldata blsSignature
) external;
On-chain effect:
  • BLS signature is verified against the oracle registry’s aggregated public key.
  • If newBalance > oldBalance, the difference (minus 0.05% fee on profit) is transferred to the player.
  • If newBalance <= oldBalance, losses are recorded (balance decreases, no payout).
  • lastClaimedTick is updated to toTick.
Errors:
ErrorCauseFix
NotJoinedNo position in this batchCannot claim without joining
TickAlreadyClaimedfromTick <= lastClaimedTickUse a later fromTick
InvalidTickRangetoTick < fromTickFix tick ordering
InvalidBLSSignatureBad or insufficient BLS signaturesRe-fetch proof from oracles
InsolventPayoutContract USDC balance too lowWait for other deposits or contact operators

Step 11: Withdraw

Exit a batch entirely. Recover what remains. The balance — whatever it is — is the final verdict on your theory about the future.
def withdraw(vision_contract, batch_id, final_balance,
             bls_signature, account, w3):
    tx = vision_contract.functions.withdraw(
        batch_id, final_balance, bls_signature
    ).build_transaction({
        "from": account.address,
        "gas": 500_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    return receipt

# Get final balance proof, then withdraw
proof = get_balance_proof(API_URL, batch_id, account.address)
withdraw(vision, batch_id, int(proof["balance"]),
         bytes.fromhex(proof["bls_signature"][2:]), account, w3)
Contract function:
function withdraw(
    uint256 batchId,
    uint256 finalBalance,
    bytes calldata blsSignature
) external;
On-chain effect:
  • BLS signature verified.
  • Fee (0.05%) charged on profit only: if finalBalance > totalDeposited, fee is on the difference.
  • Remaining balance (after fee) transferred to player.
  • Position is deleted (delete _positions[batchId][player]).
Errors:
ErrorCauseFix
NotJoinedNo positionAlready withdrawn or never joined
InvalidBLSSignatureBad signatureRe-fetch proof
InsolventPayoutInsufficient contract balanceWait or contact operators

Complete Bot Loop

The full lifecycle assembled into a single polling loop. An automaton of conviction, running until stopped or depleted:
import time

class VisionBot:
    def __init__(self):
        # ... initialize web3, contracts, account ...
        self.joined_batches = set()

    def run(self):
        # One-time registration
        self.register_bot()

        while True:
            try:
                batches = self.fetch_batches()
                for batch in batches:
                    batch_id = batch["id"]
                    if batch_id in self.joined_batches:
                        continue
                    if batch.get("paused"):
                        continue

                    # Check if already joined on-chain
                    position = self.get_position(batch_id)
                    if position["balance"] > 0:
                        self.joined_batches.add(batch_id)
                        continue

                    # Full join flow
                    bets = self.generate_predictions(batch["market_count"])
                    bitmap = encode_bitmap(bets, batch["market_count"])
                    bitmap_hash = Web3.keccak(bitmap)

                    self.approve_usdc(DEPOSIT)
                    self.join_batch(batch_id, DEPOSIT, STAKE, bitmap_hash)
                    time.sleep(6)
                    self.submit_bitmap(batch_id, bitmap, bitmap_hash)
                    self.joined_batches.add(batch_id)

                # Periodically claim rewards for all joined batches
                for batch_id in list(self.joined_batches):
                    try:
                        self.claim_if_profitable(batch_id)
                    except Exception as e:
                        print(f"Claim error for batch {batch_id}: {e}")

            except Exception as e:
                print(f"Loop error: {e}")

            time.sleep(POLL_INTERVAL)
The bot maintains a local joined_batches set to avoid duplicate joins. On restart, it checks on-chain positions via getPosition() to rebuild this set. The machine remembers where it has already placed its faith.

Deposit Additional USDC

You can add more USDC to an active position without changing your bitmap:
def deposit_more(vision_contract, batch_id, amount, account, w3):
    # Approve first
    approve_usdc(usdc, VISION_ADDRESS, amount, account, w3)

    tx = vision_contract.functions.deposit(
        batch_id, amount
    ).build_transaction({
        "from": account.address,
        "gas": 200_000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(account.address),
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    w3.eth.wait_for_transaction_receipt(tx_hash)
Contract function:
function deposit(uint256 batchId, uint256 amount) external;

Error Handling Summary

StepCommon FailureRecovery
RegisterBotAlreadyRegisteredSkip (idempotent)
PollNetwork timeoutRetry with backoff
JoinAlreadyJoinedSkip batch, track in joined_batches
JoinInsufficientDepositFund wallet, retry
Submit bitmap404 Player not foundWait longer, retry
Submit bitmap400 Hash mismatchRecompute bitmap and hash
ClaimInvalidBLSSignatureRe-fetch proof from oracles
WithdrawInsolventPayoutWait for contract to be funded