Protocol documentationSource snapshot: 0.1.0 alphaNetwork: testnet / devnet

Interfaces

Local RPC v1

The private authenticated wallet API: loopback binding, bearer-token lifecycle, exact routes, schemas, pagination, readiness gates, transaction receipts, and stable errors.

Private local API · version 1Reviewed August 31, 2026
Specification status

This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.

Purpose and trust boundary

RPC v1 is the full node’s private control surface for the local wallet and operator tooling. It exposes account state, confirmed history, blocks, synchronization status, and transaction submission.

It is intentionally not the public explorer API:

Property Private RPC Explorer API
Base routes /v1/* /api/v1/*
Writes transaction submission none
Authentication bearer token none in alpha
Default bind loopback only loopback by default, explicitly publishable
Primary client local wallet/operator public website/explorer

Never point a public web frontend at RPC. Never copy rpc.token into browser code or a static build.

Bind and ports

The server accepts only loopback host values:

127.0.0.1
::1
localhost

Any other configured bind is rejected at startup. Network defaults are:

Network RPC port
mainnet 8444
testnet 18444
devnet 28444

Mainnet cannot initialize in the current alpha; its port is reserved.

Bearer token lifecycle

On first start, the node creates:

<data-directory>/rpc.token

The token is generated with the operating system CSPRNG and is at least 32 characters. Creation uses exclusive file semantics and requests mode 0600.

When loading an existing token on POSIX, the implementation verifies:

  • the path itself is a regular file, not a symbolic link;
  • open does not follow a symlink where supported;
  • file identity does not change between metadata check and open;
  • the owner is the current user;
  • group and other permission bits are absent;
  • file size does not exceed 4,096 bytes;
  • content is valid UTF-8 with no embedded whitespace;
  • only an optional final LF or CRLF is tolerated;
  • the resulting token has at least 32 characters.

Failure is fatal rather than silently replacing suspicious material.

All routes except /health require:

Authorization: Bearer <token>

Comparison is constant-time. A missing, malformed, or incorrect header returns 401:

{ "error": "unauthorized" }

Safe command-line access

For local troubleshooting, avoid placing the token directly in shell history. One option is to read it into a task-specific variable:

LURA_RPC_TOKEN="$(tr -d '\r\n' < "$LURACOIN_DATA_DIR/rpc.token")"
curl -s \
  -H "Authorization: Bearer $LURA_RPC_TOKEN" \
  http://127.0.0.1:18444/v1/status
unset LURA_RPC_TOKEN

Protect terminal logs and process diagnostics. The token grants transaction-submission capability for that node.

Endpoint matrix

Method Route Auth Purpose
GET /health no minimal process/API probe
GET /v1/status yes node, chain, synchronization, and peers
GET /v1/accounts/{address} yes confirmed and pending-aware account state
GET /v1/accounts/{address}/transactions yes first-page pending plus indexed confirmed history
GET /v1/blocks/{height} yes one canonical block with all transactions
POST /v1/transactions yes admit and relay one complete signed transaction

The maximum HTTP request body is 1,000,000 bytes, while the submission route imposes the much smaller canonical transaction length after decoding.

Monetary JSON rule

Every amount crossing JSON is a decimal string:

{
  "balance": "5000000000",
  "spendable_balance": "4999999000",
  "value": "1000",
  "fee": "0"
}

This preserves uint64 values in JavaScript. Clients must parse amounts with BigInt, decimal-string libraries, or checked arbitrary-precision integers—never binary floating point.

Nonces, heights, ports, counts, timestamps, and API versions are JSON numbers within their bounded domains.

GET /health

Unauthenticated response:

{
  "ok": true,
  "network": "testnet",
  "api_version": 1
}

This proves the HTTP process is serving and identifies its configured network. It does not prove initialization, synchronization, Redis availability, peer connectivity, or readiness to submit.

GET /v1/status

Representative shape:

{
  "network": "testnet",
  "chain_id": 1,
  "magic": "fbc0b6db",
  "data_dir": "/home/alice/.luracoin/testnet",
  "api_version": 1,
  "initialized": true,
  "height": 314,
  "best_peer_height": 320,
  "peer_count": 1,
  "peers": [
    {
      "host": "203.0.113.10",
      "port": 9999,
      "inbound": false,
      "height": 320,
      "last_seen": 1788172000.25
    }
  ],
  "syncing": true,
  "ready": false
}

initialized means genesis/canonical storage is present. An uninitialized chain reports height -1. best_peer_height is based on connected peers’ untrusted handshake advertisements.

ready means initialized, not actively syncing, and at least as high as every connected advertisement. It is a local transaction-submission gate, not consensus finality.

GET /v1/accounts/{address}

For a valid canonical address:

{
  "network": "testnet",
  "address": "L...34 characters total...",
  "balance": "12500000000",
  "spendable_balance": "11900000000",
  "nonce": 7,
  "next_nonce": 9,
  "pending_outgoing_count": 1,
  "mempool_available": true
}

Definitions:

  • balance: confirmed canonical balance;
  • nonce: last confirmed outgoing nonce;
  • spendable_balance: balance after simulating the readable contiguous pending outgoing sequence;
  • next_nonce: nonce immediately after that pending sequence;
  • pending_outgoing_count: number of transactions in that sequence;
  • mempool_available: whether Redis could be read consistently.

An address with no confirmed record returns zero balance and nonce rather than 404.

When Redis is unavailable or malformed, confirmed values remain available but:

{
  "spendable_balance": "<confirmed balance>",
  "next_nonce": "<confirmed nonce + 1 as JSON number>",
  "pending_outgoing_count": 0,
  "mempool_available": false
}

Clients must disable sending in that state.

GET account transaction history

Route:

/v1/accounts/{address}/transactions?limit=50&cursor=<opaque>

limit defaults to 50 and is clamped into 1–200. A non-integer value returns invalid_limit.

Response:

{
  "network": "testnet",
  "address": "L...",
  "mempool_available": true,
  "transactions": [
    {
      "id": "64 lowercase hex characters",
      "chain": 1,
      "nonce": 8,
      "fee": "0",
      "value": "600000000",
      "from_address": "L...",
      "to_address": "L...",
      "status": "pending",
      "block_height": null,
      "timestamp": null
    }
  ],
  "next_cursor": "opaque-or-null"
}

Pending transactions are volatile and appear only when no cursor is supplied—the first page. They are newest-first within the contiguous local pending view. Confirmed entries add status: "confirmed", block height, and block timestamp.

If pending items fill the page, the server may return the sentinel cursor latest; pass it back unchanged to begin at the newest confirmed index entry. All other cursor values are opaque. Do not decode or synthesize them.

Stale/mismatched internal index references are logged and omitted rather than returned as incorrect history.

GET /v1/blocks/{height}

The height must be a non-negative base-10 integer. The response is the block’s JSON representation plus network, with txns containing every transaction.

Representative fields include:

{
  "network": "testnet",
  "version": 1,
  "id": "0000...",
  "prev_block_hash": "0000...",
  "height": 42,
  "miner": "L...",
  "timestamp": 1788172000,
  "bits": "1dffffff",
  "nonce": 12345,
  "txns": []
}

Transaction value and fee remain strings. A nonexistent height returns 404 block_not_found.

POST /v1/transactions

Request:

POST /v1/transactions
Content-Type: application/json
Authorization: Bearer <token>

{ "raw": "<426 hexadecimal characters>" }

raw must decode to exactly 213 bytes. The node does not accept an unsigned body, field-by-field JSON, base64, or a transaction ID.

Processing order:

  1. parse JSON and hexadecimal;
  2. enforce exact decoded length;
  3. require node readiness when attached to a running P2P node;
  4. deserialize and perform mempool admission;
  5. attempt broadcast to connected peers;
  6. return the local acceptance receipt.

Successful status is 202 Accepted:

{
  "network": "testnet",
  "accepted": true,
  "broadcast": true,
  "relay_count": 2,
  "transaction": {
    "id": "...",
    "chain": 1,
    "nonce": 8,
    "fee": "0",
    "value": "600000000",
    "from_address": "L...",
    "to_address": "L...",
    "status": "pending",
    "block_height": null,
    "timestamp": null
  }
}

accepted: true means local Redis admission succeeded. It does not mean the transaction is mined or final. broadcast is exactly relay_count > 0; a zero count remains a valid local acceptance and the node retains the transaction for bounded replay.

If initial relay throws after admission, the API still reports acceptance with zero successful relays. A wallet must not re-sign automatically in response.

Error contract

Status Error Meaning
400 invalid_address address syntax/checksum/version invalid
400 invalid_limit history limit is not an integer
400 invalid_cursor opaque history cursor is malformed
400 invalid_height block height is not a non-negative integer
400 invalid_transaction_encoding JSON/raw field/hex decoding failed
400 invalid_transaction_length decoded body is not 213 bytes; includes expected/actual
400 invalid_transaction deserialization or validation raised
401 unauthorized bearer missing or incorrect
404 block_not_found canonical height absent
422 transaction_rejected well-decoded transaction was not admitted
503 node_not_ready node is initializing or behind a connected advertisement
503 mempool_unavailable Redis failed during submission

Clients should branch on error, not human prose. Transport failure remains distinct from a JSON rejection.

Wallet integration rules

The official Electron wallet keeps RPC access in its privileged main process. The renderer requests approved operations through a narrow IPC bridge and never receives the bearer token.

The browser-wallet gateway similarly adds the bearer server-side. It exposes only four allowlisted routes, enforces same-origin request metadata and a per-process HttpOnly cookie, and never serializes rpc.token into frontend assets.

A third-party client should follow the same principles:

  • keep the token out of browser storage and renderer state;
  • validate network and chain ID on every session;
  • check ready and mempool_available before enabling send;
  • use confirmed nonce plus server-provided pending state;
  • treat amounts as integer strings;
  • present 202 as pending, never confirmed;
  • paginate with opaque cursors;
  • poll/reconcile by transaction ID and sender nonce.

Exposure prohibition

Loopback enforcement is a defense, not permission to forward the port publicly. A reverse proxy, SSH remote forward, container host mapping, or desktop webview misconfiguration can still move the trust boundary.

RPC has no public-user authorization model, account isolation, CSRF layer, rate limiting, or write quotas. Possession of its bearer token authorizes submission to that node. Keep it local.

Source anchors

Primary implementation files used for this chapter: