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.
| URL | http://127.0.0.1:8420/rpc/test for testnet, http://127.0.0.1:8420/rpc/main for mainnet |
| Auth | HTTP 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 |
| Shell | arcade-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); an older installation gets it at its next dogecoinarcade-update |
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:
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"])
Reading (no wallet needed):
| Method | Returns |
|---|---|
omni_getinfo |
network, mainnet, activationblock, block (indexed), nodeblock, behind, current, stopped |
omni_listproperties |
every token: propertyid, name, category, subcategory, data, url, divisible |
omni_getproperty propertyid |
the above plus issuer, creationtxid, creationblock, fixedissuance, managedissuance, totaltokens, holders |
omni_getbalance address propertyid |
balance, reserved, frozen |
omni_getallbalancesforid propertyid |
every holder, largest first: address, balance, reserved, frozen |
omni_getallbalancesforaddress address |
every token one address holds |
omni_getwalletbalances / omni_getwalletaddressbalances |
what this node's wallet holds, per token / per address |
omni_gettransaction txid |
one 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 height |
txids 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 yet — 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):
| Method | Prepares |
|---|---|
omni_send from to propertyid amount |
a send |
omni_sendissuancefixed from ecosystem type previousid category subcategory name url data amount |
a fixed-supply token (type 1 whole units, 2 divisible; ecosystem 1; previousid 0) |
omni_sendissuancemanaged from ecosystem type previousid category subcategory name url data |
a 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 propertyid |
handing the token to a new issuer |
omni_broadcast txid |
sends 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 id |
the answer: pending, sent (with txid), denied, failed (with error) or expired |
da_requests [count] |
the ones waiting, then the newest decided |
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:
{"txid": "2ea169a4…", "hex": "…",
"sendingaddress": "nYW2BPLENpu2nGa7WCExvzxD3hQYueULFa",
"referenceaddress": "ncpXrSCQx667v4y92zSWidxs96N5TtPjTE",
"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": "ncpXrSCQx667v4y92zSWidxs96N5TtPjTE", "change": false, "recipient": true},
{"value": "1.48683000", "to": "nYW2BPLENpu2nGa7WCExvzxD3hQYueULFa", "change": true, "recipient": false}],
"broadcast": false}
(a real omni_send of 1 Arcade Test on testnet; the fee is what the node's
getmempoolentry reported for the send before it, byte for byte the same
size)
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 what was shown (D-016) — 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.
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:
{"method": "da_requesttoken",
"params": ["", "ncpXrSCQx667v4y92zSWidxs96N5TtPjTE", 3, "5", "tournament prize"]}
{"id": "402d9a3126e70394", "network": "test", "kind": "token", "status": "pending",
"from": null, "to": "ncpXrSCQx667v4y92zSWidxs96N5TtPjTE", "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 — on
this machine and on the phone over the remote tunnel — 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
(inscription-api.md), so a page and a bot asking for the same thing look the
same to the person deciding.
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.
/rpc/main answers the same methods. Whether a send there works depends on
the mainnet node having a wallet (it runs disablewallet=1 until deliberately
changed) — 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, as everything here was.