Bots are first-class citizens on counsel. A bot is an ordinary XRPL client: it reads live markets, fetches an unsigned bet Payment, signs it with its own key, and submits it to the ledger. counsel never holds a key. Every bet carries the counsel SourceTag, so a bot’s on-chain volume counts toward leaderboard attribution the same way a manual bet does. Because counsel markets are parimutuel, there is no order book, bots add value by correcting mispricings, balancing pools, or arbitraging across venues. Each of these actions is a native tagged Payment, generating direct on-chain volume.
What a bot can do
- Value-betting, estimate the true probability of each outcome and stake when the market’s
implied_prob diverges from your model by a meaningful margin. This pushes the line toward the fair price, benefiting all participants.
- Pool-balancing, parimutuel lines drift continuously until
bet_cutoff. A bot can identify a lopsided pool near close and bet the underweighted side, capturing better odds while correcting the skew.
- Cross-venue arbitrage, take a counsel position and hedge it against the same event on another platform, locking in a spread when the implied probabilities diverge.
Install the SDK from the repo and configure the base URL:
import { Counsel } from "./counsel";
const counsel = new Counsel({ baseUrl: "https://api.counsel.markets" });
Set COUNSEL_URL to https://api.counsel.markets and reference it with process.env.COUNSEL_URL to keep the base URL out of your source code.
Read live markets
Fetch all public markets and inspect their current pool state and indicative odds:
const { markets } = await counsel.markets();
// Each market has: id, question, phase, status, outcomes[]
// Each outcome: index, label, implied_prob, payout_per_unit, pool_xrp
Filter to markets that are currently accepting bets before doing anything else:
const open = (markets as any[]).filter(
(m) => m.status === "open" && m.phase === "open"
);
Fetch a bet intent
Before committing, retrieve an unsigned transaction for the specific bet you want to place. This also returns the projected line impact of your stake:
const intent = await counsel.betIntent(marketId, walletAddress, outcomeIndex, amountXrp);
// intent.tx = unsigned XRPL Payment ready to sign
// intent.projected_implied_odds_after = { implied_prob, payout_per_unit }
betIntent maps to:
GET /api/v1/markets/:id/bet-intent
?account=WALLET_ADDRESS
&outcome=OUTCOME_INDEX
&amount=AMOUNT_XRP
The returned tx is a fully-formed XRPL Payment object. All that remains is to autofill sequence/fee fields, sign, and submit.
Sign and submit
placeBet handles the full lifecycle: it calls betIntent internally, signs with the provided seed using xrpl.js, submits, and waits for ledger validation. It returns the confirmed transaction hash.
const hash = await counsel.placeBet(process.env.BOT_SEED!, marketId, outcomeIndex, amountXrp);
console.log("tx:", hash);
Use testnet seeds only. Never commit seeds to source control. Load BOT_SEED exclusively from environment variables or a secrets manager.
Internally, placeBet connects to the XRPL WebSocket node specified in CounselOptions.wss (defaults to wss://s.altnet.rippletest.net:51233 for testnet), autofills the transaction, signs it with Wallet.fromSeed(seed), submits via submitAndWait, and asserts tesSUCCESS before returning. The WebSocket connection is disconnected after each call.
Full value-betting example
The following bot fetches all open markets, runs each outcome through a probability model, and bets when it finds sufficient edge. It also checks the projected post-stake odds before committing to avoid betting after the line has already moved past the fair price:
import { Counsel } from "./counsel";
const counsel = new Counsel({ baseUrl: process.env.COUNSEL_URL! });
const SEED = process.env.BOT_SEED!; // testnet only
// Your model: the probability you believe each outcome has.
function myModel(question: string): number[] {
// ...return [pYes, pNo]; here a naive 50/50
return [0.5, 0.5];
}
const { markets } = await counsel.markets();
for (const m of markets as any[]) {
if (m.status !== "open" || m.phase !== "open") continue;
const fair = myModel(m.question);
for (const o of m.outcomes) {
// Bet when the market underprices an outcome vs your model, with edge to spare.
if (fair[o.index] - o.implied_prob > 0.08) {
const intent = await counsel.betIntent(m.id, walletAddress, o.index, 5);
// Check the projected odds AFTER your own stake before committing.
if (intent.projected_implied_odds_after.implied_prob < fair[o.index]) {
await counsel.placeBet(SEED, m.id, o.index, 5);
}
}
}
}
The inner check on projected_implied_odds_after.implied_prob is important: in a small pool, your own 5 XRP stake can move the line enough to eliminate the edge you detected from the stale implied_prob in the market list.
Anti-slippage
Parimutuel odds are not fixed, every bet changes the pool, and therefore every subsequent payout. When you read implied_prob from GET /markets, that figure already reflects all bets submitted before your read. By the time you fetch the bet intent, further bets may have arrived.
Always use projected_implied_odds_after as your decision gate, not the market-list odds:
const intent = await counsel.betIntent(marketId, walletAddress, outcomeIndex, amountXrp);
if (intent.projected_implied_odds_after.implied_prob < myFairProbability) {
// Market still offers value after your stake is factored in, proceed.
await counsel.placeBet(SEED, marketId, outcomeIndex, amountXrp);
} else {
// Your stake alone moves the line past fair value, skip.
}
For large stakes relative to pool size, consider breaking a single bet into smaller sequential bets and re-checking the intent each time.
Rate limits
The public API requires no authentication. There are no enforced rate limits, but the API is a shared resource. Poll GET /markets at a reasonable cadence, 5-10 seconds is appropriate for most strategies. High-frequency polling does not provide an advantage on parimutuel markets: odds change when bets arrive, not on a tick schedule, and final payouts are determined at close regardless of when you placed your bet.
SDK method reference
| Method | Endpoint |
|---|
markets() | GET /api/v1/markets |
market(id) | GET /api/v1/markets/:id |
betIntent(id, account, outcome, amount) | GET /api/v1/markets/:id/bet-intent |
positions(address) | GET /api/v1/accounts/:address/positions |
leaderboard() | GET /api/v1/leaderboard |