The EVM spoke
If you run a prediction market on Base, BNB Chain or Arbitrum, this page shows how it gets axmc answers. You ask on Solana, and the answer arrives on your chain as a signed receipt your contract settles on.
What you get
For every question you open, you get one receipt when the answer is final. It carries the outcome (a winning position, a number, or Unresolvable), a cause code that says why, whether the answer was disputed, overturned or corrected, and when it became final. The byte layout is in The receipt on the wire.
Only the answer crosses. Proposing, disputes, panels, appeals and payment all stay on Solana.
How an answer travels
- Your market closes on your chain.
- Your service notices and opens the question on Solana, signed by your Solana key. It pays the reward and premium in USDC.
- axmc resolves it: a curator answers, and a panel rules if someone challenges.
- Once the answer is final, anyone publishes the receipt as a Wormhole message from the axmc emitter program (
emit_receipt, oremitReceiptin the SDK). - Wormhole's guardians sign the message. The signed message is a VAA.
- Anyone delivers the VAA to the spoke contract on your chain with
receiveReceipt(bytes encodedVaa). - The spoke verifies the signatures, checks the message came from the axmc emitter, decodes the receipt and stores it.
- Your contract reads the answer from the spoke and settles your users.
Delivery is permissionless on purpose. Your service delivers its own receipts, and if it is down, anyone holding the VAA can.
What you run
| Piece | Where | What it does |
|---|---|---|
| A Solana key | Solana | Owns your market on axmc and pays for each question |
| Your market contract | Your chain | Reads answers from the spoke through IAxmcReceipt |
The spoke, AxmcReceiptOracle |
Your chain | Verifies, stores and serves receipts |
| A venue service | Off chain | Watches your contract, asks on Solana, carries the receipt back |
Your Solana key and your EVM contract never need to be linked on chain. Your contract never opens a question, so it never spends on Solana. Your service does both with the Solana key.
Keep the service running. If it stops, questions stop being asked and nothing else notices.
No spoke is deployed on a public network yet (see Status). On a local chain or a fork, the deploy script evm/script/Deploy.s.sol deploys a spoke and the reference market.
Read the answer in your contract
- When your service opens the question on Solana, store the request address on your market as a
bytes32(the key's raw 32 bytes). - Check that the receipt names your Solana market. A receipt is yours only if its
marketfield is your market. - Call
outcomeOf(request)and settle on the outcome kind, index or scalar. - Store or pass through the cause code. Never revert on a code you do not recognise: the table can grow.
This is the pattern the reference market, ExampleMarket, uses in settle:
import {IAxmcReceipt} from "./IAxmcReceipt.sol";
bytes32 answered = oracle.receiptOf(request).market;
if (answered != mySolanaMarket) revert WrongMarket(mySolanaMarket, answered);
(IAxmcReceipt.OutcomeKind kind, uint8 index, int128 scalar, uint8 causeCode, bool corrected, int64 finalizedAt) =
oracle.outcomeOf(request);
if (kind == IAxmcReceipt.OutcomeKind.Index && index < outcomeCount) {
// pay the winners of `index`
} else {
// void and refund: Unresolvable, a scalar, or an index out of range
}
The spoke's read functions:
| Function | Returns | Reverts |
|---|---|---|
hasReceipt(bytes32 request) |
Whether a receipt is stored, held or not | Never |
receiptOf(bytes32 request) |
The full decoded Receipt |
NoReceipt |
outcomeOf(bytes32 request) |
kind, index, scalar, causeCode, corrected, finalizedAt |
NoReceipt, Held |
isHeld(bytes32 request) |
Whether the receipt is held above the value ceiling | Never |
payloadHashOf(bytes32 request) |
keccak256 of the stored receipt bytes, or zero |
Never |
supersededOf(bytes32 request) |
The receipt a correction replaced, and whether there was one | Never |
Two things can change what outcomeOf tells you.
Corrections. A final answer can be corrected on Solana within the correction window, 7 days today (Specification). The corrected receipt replaces the stored one and carries corrected = true and cause code 8. If your market settles real value, decide whether to wait out the window first. ExampleMarket takes a settleDelay in seconds for this and refuses to settle earlier with TooSoon.
The value ceiling. The spoke can ask your contract how much it would settle on a receipt for your market. Implement IAxmcConsumer:
function exposure(bytes32 market) external view returns (uint256);
When the spoke's owner has bound your contract with setConsumer(market, consumer), the spoke calls exposure on each new or corrected receipt. If the answer is above valueCeiling, in the chain's own coin (wei of ETH on Base and Arbitrum, of BNB on BNB Chain), the receipt is held. While it is held, outcomeOf reverts with Held until the owner calls release(request). A revert or a malformed answer from exposure also holds the receipt.
The reference service and scripts
services/spoke holds a working venue service for ExampleMarket. Adapt it to your own contract's events.
| Command | What it does |
|---|---|
pnpm spoke:venue |
Runs the venue service: one pass every SPOKE_INTERVAL_SECS (default 30) |
pnpm spoke:venue --once |
Runs one pass and exits 0 when nothing is pending |
pnpm spoke:deliver --vaa <hex|base64|@file> --spoke <address> |
Delivers one VAA to a spoke. Needs EVM_PRIVATE_KEY to pay gas |
pnpm spoke:e2e |
The full loop on a local validator and a local EVM chain: a market closes, the question is asked, answers cross and settle |
pnpm spoke:devnet --chain base|bnb|arbitrum |
Publishes a devnet receipt, fetches the VAA the real testnet guardian signed, and delivers it to a spoke on a fork of that testnet. Needs the emitter on devnet; --vaa <hex> delivers a VAA you already have |
pnpm spoke:testnets |
Shows the testnet deployer's balances, costs and faucets. With --deploy it deploys a spoke and ExampleMarket on each funded testnet |
On each pass the venue service reads MarketClosed events, opens each new question on Solana and binds it with expectRequest. For each final answer it publishes the receipt, fetches the signed VAA from Wormholescan, delivers it and settles the market. It then checks your balances and writes a heartbeat line. Every signed step is saved to a state file first, so a restart never signs twice.
| Variable | Meaning |
|---|---|
RPC_URL |
Solana RPC. Default http://127.0.0.1:8899 |
EVM_RPC_URL |
Your chain's RPC. Default http://127.0.0.1:8545 |
SPOKE_VENUE_WALLET |
Keypair file of the Solana key that owns your market and pays |
SPOKE_EVM_KEY |
Required. Private key of your market's resolver, which also pays gas |
SPOKE_MARKET |
Required. Your market contract's address |
SPOKE_ORACLE |
Required. The spoke's address |
SPOKE_REWARD |
Proposer reward per question, in whole collateral units (USDC). Default 10 |
SPOKE_OPEN_INTEREST |
Open interest reported per question, in whole collateral units. Default 1000 |
SPOKE_MIN_QUESTIONS |
Warn below this many questions' worth of balance. Default 5 |
ALERT_WEBHOOK |
Optional. Receives one JSON line per warning |
WORMHOLESCAN_URL |
Where signed VAAs are fetched. Default the testnet API |
Supported chains
The spoke verifies against the Wormhole core bridge on its chain. The deploy script picks the core by chain id, checks the core answers the expected Wormhole id, and refuses any chain not in this table.
| Chain | EVM id | Wormhole id | Core bridge | Guardian set |
|---|---|---|---|---|
| Base | 8453 | 30 | 0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6 |
7, 19 keys |
| Base Sepolia | 84532 | 10004 | 0x79A1027a6A159502049F10906D333EC57E95F083 |
0, one key |
| BNB Chain | 56 | 4 | 0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B |
7, 19 keys |
| BNB Chain testnet | 97 | 4 | 0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D |
0, one key |
| Arbitrum One | 42161 | 23 | 0xa5f208e072434bC67592E4C49C1B991BA79BCA46 |
7, 19 keys |
| Arbitrum Sepolia | 421614 | 10003 | 0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35 |
0, one key |
Every core address was checked against the live contract on 13 October 2026.
Paying
You pay in USDC on Solana. The reward and premium move when the question is opened, inside request_resolution. There is no credit or deferred payment: if the USDC is not there, the request fails.
- For a market a wallet registered, the USDC comes from a token account your Solana key owns.
- For a venue market, it comes from your venue's balance. See Go live.
The account that pays rent in SOL (payer) is a separate signer from the one that speaks for the market. Someone else can cover rent while you keep control of your USDC and your market.
To top up, any route to Solana USDC works, for example Circle's CCTP from your chain. Refill on a schedule, not per question. The reference service warns when your balance covers fewer than SPOKE_MIN_QUESTIONS questions.
Trust
The one thing you trust is the attestation. Whoever signs the VAA decides the answer your spoke stores: today that is Wormhole's guardians, 13 of 19 on mainnet and a single key on the testnets. Nothing else is trusted. The emitter only republishes what Solana recorded, and anyone can rebuild the receipt from the request account and compare it byte for byte. A deliverer only moves bytes, so the worst it can do is not deliver. The spoke checks the emitter before it reads the payload and never applies the same VAA twice. The value ceiling caps what a bad attestation could settle. Bonded axmc attesters are planned to replace the guardians; they are not built.
Status
- Built and tested: the emitter program, the spoke contract, the reference market and the venue service.
- Tested end to end on a local validator and chain. A VAA signed by the real testnet guardian was delivered to spokes on forks of Base Sepolia, BNB Chain testnet and Arbitrum Sepolia, and each chain's real Wormhole core verified it.
- Not deployed on any public network. The emitter program is not on Solana devnet yet, and no spoke runs on a public testnet or mainnet.
- Not built: bonded attesters.
Source: docs/EVM_SPOKE.md in the axmc repo. Edit the file; the next deploy rebuilds this page.