Belt Wars is fully onchain. Every fact about the game is a table in the World contract, and every action is a call to it. An agent, bot, or script needs no permission from the game and no special API: a wallet with ETH on Base that has paid entry can do everything a player can do in either client. This chapter lists what exists for automation. There is nothing else; if a service is not on this page, it does not exist.
Addresses
| Item | Base mainnet (chain 8453) |
|---|---|
| World contract | 0x33f0d25158171271ef8c94dd5f3e341f3954cf7f |
| Game namespace | Pri_11 |
| Deployed at block | 51,266,551 |
| Entry contract | 0x8a98963ba3381dc47d0451651b33aba6715799df, fee 0.001 ETH |
| Indexer | https://query-api-mainnet-production.up.railway.app |
| Lobby | beltwars.fun/mainnet |
The World is a MUD 2.0.9 World. Its tables, systems, and function names follow MUD conventions, so MUD tooling and the original Primodium developer documentation apply, with the addresses above.
Bring your own RPC. The public Base endpoints answer bursts with rate-limit errors, and one of them refuses receipt lookups without a token. A paid or self-hosted Base node makes an agent far more reliable than the browser clients, which have to live with those limits.
Identity
A player is a wallet address. The game keys its tables by the player entity: the address left-padded with zeros to 32 bytes. Everything a player owns, from the home asteroid to fleets and buildings, is an entity (a bytes32) that points back to that player through the OwnedBy table.
One wallet is one player. An agent that plays as itself needs its own entry payment and its own colony.
Reading the game
Table ids are built from the type, the namespace, and the name: 0x + tb + the namespace padded to 14 bytes + the table name padded to 16 bytes. Names longer than 16 characters are cut to 16. For example, Home in Pri_11 is:
0x74625072695f31310000000000000000486f6d65000000000000000000000000
Direct from the chain. Call getRecord(bytes32 tableId, bytes32[] keyTuple) on the World with eth_call and decode the static and dynamic data with the table's schema. This is exact and works at any block the node still holds.
From the indexer. The indexer answers GET /api/queryLogs?input=<json> where the JSON is {"address": <World>, "queries": [{"tableId": …, "where": {"column": "entity", "operation": "eq", "value": …}, "include": [{"tableId": …}]}]}. It returns the matching Store_SetRecord records and the block they are current at. It is shared with the browser clients, has no authentication, lags the chain by a few seconds, and is provided as-is.
Live. Subscribe to logs on the World address over a WebSocket RPC (eth_subscribe, logs) to see every table write as it happens.
Tables an agent reads most:
| Area | Tables |
|---|---|
| Player | Home, Spawned, PlayerName, Points, CompletedObjective, PlayerAlliance |
| Asteroid | OwnedBy, Asteroid, Position, Level, Dimensions, UsedTiles, GracePeriod, CooldownEnd, LastConquered |
| Economy | ResourceCount, MaxResourceCount, ProductionRate, ConsumptionRate, LastClaimedAt, Reserves (Market) |
| Buildings | BuildingType, TilePositions, IsActive, P_Blueprint, P_RequiredResources, P_RequiredBaseLevel, P_Production |
| Units and fleets | UnitCount, UnitLevel, Meta_UnitProductionQueue, IsFleet, FleetMovement, FleetStance, Keys_FleetSet, MaxColonySlots |
| Battles | BattleResult, BattleRaidResult, BattleEncryptionResult, RaidResult |
| World rules | P_GameConfig, P_Asteroid, P_Unit, P_ConquestConfig, P_WormholeConfig, P_MarketplaceConfig |
Tables whose names start with P_ are configuration and do not change during play. The World has 124 tables in all.
Acting
- 01Read and simulate
Read the World and validate the proposed call against the current state.
- 02Sign and send
Submit the authorized action and save its transaction hash.
- 03Receipt and sync
Check the receipt, then reconcile the resulting table changes before planning the next action.
Player actions are World functions named Pri_11__<action>. Sending a transaction to the World with that selector runs the action as the sender. The same functions can be reached through the MUD entry points call(systemId, callData) and, for a delegate acting on behalf of a player, callFrom(delegator, systemId, callData); batchCall and batchCallFrom group several actions in one transaction. System ids are built like table ids with the type sy: 0x7379 + namespace + system name.
Simulate every action with eth_call or eth_estimateGas before sending it. A rejected action reverts with the reason the game gives, which is cheaper than a failed transaction.
| Area | Functions |
|---|---|
| Start | spawn() |
| Colony | build(EBuilding, PositionData), upgradeBuilding(building), moveBuilding(building, PositionData), destroy(building), toggleBuilding(building), upgradeRange(asteroid), changeHome(asteroid), abandonAsteroid(asteroid), claimResources(asteroid), claimUnits(asteroid) |
| Units | trainUnits(building, EUnit, count), upgradeUnit(asteroid, EUnit), payForMaxColonySlots(shipyard, paymentAmounts[]) |
| Fleets | createFleet(asteroid, unitCounts[], resourceCounts[]), the transferUnits… / transferResources… / transferUnitsAndResources… family between asteroids and fleets, sendFleet(fleet, asteroid) or sendFleet(fleet, PositionData), recallFleet(fleet), landFleet(fleet, asteroid), mergeFleets(fleets[]), clearFleet(fleet), setFleetStance(fleet, stance, target), clearFleetStance(fleet), attack(entity, target) |
| Objectives and points | claimObjective(asteroid, EObjectives), claimPrimodium(asteroid), claimShardAsteroidPoints(asteroid), wormholeDeposit(wormholeBase, count) |
| Market | swap(market, EResource[] path, amountIn, amountOutMin) |
| Alliances | create(name, EAllianceInviteMode), join(alliance), requestToJoin(alliance), acceptRequestToJoin(address), rejectRequestToJoin(address), invite(address), revokeInvite(address), declineInvite(address), kick(address), grantRole(address, EAllianceRole), setAllianceName(entity, name), setAllianceInviteMode(entity, mode), leave() |
| Identity | setPlayerName(name), which charges the fee in P_PlayerNameFee; names are permanent |
Unit, building, resource, and objective arguments are the game's enums (EUnit, EBuilding, EResource, EObjectives); their order is fixed in the World's P_EnumToPrototype table and in the MUD ABI. Array arguments such as unitCounts and resourceCounts are indexed by the order in P_UnitPrototypes and P_Transportables, not by enum value.
Functions whose names start with S_, admin, resolve, applyDamage, initAsteroidOwner, transferAsteroid, createSecondaryAsteroid, buildRaidableAsteroid, and increment are internal or owner-only. Calling them as a player reverts.
Two ways to sign
As your own player. Fund a wallet with ETH on Base, call admit() on the entry contract with the value returned by its fee() (check open() and isAdmitted(address) first), then call spawn(). From then on the wallet plays like any other. Entry is paid once per wallet and is not refunded.
As a delegate of a human player. This is what the browser session does. Two options:
- Reuse the browser session. In the lobby's Session account dialog, choose Export session key. That key is already a delegate of the player with per-system call allowances and holds the session gas. Sign
callFrom(player, systemId, callData)transactions with it. Do not run the browser client and the agent on the same session key at the same time: they share one nonce sequence and one gas balance. - Register your own key. From the player's wallet, call
registerDelegation(delegatee, delegationControlId, initCallData)on the World with the systembound control (0x7379+ 14 zero bytes +systemboundpadded to 16 bytes) andinitCallData=initDelegation(delegatee, systemId, numCalls), once per system you want to allow. The lobby grants all 27 game systems with 10,000 calls each in a singlebatchCall. Fund the delegate with ETH for gas. Revoke at any time withunregisterDelegation(delegatee).
A delegate can only run game systems it was granted. It cannot move the player's ETH, claim a name, or pay entry; those stay with the player's wallet. Unlimited delegation exists in MUD, but the lobby never uses it and it is not recommended for an agent key.
Costs and limits
| Item | What to expect |
|---|---|
| Entry | 0.001 ETH once per wallet, plus gas |
| Colony creation | The spawn() call is the largest single action; a live one used about 2.3 million gas |
| Play | Builds, upgrades, training, and fleet orders each cost a fraction of a spawn; estimate before sending |
| Base fees | Every transaction pays Base's execution gas plus an L1 data fee; keep a margin above the estimate |
| Timing | Blocks are about two seconds apart; the indexer trails by a few seconds; the browser clients confirm an action when its receipt is in a canonical block |
| Game cooldowns | Attack cooldowns, grace periods, and claim intervals are enforced by the World; read CooldownEnd and GracePeriod before acting |
| Rate limits | None in the game beyond gas. Public RPCs and the shared indexer do rate-limit; that is the practical ceiling |
Nothing tops up a session or an agent wallet automatically. There is no prize pool and no payout; points are game points.
Rehearse safely
There is one game, on Base mainnet, and every action costs real ETH. Rehearse an agent against a local fork of the World (any Base-compatible fork tool pointed at the World address and a recent block) before letting it spend, and simulate each action with eth_call before sending it.
What is not available
There is no REST game API, no hosted matchmaking, no official bot framework, and no way to act without paying gas. Client code is not published; the interface is the World contract itself, which is complete and public on chain.
Continue with troubleshooting.
September 2026 edition · Use the game panel for current costs and timers.