axmc docs

Go live: the answer on your own pages

Show a question's live state on your market's page, and let your users answer and challenge it there, with an iframe, a widget, React components or the SDK.

None of this is needed to resolve your markets. Posting questions by API and reading the answers back is the whole integration for most markets, and Quickstart: resolve your markets by API walks it end to end: access, an API key, a Squads vault, a first question. Start there; come back here to put the resolution in front of your users.

Every path below reads accounts straight from a Solana RPC endpoint, with no server of ours in the way, and signs with the wallet your users already connect. For the full API reference, the registration call and the CPI path, read Integrating a market with axmc.

Before you start

Follow steps 1 to 3 of the Quickstart: an approved account, an API key on Venue → API, and a venue funded from a Squads vault on Venue → Vault.

To sign each question yourself instead of posting by API, add a key of your own to the venue (Venue → Vault → A key of your own), list the market with the SDK's registerVenueMarket if your platform has not posted for it yet, and open questions from Docket or with the SDK, as Integrating a market with axmc shows. If your market is a Solana program that will register itself by CPI, say so when you apply. It needs a separate approval for the program's address.

Pick a path

Path Work Your users can Your outcome names Time
iframe One tag See the live status; act in the app Not shown Minutes
Widget A script tag and a wallet handover Answer and challenge on your page Not shown: Outcome 0, Outcome 1 An afternoon
React components A provider, a panel or hooks The same, in your design system Yes, optionLabels An afternoon, once you have the build
SDK Your own UI over a typed client Everything, and you open questions from code Yes, describeOutcome A day or two, once you have the build

For a binary market where "Yes" and "No" must show on the buttons, skip the widget and use the React components or the SDK.

A market that is itself a Solana program can settle on chain with three CPI calls. A market on an EVM chain opens questions with the SDK and reads each answer on its own chain: see The EVM spoke.

What every path needs

The addresses. Devnet today. Mainnet keeps the same program ids and changes the collateral mint.

What Address
truth_oracle JCxaLqGvNHwYUkiGwHbFheFUALEe3cEiicgFBjwA1vqs
truth_stake A6GfEszTdJzTgCL1tkdViGikEDTJnWQw4F6r1V23q9Tf
truth_court 78QFAr2CjT6veNrEhnNn6tWD2wCMi6GwkooRS1k6jnZo
Collateral mint, devnet 5jC7N4z1iDwTJCuyAWJBmvEDdP7hbJFTgzSVCA7A8DCo, 6 decimals

The SDK reads the program ids from its IDLs, so you configure none of them. There is no protocol token: bonds, rewards and premiums are all in the collateral mint.

An RPC endpoint. Use a dedicated one (Helius, Triton, QuickNode; free tiers are enough). The public api.devnet.solana.com rate-limits and drops transactions under real traffic. Keep the URL out of your repo: it carries your API key.

Collateral for your users. Whoever answers posts the proposer bond and whoever challenges posts the disputer bond, both in the collateral mint. The request account carries the exact amounts. On devnet, anyone gets test collateral from the Faucet in the app's menu.

A request address. Every path renders one resolution request, which is one question: the request that POST /questions returned, or one opened from Docket → Ask a question or the SDK.

The packages. @axmc/sdk and @axmc/react are not on npm yet, and their source is not public yet. The iframe and the widget need neither: the app serves the widget's script. For the SDK or the React components, write to the operator from the contact page and you get a tarball to install (pnpm add ./axmc-sdk-0.1.0.tgz).

Path A: the iframe

No JavaScript. The iframe shows the live resolution, read-only, and its actions open the app.

<iframe
  src="https://app.axmc.xyz/embed?request=<request pubkey>&rpc=<your https rpc endpoint>"
  style="border: 0; width: 100%; height: 320px"
  title="axmc resolution"
></iframe>

rpc must be an https URL; anything else falls back to the app's own endpoint. When it points somewhere other than the app's endpoint, the embed names that host on the page. An iframe cannot reach your page's wallet, so your users cannot answer in place. Use the widget for that.

Path B: the widget

<truth-resolution> is a custom element. It shows the status, the proposed answer, the reward and bond, and the countdown, and lets a connected user answer or challenge. It refreshes every 15 seconds.

Load it and place it

<script src="https://app.axmc.xyz/truth-resolution.js"></script>

<truth-resolution
  id="resolution"
  request="<request pubkey>"
  rpc="https://<your rpc endpoint>"
></truth-resolution>

The app serves the script, so it always matches the app's protocol version. To self-host, build the widget and serve truth-resolution.js yourself: it is one self-contained file. Both attributes are live: change request and the panel reloads. rpc defaults to https://api.devnet.solana.com.

Hand over the wallet

The panel is read-only until it has a wallet. Any adapter with publicKey and signTransaction works (Phantom, Solflare, Backpack). It is a property, not an attribute, and you call load() to redraw:

const panel = document.getElementById('resolution')

onWalletConnect((adapter) => { panel.wallet = adapter; panel.load() })   // your existing connect flow
onWalletDisconnect(() => { panel.wallet = null; panel.load() })

panel.appUrl is where the panel links for anything that belongs in the app, such as following a dispute (default https://app.axmc.xyz).

In React, assign the wallet in an effect:

import { useEffect, useRef } from 'react'
import { useAnchorWallet } from '@solana/wallet-adapter-react'

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'truth-resolution': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { request?: string; rpc?: string }
    }
  }
}

export function Resolution({ request, rpc }: { request: string; rpc: string }) {
  const ref = useRef<HTMLElement>(null)
  const wallet = useAnchorWallet() // undefined until connected
  useEffect(() => {
    const panel = ref.current as (HTMLElement & { wallet?: unknown; load?: () => void }) | null
    if (!panel) return
    panel.wallet = wallet ?? null
    panel.load?.()
  }, [wallet])
  return <truth-resolution ref={ref} request={request} rpc={rpc} />
}

Theme it

The panel renders in a shadow root, so your CSS does not reach inside. Set these variables on the element instead (defaults shown):

truth-resolution {
  --truth-font: inherit;
  --truth-ink: #182023;        /* text */
  --truth-paper: #ffffff;      /* background */
  --truth-rule: #d8ded9;       /* borders */
  --truth-muted: #5a6a66;      /* secondary text */
  --truth-radius: 6px;
  --truth-accent: #0e7c86;     /* primary button, open status */
  --truth-accent-soft: #e3eeef;
  --truth-warn: #b85f16;       /* disputed, errors */
  --truth-warn-soft: #f6e7d8;
  --truth-ok: #2e7d4f;         /* resolved, closed */
  --truth-ok-soft: #e2efe7;
}

For dark mode, set the same variables under your dark selector.

Settle your book

panel.addEventListener('truth:update', (e) => {
  const { status, outcome } = e.detail   // status: 'requested' | 'proposed' | 'disputed' | 'resolved' | 'closed'
  if (status === 'resolved' || status === 'closed') refreshMyMarket()
})
panel.addEventListener('truth:error', (e) => console.warn('axmc', e.detail))

truth:update fires on every successful refresh. Treat it as a doorbell, not as the answer: read the request account yourself (Path D) before you pay anyone.

The widget does the two actions that belong on a market page: answering and challenging. Panels, appeals and the record stay in the app, and the widget links there. Finalizing an unchallenged answer and settling are permissionless, and the crank that runs with the deployment does both.

Path C: the React components

@axmc/react is the same flow with no stylesheet: a provider over the connection and wallet you already have, a panel that takes your class names, and headless hooks.

import { PublicKey } from '@solana/web3.js'
import { TruthProvider, ResolutionPanel } from '@axmc/react'

<TruthProvider connection={connection} wallet={wallet}>
  <ResolutionPanel
    request={new PublicKey(requestId)}
    optionLabels={['Yes', 'No']}
    classNames={{ root: 'card', action: 'btn btn-primary', error: 'text-danger' }}
    onSent={(signature) => console.log('sent', signature)}
  />
</TruthProvider>

wallet is any adapter with publicKey and signTransaction. Leave it out and the tree still reads; the panel says a wallet is needed instead of showing dead buttons. classNames takes root, status, outcome, meta, choices, choice, choiceSelected, input, action, note and error. pollMs sets the refresh (15 seconds by default, 0 to drive it yourself).

If the panel's structure is wrong for you, use the hooks:

import { useResolution, usePropose, useDispute, Outcome } from '@axmc/react'

const { view, loading, error, canPropose, canDispute, refresh } = useResolution(request)
const { propose, busy } = usePropose(request)   // propose(Outcome.index(1))
const { dispute } = useDispute(request)

view is the same read model the widget renders, from the same SDK function. useAttest and useCountdown are there too. The panel is about a hundred lines over these hooks, so copying it into your tree and editing it is a fine choice.

Path D: the SDK

The TypeScript client the other paths are built on. You draw the UI and decide when your book settles.

Connect

import { Connection } from '@solana/web3.js'
import { TruthClient } from '@axmc/sdk'

const connection = new Connection(process.env.RPC_URL!, 'confirmed')
const client = TruthClient.connect(connection, walletAdapter)  // any Anchor-shaped wallet
const reader = TruthClient.readOnly(connection)                // server side; never signs

Every transaction the client sends adds a priority fee of client.priorityMicroLamports (20,000 by default). Set it to 0 to price fees yourself.

Read a request

import { describeOutcome, statusName } from '@axmc/sdk'

const r = await reader.fetchRequest(request)
statusName(r.status)                                    // 'requested' | 'proposed' | 'disputed' | 'resolved' | 'closed'
r.outcome && describeOutcome(r.outcome, ['Yes', 'No'])  // your own outcome names
r.proposal?.windowEndsAt                                // end of the challenge window, unix seconds
r.terms.proposerBond, r.terms.disputerBond, r.terms.windowSecs
r.earliestProposalAt                                    // answers are refused before this

Settle your market when status is resolved or closed and outcome is set:

outcome Do
Index(i) or Scalar(v) Settle on that answer
Unresolvable Refund at par

You never see TooEarly as a final outcome: it reopens the request and the status goes back to requested. For a countdown, re-read proposal.windowEndsAt after each refresh. Every co-signature on an answer shortens what is left of the window.

Answer and challenge

import { Outcome } from '@axmc/sdk'

await client.collateralBalance(user)                                     // can they cover the bond?
await client.propose(request, Outcome.index(1), { allowUnstaked: true }) // a person answering; locks the proposer bond
await client.dispute(request)                                            // inside the window; locks the disputer bond

Pass { allowUnstaked: true } when a person picks the answer and signs it. Without it, the SDK refuses with ProposerNotStakedError unless the proposer is a registered verifier with at least the minimum stake. That rule is for bots and agents answering through the SDK. client.proposerRefusal(owner) tells you why before you try.

Every method has an …Ix twin (proposeIx, disputeIx and so on) that returns the bare instruction, for hosts that build their own transactions or pay fees through a relayer. Finalizing and settling are permissionless and the crank does them; the calls are in Integrating a market with axmc if you want the timing in your hands.

Open questions and get notified

Opening a question from code is client.requestResolution(market, args). The rules manifest, the question text and how the oracle prices them are in Integrating a market with axmc, with webhooks so you never have to poll.

Go-live checklist

The Quickstart's checklist covers posting and settling. For the paths on this page, add:

  • Your users can cover the bonds: a user without collateral sees why the answer button is disabled, not a failed transaction.
  • An answer before earliestProposalAt is refused in your UI, not by the chain.
  • A challenge inside the window shows the disputed state in your UI, and you see how it clears.
  • Your payout reads the request account or GET /requests/<request>, not the widget's truth:update event.
  • If you sign questions yourself: the venue's key of your own is one only your backend holds, and markets you link have the resolver policy and tags you want. Both are permanent.
  • If you want an AI judge, it is committed before the market's first question.
  • For mainnet, switch the RPC endpoint and the collateral mint. The program ids stay the same.

Source: docs/GO-LIVE.md in the axmc repo. Edit the file; the next deploy rebuilds this page.