DogecoinArcade
@ Create a new account

The bot RPC

docs/bot-rpc.md

DogecoinArcade serves a JSON-RPC endpoint for programs: airdrop bots, faucets, leaderboards, anything that reads token balances or moves tokens without a person clicking. It speaks in Omni Core's method names, so anyone who has scripted against Omni knows it already. The code is arcade/web/rpc.py; the tests are tests/test_rpc_web.py; a working bot is examples/airdrop.py.

Where and how

URLhttp://127.0.0.1:8420/rpc/test for testnet, http://127.0.0.1:8420/rpc/main for mainnet
AuthHTTP basic auth with the contents of ~/.dogecoinarcade/rpc.cookie (__cookie__:<secret>, mode 0600, rewritten at every start of arcade-web)
Body{"id": 1, "method": "omni_getbalance", "params": ["nAddress", 3]} — positional or named params, batches as a list
Answer{"result": ..., "error": null, "id": 1}; on failure error is {"code", "message"} in bitcoind's codes (src/rpc/protocol.h): -32601 unknown method, -32602 wrong arguments, -8 bad value, -5 bad address, -4 not this wallet's address
Shellarcade-rpc omni_listproperties, arcade-rpc -main omni_getinfo — reads the cookie for you; testnet unless -main. The installer puts it beside dogecoinarcade (~/.local/bin, or %LOCALAPPDATA%\DogecoinArcade\bin on Windows); dogecoinarcade-update adds it to an existing installation

Nothing listens anywhere but loopback, and the cookie is the whole key: a browser tab cannot present it, so this endpoint is out of reach of the cross-site posts the forms guard against with their CSRF token, and a script never has a password to keep. If the server restarts, the cookie changes — read it when you start, as you would bitcoind's.

Python, in full:

```python import base64, json, urllib.request from pathlib import Path

cookie = (Path.home() / ".dogecoinarcade" / "rpc.cookie").read_text().strip() auth = base64.b64encode(cookie.encode()).decode()

def rpc(method, *params): body = json.dumps({"id": 1, "method": method, "params": list(params)}).encode() req = urllib.request.Request("http://127.0.0.1:8420/rpc/test", data=body, headers={"Content-Type": "application/json", "Authorization": f"Basic {auth}"}) answer = json.loads(urllib.request.urlopen(req).read()) if answer["error"]: raise RuntimeError(answer["error"]["message"]) return answer["result"]

for holder in rpc("omni_getallbalancesforid", 3): print(holder["address"], holder["balance"]) ```

Methods

Reading (no wallet needed):

MethodReturns
omni_getinfonetwork, mainnet, activationblock, block (indexed), nodeblock, behind, current, stopped
omni_listpropertiesevery token: propertyid, name, category, subcategory, data, url, divisible
omni_getproperty propertyidthe above plus issuer, creationtxid, creationblock, fixedissuance, managedissuance, totaltokens, holders
omni_getbalance address propertyidbalance, reserved, frozen
omni_getallbalancesforid propertyidevery holder, largest first: address, balance, reserved, frozen
omni_getallbalancesforaddress addressevery token one address holds
omni_getwalletbalances / omni_getwalletaddressbalanceswhat this node's wallet holds, per token / per address
omni_gettransaction txidone indexed token transaction: sendingaddress, referenceaddress, block, blockhash, blocktime, positioninblock, confirmations, type_int, type, valid, invalidreason, propertyid, propertyname, divisible, amount
omni_listtransactions [address="*" count=10 skip=0 startblock endblock]recent token transactions, oldest first
omni_listblocktransactions heighttxids of the token transactions in a block
help [method]usage lines

Amounts are strings the way Omni gives them: "1000.00000000" for a divisible token, "1000" for whole units. reserved and frozen are always zero — the arcade has no exchange or freezing — and are there so an Omni client reading them does not break.

Writing (needs the chain's node to have a wallet, and fromaddress in it):

MethodPrepares
omni_send from to propertyid amounta send
omni_sendissuancefixed from ecosystem type previousid category subcategory name url data amounta fixed-supply token (type 1 whole units, 2 divisible; ecosystem 1; previousid 0)
omni_sendissuancemanaged from ecosystem type previousid category subcategory name url dataa managed-supply token
omni_sendgrant from to propertyid amount [memo]a grant ("" as to grants to the issuer)
omni_sendrevoke from propertyid amount [memo]a revoke from the issuer's own balance
omni_sendchangeissuer from to propertyidhanding the token to a new issuer
omni_broadcast txidsends a prepared transaction; returns the txid

A program that is not trusted to spend on its own asks instead:

Method
da_requestsend from to amount [note]ask the owner to send coins; "" as from lets the wallet choose
da_requesttoken from to propertyid amount [note]ask the owner to send tokens
da_requestinscription to inscription [note]ask the owner to hand over an inscription, by number or txid
da_request idthe answer: pending, sent (with txid), denied, failed (with error) or expired
da_requests [count]the ones waiting, then the newest decided

Public machine talk, one node to every node (Broadcasts):

Method
da_publish topic body [from]prepares a public broadcast under a topic; send it with da_broadcast txid
da_broadcasts [topic after count [senders]]confirmed broadcasts from every node, oldest first; pass the last id as after

Sending is two calls

This is the one place the RPC is deliberately not Omni's. In Omni Core omni_send broadcasts and returns a txid. Here every omni_send* method builds, funds and signs the transaction and returns it unsent:

``json {"txid": "2ea169a4…", "hex": "…", "sendingaddress": "nSenderAddressExample1111111111111", "referenceaddress": "nRecipientAddressExample2222222222", "class": "C", "size": 257, "fee": "0.00257000", "outputscost": "0.01000000", "total": "0.01257000", "outputs": [ {"value": "0.00000000", "to": "token data (OP_RETURN)", "change": false, "recipient": false}, {"value": "0.01000000", "to": "nRecipientAddressExample2222222222", "change": false, "recipient": true}, {"value": "1.48683000", "to": "nSenderAddressExample1111111111111", "change": true, "recipient": false}], "broadcast": false} ``

(the shape of an omni_send of 1 Arcade Test on testnet; addresses shortened to placeholders)

omni_broadcast <txid> then sends exactly those bytes. A bot sees the fee before it pays it, a typo in an amount costs nothing, and the rule the whole interface follows — broadcast exactly what was shown — holds for scripts too. A bot that wants Omni's behaviour calls the two in a row. Prepared transactions are kept in the server's memory (the newest hundred) and forgotten on restart; omni_broadcast of a txid it does not hold is refused, never rebuilt.

Prepare and broadcast one at a time. Two prepares in a row from the same address pick the same coins, and the second broadcast would be rejected as a double spend. After a broadcast the change is spendable at once, so the next prepare chains on it — up to the node's limit of 25 unconfirmed transactions in a chain (validation.h: DEFAULT_ANCESTOR_LIMIT), after which omni_broadcast fails with too-long-mempool-chain until a block lands. examples/airdrop.py waits for the block and carries on.

Every refusal happens before anything is spent: a send of more than the address holds, an address on the other chain, an address this wallet cannot sign for, an amount with too many decimals, a grant by anyone but the issuer.

Asking is one call

omni_send + omni_broadcast is for a program that holds the wallet: the cookie is the same authority as the shell. A marketplace bot, a game server, or anything a stranger's code can drive should not have that, and does not need it. It files a request:

``json {"method": "da_requesttoken", "params": ["", "nRecipientAddressExample2222222222", 3, "5", "tournament prize"]} ``

``json {"id": "402d9a3126e70394", "network": "test", "kind": "token", "status": "pending", "from": null, "to": "nRecipientAddressExample2222222222", "amount": "5", "propertyid": 3, "propertyname": "Arcade Test", "origin": "rpc", "note": "tournament prize", "txid": null, "error": null, ...} ``

Nothing is built. The request appears on the wallet's Approvals page — locally or over remote access — where the owner sees the transaction it would be, fee and every output, and presses Approve and send or Refuse. The bot polls da_request <id> until status is no longer pending; sent carries the txid, and sent is not confirmed: the node's own gettransaction <txid> says how deep it is, as does GET /r/tx/<txid> on the wallet. A request nobody answers within an hour expires, and no more than twenty may wait at once.

What can be refused at once is refused at once, with -8: an address on the other chain, a token that does not exist, an inscription this wallet does not hold, an address it cannot sign for. Whether a balance covers it is checked when the owner looks, because that is when it matters.

The same queue answers an inscribed page over POST /r/send (see the inscription API reference), so a page and a bot asking for the same thing look the same to the person deciding.

Airdrops

The shape is omni_getallbalancesforid for the holders, then omni_send + omni_broadcast per holder. That is what examples/airdrop.py does:

`` python3 examples/airdrop.py --from nYourAddress --holders-of 3 --token 3 --amount 5 python3 examples/airdrop.py --from nYourAddress --holders-of 3 --token 3 --amount 5 --send ``

Dry run without --send; testnet without --main. It records each txid in a done-file so a stopped run resumes without paying anyone twice. Omni's send-to-owners (type 3), which does this in one transaction, is not implemented in the engine and a transaction of that type would stop the index; do not construct one.

Mainnet

/rpc/main answers the same methods. Whether a send there works depends on the mainnet node having a wallet (by default it runs disablewallet=1) — and on the operator: a bot with the cookie can spend from that wallet, which is the same authority as the shell that runs pepecoin-cli. Test on /rpc/test first.

← all docs