Skip to content

HTTP API

Base operator URL: http://127.0.0.1:<rpc-port>

The API has one unversioned route set. /v1/... does not exist. The operator listener is loopback-only and authenticated (Authentication). The optional public-read listener is unauthenticated. The optional public-read listener registers only the GET routes in the read table below, plus POST /transactions when its operator turns public submit on (Public submit); it is off by default.

Requests and responses are JSON unless noted. Operator POSTs require Content-Type: application/json and a loopback Host authority.

The operator listener works like bitcoind’s RPC cookie. At every start the node writes a fresh random cookie to <data-directory>/.cookie (override with --rpc-cookie-file; under lattice up that is <root>/chains/Nexus/.cookie), mode 0600, content __cookie__:<token>, and removes it at exit. Every route on the operator port requires it except GET/HEAD /health (public chain status, also served by the public listener; open so health probes need no secret). Send either:

  • Authorization: Basic <base64 of the file's content> (curl --user "$(cat .cookie)"), or
  • Authorization: Bearer <token> (the part after __cookie__:).

Anything else gets 401 (no WWW-Authenticate challenge). The cookie changes at every restart; clients re-read the file.

Browsers are refused: a request with an Origin header gets 403 unless the operator lists that exact origin with --rpc-allowed-origin (rpcAllowedOrigins in lattice.json), e.g. chrome-extension://<extension id>. A listed origin gets CORS preflight answers (GET, HEAD, POST; headers Authorization, Content-Type) and Access-Control-Allow-Origin on responses, refusals included, and still needs the cookie.

Every 64- or 128-bit consensus integer, in responses and in the POST /transactions body, is a canonical base-10 JSON string: 0, or an optional - then a nonzero digit then digits (^(?:0|-?[1-9][0-9]*)$). A JSON number, a leading + or zero, -0, or an out-of-range value is refused (400). These are heights, revisions, timestamps, block nonces, balances, account and transaction nonces, deltas, deposit/receipt/withdrawal nonces and amounts, rewardCredited, minRelayFee, finalityWindow, and the chain spec’s 64-bit fields. Small counts and sizes (transactionCount, mempoolCount, mempoolBytes, count, version, maxBlockSize, offsets) stay JSON numbers. Targets are hex strings. An absent optional field is omitted, never null.

The mining routes (/mining/templates, /mining/work) are the miner’s protocol, not this read/submit contract, and are unchanged: their block is the consensus block’s own encoding.

GET routes accept an optional absolute query value:

?chainPath=Nexus/Alpha

Omitting it selects Nexus. A named path must be hosted by this process or the request returns 404.

Transactions carry body.chainPath; the node routes them to that level. Mining templates are requested at Nexus and can carry every hosted descendant.

Route Purpose Public-read listener
GET /health Published chain and mempool status yes
GET /transactions/:cid Content-verified transaction by CID yes
GET /volumes/:cid Complete Volume as the Ivy binary archive yes
GET /accounts/:owner?block=<cid> Account balance and next nonce at an accepted block yes
GET /api/block/latest Latest executed block summary yes
GET /api/block/:height-or-cid Block detail yes
GET /api/blocks?before=<height>&limit=<n> Page of canonical block summaries, newest first yes
GET /api/block/:cid/transactions Paginated block transactions yes
GET /api/block/:cid/children Bounded child commitments yes
GET /api/transaction/:cid Explorer transaction model yes
GET /api/state/account/:addr Account at the current executed tip yes
GET /api/deposits?limit=<n>&after=<key> Page of active deposits, with a state proof yes
GET /api/deposit-state?key=<demander/amount/nonce> One deposit key: its locked amount, 0 once withdrawn, or absent; with a state proof yes
GET /api/receipt-state?demander=&amount=&nonce=&chainPath=P/D The withdrawer a receipt on P records for a deposit on P/D, with a state proof yes
GET /api/mempool Bounded mempool listing yes
GET /api/fees The blocks this node’s pool would fill next, for fee pricing yes
GET /api/peers?chainPath=P The chain’s own current peer count: the open sessions that serve it. Without chainPath, Nexus’s, which is every ready session yes
GET /api/chain/info Chain metadata yes
GET /api/chain/spec Active chain spec yes
GET /api/chain/genesis Selected genesis CID yes
GET /api/chain/children?chainPath=P The child directories P’s executed blocks have committed, paged yes
GET /api/chain/endpoints?chainPath=P/D Declared read URLs of a child chain, unverified yes
GET /status Operator status including template digest no
GET /metrics Prometheus exposition no
GET /core/snapshot Internal root snapshot no

Block transaction pages accept offset and limit; limits are capped at 100. CID and response sizes are bounded before decoding.

GET /volumes/:cid returns application/vnd.lattice.volume: Ivy’s canonical complete-Volume archive (big-endian count and length framing, CID-sorted entries), capped at 64 MiB. On the loopback port it serves only a Volume the node holds. On the public read listener, a Volume the node does not hold is read from peers, checked against its CID, served and not kept; readers asking for the same Volume share one read, and --public-read-fetches (default 4) caps how many are read at once. No more run than fit, at the 64 MiB cap each, in half of the node’s in-flight content budget (--serving-max-in-flight-volume-bytes), so its own sync keeps the other half; at least one always runs. At the default budget (128 MiB) that is one at a time; the node prints the number at start. Past the limit, or while the budget or the node’s pending peer requests have no room, the answer is 503 with Retry-After, not 404, and a reader should ask again. Behind a proxy that presents every client as one address, all clients share these reads. --public-read-fetches 0 serves only what the node holds. A Volume no peer served within the caps above is 404. The immutable response is billed to the expensive public-read budget. Like the other per-level reads, a hosted child is selected with ?chainPath=Nexus/Alpha; omitting it selects Nexus.

Both /api/block/latest and /api/block/:height-or-cid report the block’s coinbase. rewardRecipient is the header’s recipient address; it is omitted when the block burns its reward. rewardCredited is what consensus credited that recipient (Block.coinbaseAmount): the block reward at that height plus the block’s fees, the balance excess of its transactions. It is 0 for a burned block and omitted only when this node does not hold the block’s spec or transaction bodies.

/api/blocks lists the chain to the tip this node acts on, newest first, at any hosted level (?chainPath=). It returns {"blocks": [...], "nextBefore": "<height>"}; each row is height, hash, previousBlock, timestamp, transactionCount and rewardRecipient. Rows are read from block headers and the transactions dictionary root only — no transaction body — so there is no rewardCredited (read /api/block/:height-or-cid for it). before (default: tip + 1, and clamped to it) is exclusive; limit defaults to 10 and is capped at 25; at most limit heights are visited, and a height whose block this node does not hold is omitted. Pass nextBefore as the next page’s before; it is omitted once height 0 is listed. A non-numeric before, or a non-numeric or non-positive limit, is 400.

/api/block/:cid/children works at any hosted level (?chainPath=). Each entry is the child’s directory and committed blockHash; height and transactionCount are omitted when this node does not hold the child block (it does not host that child chain).

GET /api/chain/children?chainPath=Nexus/Alpha lists, in order, the child directories the chain’s executed blocks have committed. The node keeps the list in memory: built from the blocks it holds when it starts, then grown by every block that executes valid. It only grows. A listed child is reached through its hosts (/api/chain/endpoints).

The list is paged: limit (default and cap 200) names, starting after the name after (any string; omitted, from the first). next is the after for the following page and is absent on the last. A node predating paging answers the whole list without next.

{ "chainPath": ["Nexus", "Alpha"], "directories": ["Beta", "Gamma"], "next": "Gamma" }

GET /api/chain/endpoints?chainPath=Nexus/Alpha/Beta names a child; this node must host its parent (Nexus/Alpha), else 404. 400 for a missing or malformed path, or Nexus itself.

{
"chainPath": ["Nexus", "Alpha", "Beta"],
"committedBlock": "<cid of the newest child block the parent commits, or null>",
"endpoints": ["https://reads.example.org"],
"submitEndpoints": ["https://reads.example.org"]
}

submitEndpoints is the subset of endpoints whose hosts also declare that they accept POST /transactions at that URL (equally unverified: confirm with GET /api/chain/info there, whose acceptsSubmit is true). A host that accepts submits answers .response.v2 (the v1 fields plus acceptsSubmit) before its .response.v1; an older reader drops the unknown topic and still gets the URL, and a v1 answer alone counts as not accepting. An answer from a node predating the field has no submitEndpoints; read it as empty.

endpoints lists this node’s own declared URL first when it hosts the child, then URLs other hosts declared over the overlay (lattice.overlay.read-endpoint.request.v1 / .response.v1; a node answers only for a level it hosts). The lookup asks at most 8 hosts found through the child’s read-endpoint provider records, in random order, takes at most 2 URLs from any one, ends within 2 seconds (provider discovery included), closes any session it dialed only to ask, coalesces identical concurrent lookups, caches a result for 30 seconds (an empty one for 5), and runs at most 16 lookups at once: past that, a lookup for another name finds nothing and is not cached. The URLs are NOT verified and may name any host, internal addresses included: check what a URL serves before using it (GET /api/chain/info?chainPath=... there names the chain), and do not dial a non-public host on a third party’s behalf.

A child is found through the hosts that announce it, so it is listed whether or not the parent’s recent blocks commit it. committedBlock is information only: the newest child block the parent’s last 64 canonical blocks commit, null when they commit none. It can be a block the child chain did not keep, or one nobody serves, so a reader never requires a URL to serve it. The route answers 404 when this node does not host the parent, or when nothing hosts, declares or commits the child. Billed to the expensive public read budget.

{
"phase": "active",
"chainPath": ["Nexus"],
"nexusGenesisCID": "bafyreiggtg4ezifboyekbf4gxst2jr3mpjqcxsbmopy7w6fp46g4ngpgxa",
"tipCID": "<cid>",
"height": "42",
"mempoolCount": 3,
"mempoolBytes": 2048,
"bestHeaderHeight": "57"
}

bestHeaderHeight is the height of the best header chain the node knows (absent before any header); height is the height of the heaviest executed tip, the block the node acts on. It is on the best header chain unless a heavier chain’s bodies are not held, so sync progress is height of bestHeaderHeight. Both are per level (?chainPath=).

waiting is present while the level’s next block cannot be executed, and says which block and why, as body of <cid>: <reason>. The reason is one of a fixed set: exceeds this node's byte budget (verified content of the body is past what this node holds for one fetch), not obtained from any peer, local storage failed, or not connected: <failure> (its connect ended without a verdict; <failure> is the import failure’s name). No error text and no setting’s value is shown: the error behind a reason, and the budget, are in the node’s log. The node keeps retrying; the field goes when the block executes, is excluded, or leaves the best chain.

/health reads an immutable published view and never enters the core loop. It has no templateDigest. /status uses the root runtime’s current template digest so a miner can detect any hosted-tree input change.

A hosted child with no executed genesis reports awaitingGenesis and null tip fields. A child genesis is a normal child root secured by a proof from a mined ancestor grind; no deployment endpoint or parent authorization transaction is involved.

{
"transaction": {
"signatures": {"<public-key-hex>": "<signature-hex>"},
"body": {
"accountActions": [],
"actions": [],
"depositActions": [],
"receiptActions": [],
"withdrawalActions": [],
"signers": ["<address>"],
"nonce": "0",
"chainPath": ["Nexus", "Alpha"]
}
}
}

Response:

{
"transactionCID": "<cid>",
"mempoolCount": 4,
"mempoolBytes": 2600
}

The body is content-bound and the signature, nonce, funding, fee, and path are validated before admission. An unhosted path returns 404. A transaction must fit in a block by itself: past maxBlockSize less 2048 bytes for the block’s own content and its place in it, the refusal is tooLarge (so a chain whose maxBlockSize is 2048 or less admits nothing). So is one whose state growth, at most (Lattice’s stateGrowthCeiling), is past maxStateGrowth less 128 bytes for the coinbase’s.

The node’s pool admits only transactions paying at least the operator’s --min-relay-fee (default 1): the fee is the transaction’s balance excess, which the block’s recipient is credited. Below it the refusal is belowMinRelayFee. This is node relay policy, never consensus: a block carrying a cheaper transaction stays valid, and other nodes choose their own floor. GET /api/chain/info reports this node’s as minRelayFee.

A refusal names its reason. The retriable ones say to send the same transaction again later; the rest say to change it.

Reason Meaning Retry as is
nonceAhead an earlier transaction from its first signer is not mined yet yes, once it is
unavailable this node cannot judge it yet (state, a policy module, a withdrawal’s receipt) yes
insufficientFunds an account it debits cannot also pay what pooled transactions debit it, and displacing them would not put it in the next block and make that block pay the relay floor more yes, once those are mined
full the pool is full of transactions paying a higher rate (429) yes
contextChanged the tip moved too often while it waited (503) yes
feeTooLow it shares a nonce, state key, deposit or receipt with pooled transactions, and displacing them would not put it in the next block and make that block pay the relay floor more (uncongested: all their fees plus the floor) no: raise the fee
belowMinRelayFee its fee is under this node’s floor no: raise the fee
tooLarge, invalidState it can never be pooled no

GET /api/chain/info also reports the chain’s node-local finality window, in blocks, as finalityWindow: the operator’s --finality-window for that chain. It is node policy, never consensus, and is absent when off (the default).

GET /api/fees projects the next blocks (up to 12) this node’s pool would fill if nothing else arrived, as this node’s builder fills them: pooled transactions by fee per weight, each block taking them up to the chain’s transaction count, maxBlockSize (each charged its place in the block) and maxStateGrowth (each charged its state growth at most), skipping one that does not fit for a smaller one behind it. A block’s rate is the lowest it carries. It is recomputed on each new tip and, while the pool changes, at most once a second.

A transaction’s weight is the most of a block it takes of any limit, in the block’s bytes: the largest of its size plus transactionAllowance (its place in the block), its state growth (Lattice’s stateGrowthCeiling) times blockRoom / growthRoom rounded up, and blockRoom / maxTransactions. Where neither growth nor count binds before size, as on Nexus, weight is the bytes a block charges it. weight gives the four for this chain.

{
"minRelayFee": "1",
"blocks": [
{ "transactionCount": 1500, "bytes": 570000, "lowestFee": "40", "lowestFeeBytes": 380, "lowestFeeWeight": 636, "full": true },
{ "transactionCount": 400, "bytes": 140000, "lowestFee": "1", "lowestFeeBytes": 350, "lowestFeeWeight": 606, "full": false }
],
"weight": { "blockRoom": 998208, "growthRoom": 2999872, "maxTransactions": 5000, "transactionAllowance": 256 }
}

A block is full when ready transactions were left for a later one. While none is full (or blocks is empty), a fee at the floor makes the next block. To be in the first k blocks when block k is full, a transaction of weight w pays more than lowestFee * w / lowestFeeWeight of that block. Older nodes omit weight and lowestFeeWeight; their rates are per byte. An older client that prices per byte against this node is close where weight tracks size, as on Nexus, and underpays on a chain whose growth or count binds first.

This is an estimate from one node’s pool, not a promise: other miners fill their blocks otherwise, other pools differ, and new transactions arrive. Fees above UInt64 never occur for pooled transactions, which are funded.

GET /api/transaction/:cid reports blockHeight, blockHash and timestamp (the block’s) when the transaction is in this chain’s canonical executed chain, and omits them otherwise (pending, never mined, or left by a reorg). Nothing is indexed: a signer’s next nonce only rises along the canonical chain, and executing the transaction is what moves its first signer’s nonce past the transaction’s, so the node binary-searches the canonical post-states (at most ~64 account reads) for the first block whose nonce exceeds it, then confirms that block’s transaction list holds the CID (a rival at the same nonce also moves the nonce). Reorg-correct because it reads the current canonical chain; the answer is cached for 3 seconds, not as immutable. Billed to the expensive public read budget. When the post-state or block content needed is not held here, inclusion is omitted.

POST /transactions on the public-read listener exists only when the operator starts the node with --public-submit ("publicSubmit": true in lattice.json); otherwise it is 404. When on, it takes the same body and gives the same answer and named refusals as the operator route, without the operator route’s loopback Host and Content-Type checks (CORS allows POST on that listener). Differences:

  • the body is capped at 1 MiB before decoding (413 above it);
  • each client is billed to its expensive read budget, and the listener to a separate submit budget (--public-submit-rate, default 10/s): 429 over either;
  • the transaction is volatile, never written to the local journal, and is held to the pool’s ordinary capacity rules (fee-rate eviction and replacement) with no priority over peer gossip. While it waits for its preflight verdict it counts against its own bound (full when over). Once admitted it is relayed by ordinary gossip.

GET /api/chain/info reports acceptsSubmit for the listener that answers: true on the operator listener, and on the public listener only when public submit is on.

{
"recipients": [
{"chainPath": ["Nexus"], "address": "<address>"},
{"chainPath": ["Nexus", "Alpha"], "address": "<address>"}
],
"minimumWork": [
{"chainPath": ["Nexus"], "work": "0x100000000"}
]
}

recipients names the reward and fee destination by absolute path. A missing entry burns that level’s payout. Each block commits its recipient in its proof-of-work preimage.

minimumWork is a miner-local search filter. It can demand harder hashes than the scheduled target but cannot make consensus easier and is not committed as the chain’s target.

The response contains:

  • workID — opaque issued-work identifier;
  • block — complete nonce-zero Nexus candidate carrying child candidates;
  • searchTarget — the easiest threshold that can advance at least one carried level, constrained by the request’s minimum-work plan;
  • targets — thresholds useful to the worker;
  • chainPath — ['Nexus'];
  • expiresInMilliseconds — remaining lifetime;
  • templateDigest — fingerprint of the complete hosted tree’s template inputs.

A change to what the next block at any hosted level would carry, or a new candidate, changes the digest, even if the Nexus tip did not move.

{"workID": "<opaque work id>", "nonce": 123456}

Response:

{
"accepted": true,
"disposition": "canonicalized",
"tipCID": "<cid>",
"durableChildProofs": []
}

Possible dispositions are canonicalized, acceptedSide, childOnly, duplicate, and invalid. childOnly means the hash missed Nexus but met at least one carried child target. The node stores only child blocks actually secured by that hash.

The reply is sent after the resulting node step is durable. Every affected level’s facts and stream cursors commit in one state.db transaction.

The child chains the node hosts change while it runs. These routes are on the operator port only.

{"chains": ["Nexus/Alpha"]}

The hosted child chains, parent first.

{"hostChain": ["Nexus/Alpha", "Nexus/Beta=/path/to/spec.json"],
"premineTo": ["Nexus/Beta=<address>"],
"chainPeer": ["Nexus/Alpha=<public-key>@host:port"],
"policyModule": ["/path/to/module.wasm"]}

The node hosts these chains and only these, from now on. Each entry reads as the start flag of the same name (--host-chain, …); only hostChain is required. A newly listed chain starts as it would at start and syncs from the node’s peers; a chain left out stops with every chain below it, and what it stored stays, so listing it again resumes it. Answers as GET /hosted-chains. 400 when the list does not hold together (a child before its parent, a flag value that does not parse); 404 (notHeld, unknownChain) when a chain could not start from what it stored.

A peer that connects afterwards is told the new list in the hello. A peer already connected learns a newly hosted chain from its provider record, and a peer that asks for a stopped one is answered as by any node that does not host it.

A Volume the operator names by its root CID is held, served to peers and announced as held, until the operator unpins it. The node does not look inside it or follow anything it names, and it is kept whatever blockBodies says. These routes are on the operator port only.

{"pins": ["<cid>"]}
{"cid": "<cid>", "entries": {"<cid>": "<base64 bytes>"}}

Pins the Volume and answers the pins as GET /pins does. A Volume the node holds needs only cid. One it does not hold is stored only as the operator provides it in entries, the root and the entries it lists, each checked against its CID; the node never reads a pin from peers. 404 (notHeld) when the node does not hold it and entries does not provide it; 400 (invalidVolume) when entries lacks the root or an entry is not the bytes its CID names; 409 (conflictingVolume) when the store holds that root with other entries; 503 (contextChanged) when the Volume went from the store before it was pinned.

Reads the Volume from peers and answers it in the shape POST /pins takes, each entry checked against its CID, without storing it: an application validates what it reads and pins it if it keeps it. 404 (notServed) when no peer serves it.

Unpins the Volume and answers the pins left. It goes from the store unless something else the node keeps names it. A CID the operator did not pin changes nothing.

  • 400 Bad Request — malformed JSON, invalid content, policy refusal, or invalid work;
  • 413 Content Too Large — public submit body over 1 MiB;
  • 401 Unauthorized — operator route without the node’s cookie (no challenge header);
  • 403 Forbidden — browser Origin not listed, or a non-loopback Host;
  • 404 Not Found — unknown resource or unhosted chainPath;
  • 409 Conflict — a pin whose root the store holds with other entries;
  • 415 Unsupported Media Type — operator POST without JSON content type;
  • 429 Too Many Requests — mempool or public-read rate limit;
  • 503 Service Unavailable — stopping process or transient core context.

Error bodies use Hummingbird’s JSON error envelope and preserve the named refusal where available.


Source: lattice-node/docs/rpc-api.md at 9abd15b1bb05