PIVArenaC:\DOCS\API>_

Bot API

Write a program that plays PIVArena. Bots use the same games, stakes, rake, rakeback, limits and fairness as people. Every bot is shown with a BOT tag.

1. Start

  1. Sign in at pivarena.com, then open Account → Bots.
  2. Create bot and give it a name. It is a separate account, owned by you, with its own balance.
  3. Create key. Copy it right away: it is shown once and only a hash is stored.
  4. Fund the bot by sending PIV to its deposit address (Account page, or POST /api/v1/deposit-address).

Base URL: https://pivarena.com/api/v1. Bodies are JSON. Amounts you send are PIV decimal strings ("1.5"). Amounts you get back are satoshi strings (1 PIV = 100000000).

2. Keys & scopes

Send the key on every request: Authorization: Bearer pva_.... Cookies are ignored on /api/v1.

ScopeAllows
readBalance, deposit address, games, open games, match state, fairness, history, WebSocket.
playCreate, join, cancel, act, rematch, claim rakeback.
withdrawOff by default. Needs your 2FA to create. Pays out only to the bot’s payout address, which you set on the Account page with 2FA.

Limits: 120 requests per minute per key, 60 of them writes. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After (seconds). Up to 5 bots per player and 5 active keys per bot. Revoke a key on the Account page and it stops working at once.

3. Bot rules

4. Endpoints

Path & bodyScopeWhat it does
GET/api/v1/mereadThe bot account, key scopes, balances and limits.
GET/api/v1/balancereadSatoshi strings: balance (spendable on stakes), withdrawable, locked (deposits still confirming), pendingDeposits, inPlay.
POST/api/v1/deposit-address
{"kind":"shield"|"transparent"}
readThe bot’s own deposit address.
GET/api/v1/gamesreadEvery game id with its rake, options and action format.
GET/api/v1/matches/open?arena=main|bots|all&game=readOpen games. Each has joinable and, if not, the reason.
POST/api/v1/matches
{"game":"coinflip","stake":"1.5","options":{},"arena":"main"|"bots","clientSeed":"optional"}
playCreate a game. The stake is held at once. target makes a private challenge to one player.
POST/api/v1/matches/:id/join
{"clientSeed":"optional"}
playJoin an open game (same stake).
POST/api/v1/challenges/:code/accept
{"clientSeed":"optional"}
playAccept a private challenge or rematch offer by its code.
POST/api/v1/challenges/:code/declineplayDecline a rematch offer.
POST/api/v1/matches/:id/cancelplayCancel your own open game before anyone joins. The stake is refunded.
POST/api/v1/matches/:id/action
{"type":"drop","col":3}
playMake a move. The body is the game’s action object (see /api/v1/games).
POST/api/v1/matches/:id/rematchplayOffer a rematch after a settled duel.
GET/api/v1/matches/:idreadThe match as your seat sees it: players, mySeat, game view, result.
GET/api/v1/matches/:id/fairreadServer seed hash, server seed (after the match), every client seed and the result.
GET/api/v1/historyreadThe bot’s recent matches (rows) and its last 20 transfers with its owner (transfers).
GET/api/v1/transfersreadPIV moved between the bot and its owner: direction (in = from the owner, out = to the owner), amount, lockedAmount (part not yet withdrawable), via (owner, delete, sweep), createdAt. The owner is never named.
POST/api/v1/rakeback/claim
{"nonce":"unique-per-claim"}
playClaim accrued rakeback into the balance.
POST/api/v1/withdraw
{"amount":"10"}
withdrawWithdraw to the payout address the owner set. No other address is possible.
WS/ws/botreadLive pushes. Send the same Authorization header on the upgrade.

Match fields you’ll use most: status (open, active, settled, cancelled), mySeat (-1 if you’re watching), players[] (each with bot and house flags), view (game-specific and only what your seat may see; turn games include toAct and a deadline: move before it or the clock decides), result.

5. Games

idGameRakeOptionsActions
coinflipCoin Flip2.5%{"side":"heads"|"tails"} (the creator's side; the joiner gets the other)none: settles the moment the second player joins
diceDice Duel2.5%{}none: settles on join
highcardHigh Card2.5%{}none: settles on join
rpsRock Paper Scissors2.5%{"bestOf":1|3|5}{"type":"commit","hash":"<sha256 hex of choice:salt>"} then {"type":"reveal","choice":"rock"|"paper"|"scissors","salt":"<16-64 chars [A-Za-z0-9_-]>"}
chessChess3%{"minutes":1|3|5|10|15,"increment":0|2|3|5|10}{"type":"move","from":"e2","to":"e4","promotion"?:"q"|"r"|"b"|"n"} | {"type":"resign"} | {"type":"offer_draw"} | {"type":"accept_draw"} | {"type":"decline_draw"}
pokerHold'em sit & go5%{"players":2|3|4|6}{"type":"fold"} | {"type":"check"} | {"type":"call"} | {"type":"raise","to":<chips>} | {"type":"allin"}
liarsdiceLiar's Dice2.5%{}{"type":"bid","qty":n,"face":1..6} | {"type":"call"}
minesMines Duel2.5%{"mineCount":3|5|7}{"type":"reveal","cell":0..24}
connect4Connect Four3%{"turnSeconds":15|30|60}{"type":"drop","col":0..6} | {"type":"resign"}
battleshipBattleship3%{"turnSeconds":15|30|60}{"type":"random"} or {"type":"place","ships":[{"id":"C"|"B"|"R"|"S"|"D","r":row,"c":col,"v":vertical}]}, then {"type":"ready"}, then {"type":"fire","cell":0..99} | {"type":"resign"}
pizzaPizza Wheel5%{"slices":4|8,"players":2|4} (equal stakes; every seat gets slices/players slices){"type":"pick","slice":0..slices-1} on your turn (view.toAct === mySeat; snake order in view.schedule, 20 s per pick, 30 s for the first). A missed pick gets a random free slice; leftovers that can only go to one seat are assigned automatically. view.winningSlice stays null until every slice is owned

Rock paper scissors uses commit–reveal: send sha256(choice + ":" + salt) as hex, then reveal the choice and salt.

6. WebSocket

Connect to wss://pivarena.com/ws/bot with the same Authorization header. Up to 5 sockets per key. Send JSON messages:

{"t":"sub","ch":"lobby"}          // open games, live games, recent results
{"t":"sub","ch":"match","id":123}  // pushes for one match (up to 20)
{"t":"unsub","ch":"match","id":123}
{"t":"ping"}

You receive:

tWhen
helloOn connect: your username, plus the house and bots name lists.
lobbyOpen, live and recent games changed (open[] items carry arena and creator_bot).
matchA subscribed match changed. view is the same object as GET /matches/:id.
balanceYour balance changed.
joinedSomeone joined a game you sit at (matchId, by, full, seats, max, host: true if it’s your game). On 4-seat tables every seated player gets one per arrival.
rematch_offerA rematch offer for you: accept with /challenges/:code/accept using code.
rematch_declinedYour rematch offer was declined.
errBad or rate-limited message.

7. Errors

Errors are JSON: {"error":"human readable message"}, some with a machine-readable code, e.g. {"error":"bot paused: the owner paused this bot","code":"bot_paused"}.

StatusMeaning
400Invalid input, not enough balance, or a move the game doesn’t allow.
401Missing, malformed or revoked key.
403Missing scope, or a rule blocks it (bot arena, opted-out player, own bot, account status). code: bot_paused when the owner paused the bot.
404No such match or challenge.
409Wrong state: already joined, already started, too many open games.
429Rate limited. Wait Retry-After seconds.
503Deposit address pool refilling; retry in a minute.

8. Examples

curl

export KEY=pva_your_key_here
curl -s https://pivarena.com/api/v1/me -H "Authorization: Bearer $KEY"
curl -s "https://pivarena.com/api/v1/matches/open?arena=bots&game=coinflip" -H "Authorization: Bearer $KEY"
curl -s -X POST https://pivarena.com/api/v1/matches -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"game":"coinflip","stake":"1","options":{"side":"heads"},"arena":"bots"}'

JavaScript (Node 18+)

const KEY = process.env.PIVARENA_KEY;
const api = async (path, body) => {
  const r = await fetch('https://pivarena.com/api/v1' + path, {
    method: body ? 'POST' : 'GET',
    headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  });
  const j = await r.json();
  if (!r.ok) throw new Error(r.status + ' ' + j.error);
  return j;
};

const { matches } = await api('/matches/open?arena=bots&game=connect4');
const game = matches.find((m) => m.joinable);
if (game) {
  await api('/matches/' + game.id + '/join', {});
  let v = await api('/matches/' + game.id);
  while (v.status === 'active') {
    if (v.view && v.view.toAct === v.mySeat) await api('/matches/' + game.id + '/action', { type: 'drop', col: 3 });
    await new Promise((r) => setTimeout(r, 1000));
    v = await api('/matches/' + game.id);
  }
  console.log(v.status, v.result);
}

Python

import os, requests
S = requests.Session()
S.headers["Authorization"] = "Bearer " + os.environ["PIVARENA_KEY"]
BASE = "https://pivarena.com/api/v1"

me = S.get(BASE + "/me").json()
print(me["username"], int(me["balances"]["balance"]) / 1e8, "PIV")

r = S.post(BASE + "/matches", json={"game": "dice", "stake": "0.5", "arena": "bots"})
r.raise_for_status()
print("created match", r.json()["id"])

fair = S.get(BASE + "/matches/%d/fair" % r.json()["id"]).json()
print(fair["serverSeedHash"])

WebSocket (Node, ws package)

import WebSocket from 'ws';
const ws = new WebSocket('wss://pivarena.com/ws/bot', { headers: { Authorization: 'Bearer ' + process.env.PIVARENA_KEY } });
ws.on('open', () => ws.send(JSON.stringify({ t: 'sub', ch: 'lobby' })));
ws.on('message', (raw) => {
  const m = JSON.parse(raw);
  if (m.t === 'lobby') console.log(m.open.filter((g) => g.arena === 'bots').length, 'open bot-arena games');
  if (m.t === 'joined') ws.send(JSON.stringify({ t: 'sub', ch: 'match', id: m.matchId }));
});