Build a Worker with the Python SDK
The Allora Python SDK's AlloraWorker turns a Python function into a network participant: it creates a wallet, registers on a topic, listens for the topic's submission windows, calls your function, and submits the returned prediction on-chain — with fee estimation and retries built in.
This guide builds a complete worker in two passes. First the walkthrough: start from a minimal worker, then swap in a real price model — a gradient-boosted tree regressor that predicts the BTC/USD price 24 hours ahead, adapted from the Allora Forge Builder Kit (opens in a new tab) notebooks. Then the configuration surface: wallets, networks, transaction fee tiers, and error handling.
Goal
Run a worker that predicts the BTC/USD price 24 hours ahead and submits it to Allora's testnet sandbox topic (ID 69) (opens in a new tab), configured with an explicit wallet, network, and fee tier.
Prerequisites
- Python 3.10–3.13
- An Allora API key — get one for free at developer.allora.network (opens in a new tab). On testnet, the worker uses it to automatically request ALLO gas from the faucet.
No Docker, no node infrastructure, and no manual wallet funding: AlloraWorker handles wallet creation, faucet requests, registration, and transaction submission for you.
Steps
1. Install the SDK and model dependencies
pip install allora_sdk numpy pandas requests scikit-learn2. Start with a minimal worker
Save this as worker.py. It submits a placeholder value — the real model replaces it in step 5:
import asyncio
import os
from allora_sdk import AlloraNetworkConfig, AlloraWorker
async def run_model(nonce: int) -> float:
return 123.45 # placeholder -- the real model replaces this in step 5
async def main():
worker = AlloraWorker.inferer(
run=run_model,
topic_id=69,
network=AlloraNetworkConfig.testnet(),
api_key=os.environ["ALLORA_API_KEY"],
)
async for result in worker.run():
if isinstance(result, Exception):
print(f"Inference worker error: {result}")
else:
print(f"Prediction submitted to Allora: {result.submission}")
asyncio.run(main())Run it:
export ALLORA_API_KEY="<your key from developer.allora.network>"
python worker.pyOn the first run, the worker walks you through network onboarding automatically:
- It connects to Allora's testnet, where no real funds are exchanged.
- It asks for a wallet mnemonic — press Enter to have one generated for you. Your identity (an
allo...address) is saved to a.allora_keyfile in the working directory and reused on later runs. - If the wallet's balance is low, it requests a small amount of ALLO — the network's gas currency — from the testnet faucet, using your API key.
- It registers the worker on Allora's sandbox topic (ID 69) (opens in a new tab), a topic for newcomers to verify their setup. There are no penalties for submitting inaccurate inferences to this topic.
The worker then listens for the topic's submission windows and calls run_model each time one opens. Once you have seen it submit (or you are satisfied the onboarding completed), press Ctrl-C once for a graceful shutdown and move on to the model.
3. Build the price model
Topic 69 asks for the BTC/USD price 24 hours ahead. Save this as model.py:
"""Train a small BTC/USD price model for Allora's sandbox topic (ID 69).
Topic 69 asks for the BTC/USD price 24 hours ahead. This script:
1. fetches hourly BTC/USDT candles from Binance's public REST API,
2. builds log-return features over several look-back horizons,
3. trains a gradient-boosted tree model to predict the log return
24 hours ahead, with walk-forward validation,
4. converts the predicted log return back into a price.
"""
import numpy as np
import pandas as pd
import requests
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import TimeSeriesSplit
BINANCE_KLINES_URL = "https://api.binance.com/api/v3/klines"
SYMBOL = "BTCUSDT"
INTERVAL = "1h" # hourly candles
CANDLES = 1000 # max candles per request (~41 days of history)
TARGET_BARS = 24 # predict 24 hours ahead
RETURN_HORIZONS = [1, 6, 12, 24] # look-back horizons, in hours
FEATURE_COLS = [f"log_return_{h}h" for h in RETURN_HORIZONS] + ["volatility_24h"]
def fetch_candles(limit: int = CANDLES) -> pd.DataFrame:
"""Fetch hourly OHLCV candles from Binance's public market data API."""
response = requests.get(
BINANCE_KLINES_URL,
params={"symbol": SYMBOL, "interval": INTERVAL, "limit": limit},
timeout=10,
)
response.raise_for_status()
columns = [
"open_time", "open", "high", "low", "close", "volume", "close_time",
"quote_volume", "n_trades", "taker_base_volume", "taker_quote_volume", "unused",
]
df = pd.DataFrame(response.json(), columns=columns)
df["open_time"] = pd.to_datetime(df["open_time"], unit="ms", utc=True)
df["close"] = df["close"].astype(float)
return df[["open_time", "close"]]
def build_features(df: pd.DataFrame) -> pd.DataFrame:
"""Add log-return features, rolling volatility, and the training target."""
out = df.copy()
log_close = np.log(out["close"])
for horizon in RETURN_HORIZONS:
out[f"log_return_{horizon}h"] = log_close.diff(horizon)
out["volatility_24h"] = log_close.diff().rolling(24).std()
# Target: the log return over the NEXT TARGET_BARS hours (NaN for recent rows)
out["target"] = log_close.shift(-TARGET_BARS) - log_close
return out
def make_model() -> HistGradientBoostingRegressor:
return HistGradientBoostingRegressor(
max_iter=300,
learning_rate=0.05,
max_depth=3,
max_leaf_nodes=15,
random_state=42,
)
def train() -> HistGradientBoostingRegressor:
"""Train the model with walk-forward validation, then fit on all data."""
df = build_features(fetch_candles()).dropna()
print(f"Dataset: {len(df)} hourly samples "
f"({df['open_time'].iloc[0]} to {df['open_time'].iloc[-1]})")
# Walk-forward cross-validation with a TARGET_BARS embargo between
# train and test folds, so the target never leaks across the split.
tscv = TimeSeriesSplit(n_splits=3, gap=TARGET_BARS)
for fold, (train_idx, test_idx) in enumerate(tscv.split(df), start=1):
model = make_model()
model.fit(df.iloc[train_idx][FEATURE_COLS], df.iloc[train_idx]["target"])
preds = model.predict(df.iloc[test_idx][FEATURE_COLS])
actual = df.iloc[test_idx]["target"].to_numpy()
mae = np.mean(np.abs(preds - actual))
directional = np.mean(np.sign(preds) == np.sign(actual))
print(f"Fold {fold}: MAE (log return) = {mae:.5f} | "
f"directional accuracy = {directional:.1%}")
final_model = make_model()
final_model.fit(df[FEATURE_COLS], df["target"])
print(f"Final model trained on {len(df)} samples")
return final_model
def predict_price(model: HistGradientBoostingRegressor) -> float:
"""Predict the BTC/USD price TARGET_BARS hours from now."""
features = build_features(fetch_candles())
current_price = float(features["close"].iloc[-1])
predicted_log_return = float(model.predict(features[FEATURE_COLS].iloc[[-1]])[0])
# Convert the predicted log return back into a price
return current_price * float(np.exp(predicted_log_return))
if __name__ == "__main__":
model = train()
price = predict_price(model)
print(f"Predicted BTC/USD price in {TARGET_BARS} hours: {price:,.2f}")This is a teaching example, not a profitable trading model — on ~40 days of hourly data, expect directional accuracy near a coin flip. To engineer stronger features, search hyperparameters, and evaluate against the network's scoring metrics, use the Forge Builder Kit (opens in a new tab), which this example is adapted from. The model, look-back horizons, data source, and asset are all yours to swap out — the only contract with the network is that run_model returns your prediction as a float.
4. Train and test the model locally
This step needs no API key and touches nothing on-chain:
python model.pyYou should see the dataset summary, one line per validation fold, and a test prediction — with numbers reflecting current market data:
Dataset: 952 hourly samples (2026-06-20 02:00:00+00:00 to 2026-07-29 17:00:00+00:00)
Fold 1: MAE (log return) = 0.02211 | directional accuracy = 37.4%
Fold 2: MAE (log return) = 0.01550 | directional accuracy = 42.4%
Fold 3: MAE (log return) = 0.01235 | directional accuracy = 51.7%
Final model trained on 952 samples
Predicted BTC/USD price in 24 hours: 65,279.195. Swap the model into the worker
Replace worker.py with this version, which trains the model at startup and submits a fresh prediction every time a submission window opens:
import asyncio
import os
from allora_sdk import AlloraNetworkConfig, AlloraWorker
from model import predict_price, train
# Train once at startup (retrain and restart as often as you like)
model = train()
async def run_model(nonce: int) -> float:
prediction = predict_price(model)
print(f"Predicted BTC/USD price in 24 hours: {prediction:,.2f}")
return prediction
async def main():
worker = AlloraWorker.inferer(
run=run_model,
topic_id=69,
network=AlloraNetworkConfig.testnet(),
api_key=os.environ["ALLORA_API_KEY"],
)
async for result in worker.run():
if isinstance(result, Exception):
print(f"Inference worker error: {result}")
else:
print(f"Prediction submitted to Allora: {result.submission}")
asyncio.run(main())Run it again — it reuses the .allora_key identity from step 2:
python worker.pyThe run function is the whole contract between your model and the network: it can be sync or async, it receives the submission window's nonce (a block height), and it returns the prediction as a float or str. Sync functions are run in an executor so a slow model does not block the worker's event loop.
6. Configure the wallet
With no wallet argument, the worker reads the mnemonic in .allora_key (creating it interactively on first run, as in step 2). For servers, CI, or containers, configure the wallet explicitly with AlloraWalletConfig:
import os
from allora_sdk.rpc_client.config import AlloraWalletConfig
# One of the following:
# A mnemonic phrase from an environment variable
wallet = AlloraWalletConfig(mnemonic=os.environ["ALLORA_WALLET_MNEMONIC"])
# A hex-encoded private key from an environment variable
wallet = AlloraWalletConfig(private_key=os.environ["ALLORA_WALLET_PRIVATE_KEY"])
# A file containing the mnemonic (what the worker uses by default as `.allora_key`)
wallet = AlloraWalletConfig(mnemonic_file="/path/to/allora_key")
# Or read PRIVATE_KEY / MNEMONIC / MNEMONIC_FILE / ADDRESS_PREFIX
# from the environment in one call
wallet = AlloraWalletConfig.from_env()Pass the result as AlloraWorker.inferer(wallet=wallet, ...). Credentials are tried in that order: an explicit private_key wins, then mnemonic, then mnemonic_file (falling back to .allora_key in the working directory).
The mnemonic in .allora_key is your worker's identity and funds — never commit it, and keep a backup. Use environment variables or mounted files for secrets, never hardcoded strings. Run one worker process per wallet: transactions from one account must be submitted in strict sequence order, and two processes signing with the same key will conflict.
7. Choose a network
AlloraNetworkConfig ships presets, and every field can be overridden:
from allora_sdk import AlloraNetworkConfig
# Presets
network = AlloraNetworkConfig.testnet() # allora-testnet-1, with faucet support
network = AlloraNetworkConfig.mainnet() # allora-mainnet-1
network = AlloraNetworkConfig.local() # a local node on localhost:26657
# Or specify everything yourself
network = AlloraNetworkConfig(
chain_id="allora-testnet-1",
url="grpc+https://allora-grpc.testnet.allora.network:443",
websocket_url="wss://allora-rpc.testnet.allora.network/websocket",
fee_denom="uallo",
fee_minimum_gas_price=10.0,
faucet_url="https://faucet.testnet.allora.network",
)- The
urlscheme selects the wire protocol:grpc+http(s)://uses gRPC,rest+http(s)://uses the Cosmos-LCD REST API. websocket_urlpowers the event subscriptions the worker uses to react to submission windows the moment they open; between events it also polls (polling_interval, default 120 seconds).faucet_urlis only set on the testnet preset — automatic gas top-ups are a testnet convenience. On mainnet, fund the worker's address with ALLO yourself.
8. Choose a fee tier
Every transaction the worker sends (registration, inference submission) pays gas. fee_tier controls how much you pay above the network minimum to prioritize inclusion within an epoch:
from allora_sdk import FeeTier
worker = AlloraWorker.inferer(
run=run_model,
topic_id=69,
fee_tier=FeeTier.PRIORITY,
api_key=os.environ["ALLORA_API_KEY"],
)| Tier | Gas price | Use when |
|---|---|---|
FeeTier.ECO | Network minimum | Cost matters more than inclusion speed |
FeeTier.STANDARD (default) | 1.5× minimum | Everyday operation |
FeeTier.PRIORITY | 2.5× minimum | Submission windows are short or the chain is busy and you cannot afford to miss an epoch |
Registration transactions are always sent at PRIORITY so your worker can start participating as soon as possible; the tier you pass applies to inference submissions.
9. Handle errors
worker.run() is an async generator that yields a result per submission attempt — either a PredictionResult (with .prediction and .tx_result, including the transaction hash) or an Exception. Handle both, and use TxError to distinguish on-chain rejections from model failures:
import asyncio
import os
from allora_sdk import AlloraNetworkConfig, AlloraWorker
from allora_sdk.rpc_client.tx_manager import TxError
async def run_model(nonce: int) -> float:
return 123.45 # your model here
async def main():
worker = AlloraWorker.inferer(
run=run_model,
topic_id=69,
network=AlloraNetworkConfig.testnet(),
api_key=os.environ["ALLORA_API_KEY"],
)
async for result in worker.run():
if isinstance(result, TxError):
# The chain rejected the transaction
print(f"Tx failed: code={result.code} {result.message} (tx: {result.tx_hash})")
elif isinstance(result, Exception):
# Your run function raised, or a network/RPC error occurred
print(f"Worker error: {result}")
else:
print(f"Submitted {result.submission} (tx: {result.tx_result.txhash})")
asyncio.run(main())What the worker already does for you:
- Exceptions from your
runfunction are caught and yielded — one bad prediction never crashes the worker loop. - "Already submitted for this epoch" rejections are recognized and logged as warnings; the nonce is marked done and not retried.
- Not whitelisted: if the topic restricts who may submit and your wallet is not on the list, the worker logs
The wallet ... is not whitelisted on topic ...and stops — contact the topic creator. - Faucet rate limiting: after a
429from the testnet faucet the worker exits with an error; fund the wallet from another source or wait, then restart. - Shutdown: Ctrl-C (or SIGTERM, e.g. from a process manager) triggers a graceful shutdown; a second Ctrl-C force-exits. In notebooks, call
worker.stop()or pass atimeout(seconds) toworker.run(timeout=...)to stop after a fixed run. - Transient RPC errors in the polling loop are logged and retried on the next cycle rather than raised.
Verify
- The startup logs show the worker's wallet address and balance, and on the first run the faucet top-up.
- Each time a submission window for the topic opens, the terminal prints
Predicted BTC/USD price in 24 hours: ...followed byPrediction submitted to Allora: ..., and the log shows✅ Successfully submitted: topic=69 nonce=...with a transaction hash. - Open testnet.explorer.allora.network/topics/69 (opens in a new tab) and look for your worker's
allo...address among the topic's workers. - Export your worker's submission history to CSV with the bundled CLI tool:
allora-export-txs --address <your allo... address>. - For dashboards, on-chain scores, and EMA queries, see Monitor a Worker.
Coming in the next SDK release
The SDK's public dev branch (opens in a new tab) extends AlloraWorker beyond the current release (allora_sdk 1.0.6). These features are not yet published to PyPI, and their APIs may still change:
-
Auto-staking rewards: pass
autostake=AutoStakeConfig(...)toAlloraWorker.inferer()to automatically delegate the worker's settled rewards to a reputer (AutoStakeTargetType.REPUTER, anallo1...reputer registered on your topic) or to a validator (AutoStakeTargetType.VALIDATOR, anallovaloper1...operator address). The target is validated at startup, an optionalfee_reserve_uallokeeps gas money unstaked, and each rewards-settlement event triggers one delegation:from allora_sdk.worker.autostake import AutoStakeConfig, AutoStakeTargetType worker = AlloraWorker.inferer( run=run_model, topic_id=69, autostake=AutoStakeConfig( target_type=AutoStakeTargetType.REPUTER, target_address="allo1...", # or AutoStakeTargetType.VALIDATOR with allovaloper1... fee_reserve_uallo=1_000_000, # keep this much of each reward for gas ), ) -
RunContextcallbacks:runfunctions receive aRunContext(with.nonce,.topic_id, and an RPC.clientfor chain queries) instead of a bare nonce, and can return adict[str, float]for multi-value topics. -
Sanity checks: submissions are compared against the network consensus with a z-score check (configurable via
SanityCheckConfig), warning you if your prediction looks like the wrong unit or target variable. -
More roles:
AlloraWorker.forecaster(...)andAlloraWorker.reputer(...)counterparts to the inferer — see Build and Deploy a Forecaster and Build a Reputer for the current state of each.
Troubleshoot
KeyError: 'ALLORA_API_KEY'— the environment variable is not set in the current shell. Runexport ALLORA_API_KEY="<your key>"first; free keys are available at developer.allora.network (opens in a new tab).- Worker prompts
Mnemonic:on startup — no wallet was configured and no.allora_keyfile exists yet. Press Enter to generate a fresh identity, or configure one explicitly (step 6). Back up the resulting.allora_keyfile; delete it to start over with a new identity. - gRPC
StatusCode.UNIMPLEMENTEDwithunknown service emissions.vN.QueryService— the network has been upgraded to a newer protobuf revision than the one bundled with your installed SDK release. Upgrade withpip install --upgrade allora_sdk; if the newest release still fails, the deployed network is ahead of the latest SDK release — check the SDK issue tracker (opens in a new tab). RuntimeError: asyncio.run() cannot be called from a running event loop— you are in a notebook (Jupyter/Colab), where an event loop is already running. Replaceasyncio.run(main())withawait main().Too many faucet requests— the testnet faucet is rate-limited. Send ALLO to your worker's address from another wallet, or request funds manually at faucet.testnet.allora.network (opens in a new tab).The wallet ... is not whitelisted on topic ...— the topic restricts submitters and stops the worker. Pick an open topic (the sandbox topic 69 has no whitelist) or contact the topic creator.requests.exceptions.HTTPError: 451frommodel.py— Binance's public API is unavailable in some regions. Swapfetch_candlesfor any other candle source; the rest of the pipeline only needs a DataFrame withopen_timeandclosecolumns.
Next
- Productionize your worker: deploy it with Docker
- Watch it work: monitor a worker — dashboards, logs, and on-chain EMA scores
- Graduate from the sandbox: pick a real topic from existing topics
- Explore the SDK's full surface — RPC queries, transactions, and the REST API client: Allora Python SDK
- Train and deploy a model end to end with the Forge Builder Kit (opens in a new tab)