Build on Allora
Build and Deploy a Forecaster

Build and Deploy a Forecaster

A forecaster is a worker that predicts how accurate other workers' inferences will be, instead of (or in addition to) answering the topic's question itself. For each inferer, it submits a forecasted loss — an estimate of the error that inferer's inference will show against the eventual ground truth. The network turns forecasted losses into regrets and weights and combines them into forecast-implied inferences, making the network inference context-aware — better than any individual model's output. Forecast and Synthesis explains the mechanism end to end.

⚠️

Standalone forecaster tooling is not published yet. The high-level AlloraWorker in the current Python SDK release (allora_sdk 1.0.6) automates the inferer role only; first-class forecaster support is in development on the SDK's public dev branch (opens in a new tab) for an upcoming release. If you want to participate on Allora today, the smoothest path is running an inference worker — see build a worker with the Python SDK.

That said, forecast submission itself is available in the published SDK through its lower-level RPC client: client.emissions.tx.insert_worker_payload() accepts a forecast_elements parameter — the same transaction the upcoming forecaster tooling drives. The steps below use that published surface.

Goal

Register a worker on an Allora testnet topic and submit a forecast — predicted losses for the topic's active inferers — using the published Python SDK (allora_sdk (opens in a new tab)).

Prerequisites

Steps

1. Install the SDK

pip install allora_sdk

2. Understand the payload

client.emissions.tx.insert_worker_payload() submits one worker payload per epoch. Alongside the required inference_value — your own answer to the topic's question — the payload can carry forecast_elements, one entry per inferer whose accuracy you are predicting:

forecast_elements = [
    {"inferer": "allo1...", "value": "0.05"},  # your predicted loss for this inferer
]

Both fields are strings: the inferer's allo... address and your forecasted loss for its inference in the current epoch. Because inference_value is required, a forecaster built on the published SDK always infers and forecasts — a combination the network explicitly supports (some workers do both).

3. Write the forecaster

Save this as forecaster.py, replacing the bodies of forecast_losses and my_inference with your models:

import asyncio
import os
 
from allora_sdk import AlloraRPCClient, FeeTier
from allora_sdk.protos.emissions.v9 import (
    CanSubmitWorkerPayloadRequest,
    GetForecastsAtBlockRequest,
    GetLatestNetworkInferencesRequest,
    GetUnfulfilledWorkerNoncesRequest,
    IsWorkerRegisteredInTopicIdRequest,
)
from allora_sdk.rpc_client.config import AlloraWalletConfig
 
TOPIC_ID = 69
POLL_SECONDS = 120
 
 
def forecast_losses(inferer_addresses: list[str], nonce: int) -> dict[str, float]:
    # Your ML model goes here: for each inferer, predict the loss of the
    # inference it submits this epoch.
    return {address: 0.05 for address in inferer_addresses}
 
 
def my_inference(nonce: int) -> float:
    # A worker payload always carries your own inference for the topic.
    return 123.45
 
 
async def main():
    client = AlloraRPCClient.testnet(
        wallet=AlloraWalletConfig(mnemonic=os.environ["ALLORA_WALLET_MNEMONIC"]),
        debug=False,
    )
    address = client.address
 
    # Register on the topic (first run only) -- forecasters register as workers,
    # exactly like inferers
    registered = client.emissions.query.is_worker_registered_in_topic_id(
        IsWorkerRegisteredInTopicIdRequest(topic_id=TOPIC_ID, address=address)
    )
    if not registered.is_registered:
        tx = await client.emissions.tx.register(
            topic_id=TOPIC_ID,
            owner_addr=address,
            sender_addr=address,
            is_reputer=False,
            fee_tier=FeeTier.PRIORITY,
        )
        result = await tx.wait()
        if result.code != 0:
            raise SystemExit(f"Registration failed (code {result.code}): {result.raw_log}")
        print(f"Registered {address} on topic {TOPIC_ID}")
 
    whitelisted = client.emissions.query.can_submit_worker_payload(
        CanSubmitWorkerPayloadRequest(topic_id=TOPIC_ID, address=address)
    )
    if not whitelisted.can_submit_worker_payload:
        raise SystemExit(f"{address} is not whitelisted on topic {TOPIC_ID} -- contact the topic creator")
 
    # Wait for an open submission window (an unfulfilled worker nonce)
    while True:
        resp = client.emissions.query.get_unfulfilled_worker_nonces(
            GetUnfulfilledWorkerNoncesRequest(topic_id=TOPIC_ID)
        )
        nonces = resp.nonces.nonces if resp.nonces is not None else []
        if nonces:
            nonce = nonces[0].block_height
            break
        print(f"No open submission window on topic {TOPIC_ID}, retrying in {POLL_SECONDS}s")
        await asyncio.sleep(POLL_SECONDS)
 
    # Find the inferers whose accuracy you are forecasting
    latest = client.emissions.query.get_latest_network_inferences(
        GetLatestNetworkInferencesRequest(topic_id=TOPIC_ID)
    )
    if latest.network_inferences is None or not latest.network_inferences.inferer_values:
        raise SystemExit(f"Topic {TOPIC_ID} has no network inferences yet -- nothing to forecast")
    inferers = [v.worker for v in latest.network_inferences.inferer_values]
 
    # Run your model and submit the forecast alongside your own inference
    losses = forecast_losses(inferers, nonce)
    pending = await client.emissions.tx.insert_worker_payload(
        topic_id=TOPIC_ID,
        inference_value=str(my_inference(nonce)),
        nonce=nonce,
        forecast_elements=[
            {"inferer": inferer, "value": str(loss)} for inferer, loss in losses.items()
        ],
        fee_tier=FeeTier.STANDARD,
    )
    result = await pending.wait()
    if result.code != 0:
        raise SystemExit(f"Submission failed (code {result.code}): {result.raw_log}")
    print(f"Forecast for {len(losses)} inferers submitted in transaction {result.txhash}")
 
 
asyncio.run(main())

4. Run it

export ALLORA_WALLET_MNEMONIC="<your wallet mnemonic>"
python forecaster.py

The script registers your address on the topic if needed, waits for the topic's next submission window, fetches the current inferers, and submits your forecasted losses alongside your own inference.

Verify

  • The terminal prints Forecast for N inferers submitted in transaction <hash>.

  • Read the forecast back from the chain — add these lines to the end of main() in forecaster.py:

      forecasts = client.emissions.query.get_forecasts_at_block(
          GetForecastsAtBlockRequest(topic_id=TOPIC_ID, block_height=nonce)
      )
      if forecasts.forecasts is not None:
          print([f.forecaster for f in forecasts.forecasts.forecasts])

    Your allo... address should appear in the printed list of forecasters.

  • Open testnet.explorer.allora.network/topics/69 (opens in a new tab) and look for your address among the topic's workers.

Troubleshoot

  • gRPC StatusCode.UNIMPLEMENTED with unknown service emissions.v9.QueryService — the network has been upgraded to a newer protobuf revision than the one bundled with your installed SDK release. Upgrade with pip 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).
  • ... is not whitelisted on topic ... — the topic restricts who may submit worker payloads. Contact the topic creator to get your address whitelisted, or use the sandbox topic (ID 69) (opens in a new tab).
  • Submission fails with code 75 or 78 — you already submitted a worker payload for this nonce. Each address submits at most one payload per epoch; wait for the next submission window.
  • Topic ... has no network inferences yet — forecasting needs inferences to forecast against. Pick a topic with active inferers (existing topics), or wait until the topic completes an epoch with inference submissions.
  • insufficient funds when registering or submitting — the wallet needs ALLO for gas. On testnet, request funds at faucet.testnet.allora.network (opens in a new tab).

Next