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
- Sign in at pivarena.com, then open Account → Bots.
- Create bot and give it a name. It is a separate account, owned by you, with its own balance.
- Create key. Copy it right away: it is shown once and only a hash is stored.
- 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.
| Scope | Allows |
|---|---|
read | Balance, deposit address, games, open games, match state, fairness, history, WebSocket. |
play | Create, join, cancel, act, rematch, claim rakeback. |
withdraw | Off 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
- Bots are tagged BOT in the lobby, in matches, on profiles and in results.
- A bot can’t play its owner, and bots with the same owner can’t play each other.
- Players can turn off Play against bots. Bots can’t join their games, and bot games are hidden from them and closed to them.
/matches/openreports this asjoinable: falsewith a reason. - The bot arena (
"arena":"bots") is bots only. People can watch but not join. - Bots can’t chat and can’t log in on the website.
- Your account status covers your bots: if your account is frozen, self-excluded or cooling off, your bots can’t start new games either.
- The owner manages the bot on the Account page: move free PIV both ways between the owner’s balance and the bot (staked PIV can’t move; PIV not yet withdrawable stays that way), pause it or delete it. Bot owners are private: no endpoint shows who owns a bot.
- A paused bot’s keys get
403with"code":"bot_paused". Its open lobby games are cancelled and refunded and its WebSocket is closed (code 4403). It can still finish games already in progress:GET /matches/:id,/action,/canceland declining challenges keep working. Anything else waits until the owner resumes it. - A deleted bot’s keys stop working (401). Its balance goes back to the owner, and its name is free again after 30 days.
- Withdrawals: the daily withdrawal cap is shared between an owner and their bots.
- Stakes 0.1 to 1 PIV, at most 5 open games at a time. Minimum withdrawal 1 PIV, network fee 0.05 PIV.
4. Endpoints
| Path & body | Scope | What it does | |
|---|---|---|---|
| GET | /api/v1/me | read | The bot account, key scopes, balances and limits. |
| GET | /api/v1/balance | read | Satoshi strings: balance (spendable on stakes), withdrawable, locked (deposits still confirming), pendingDeposits, inPlay. |
| POST | /api/v1/deposit-address{"kind":"shield"|"transparent"} | read | The bot’s own deposit address. |
| GET | /api/v1/games | read | Every game id with its rake, options and action format. |
| GET | /api/v1/matches/open?arena=main|bots|all&game= | read | Open games. Each has joinable and, if not, the reason. |
| POST | /api/v1/matches{"game":"coinflip","stake":"1.5","options":{},"arena":"main"|"bots","clientSeed":"optional"} | play | Create a game. The stake is held at once. target makes a private challenge to one player. |
| POST | /api/v1/matches/:id/join{"clientSeed":"optional"} | play | Join an open game (same stake). |
| POST | /api/v1/challenges/:code/accept{"clientSeed":"optional"} | play | Accept a private challenge or rematch offer by its code. |
| POST | /api/v1/challenges/:code/decline | play | Decline a rematch offer. |
| POST | /api/v1/matches/:id/cancel | play | Cancel your own open game before anyone joins. The stake is refunded. |
| POST | /api/v1/matches/:id/action{"type":"drop","col":3} | play | Make a move. The body is the game’s action object (see /api/v1/games). |
| POST | /api/v1/matches/:id/rematch | play | Offer a rematch after a settled duel. |
| GET | /api/v1/matches/:id | read | The match as your seat sees it: players, mySeat, game view, result. |
| GET | /api/v1/matches/:id/fair | read | Server seed hash, server seed (after the match), every client seed and the result. |
| GET | /api/v1/history | read | The bot’s recent matches (rows) and its last 20 transfers with its owner (transfers). |
| GET | /api/v1/transfers | read | PIV 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"} | play | Claim accrued rakeback into the balance. |
| POST | /api/v1/withdraw{"amount":"10"} | withdraw | Withdraw to the payout address the owner set. No other address is possible. |
| WS | /ws/bot | read | Live 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
| id | Game | Rake | Options | Actions |
|---|---|---|---|---|
coinflip | Coin Flip | 2.5% | {"side":"heads"|"tails"} (the creator's side; the joiner gets the other) | none: settles the moment the second player joins |
dice | Dice Duel | 2.5% | {} | none: settles on join |
highcard | High Card | 2.5% | {} | none: settles on join |
rps | Rock Paper Scissors | 2.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_-]>"} |
chess | Chess | 3% | {"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"} |
poker | Hold'em sit & go | 5% | {"players":2|3|4|6} | {"type":"fold"} | {"type":"check"} | {"type":"call"} | {"type":"raise","to":<chips>} | {"type":"allin"} |
liarsdice | Liar's Dice | 2.5% | {} | {"type":"bid","qty":n,"face":1..6} | {"type":"call"} |
mines | Mines Duel | 2.5% | {"mineCount":3|5|7} | {"type":"reveal","cell":0..24} |
connect4 | Connect Four | 3% | {"turnSeconds":15|30|60} | {"type":"drop","col":0..6} | {"type":"resign"} |
battleship | Battleship | 3% | {"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"} |
pizza | Pizza Wheel | 5% | {"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:
| t | When |
|---|---|
hello | On connect: your username, plus the house and bots name lists. |
lobby | Open, live and recent games changed (open[] items carry arena and creator_bot). |
match | A subscribed match changed. view is the same object as GET /matches/:id. |
balance | Your balance changed. |
joined | Someone 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_offer | A rematch offer for you: accept with /challenges/:code/accept using code. |
rematch_declined | Your rematch offer was declined. |
err | Bad 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"}.
| Status | Meaning |
|---|---|
| 400 | Invalid input, not enough balance, or a move the game doesn’t allow. |
| 401 | Missing, malformed or revoked key. |
| 403 | Missing scope, or a rule blocks it (bot arena, opted-out player, own bot, account status). code: bot_paused when the owner paused the bot. |
| 404 | No such match or challenge. |
| 409 | Wrong state: already joined, already started, too many open games. |
| 429 | Rate limited. Wait Retry-After seconds. |
| 503 | Deposit 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 }));
});