Blocks
The ledger is a chain of signed blocks, each naming the one before it by hash. List them newest first, filter them, read one with its transactions, and check its hash and signature yourself.
GET /api/blocks
https://mlx.app/api/blocks?limit=2Blocks, newest first, with the chain head. Without filters it is a fast page through the newest blocks; with any of sort, min, period or cursor it becomes a filtered scan.
curl "https://mlx.app/api/blocks?limit=2"Newest first
| Parameter | Type | Notes |
|---|---|---|
limit | integer | How many blocks. The route has a default and a maximum, and clamps anything outside it. |
before | integer | Start below this height, for the next page: pass the last height you got. |
{
"head": {
"height": 397,
"hash": "ef5f250abbe3aa7f14767ea19ce5b1b67525fff8a8c039f0e72d5a7d33d04601",
"ts": 1793113786120
},
"blocks": [
{
"height": 397,
"prev": "3e8c4828eaf513d12d0096afc3db2b8a7833c6eb8e24d913df7dd6a5a66d0d63",
"ts": 1793113786120,
"txRoot": "26c5d2d2f219185f18af55dca8b38e793688d8ae61a86ab397c78b0769e9c31a",
"count": 1,
"producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
"hash": "ef5f250abbe3aa7f14767ea19ce5b1b67525fff8a8c039f0e72d5a7d33d04601",
"txs": ["26c5d2d2f219185f18af55dca8b38e793688d8ae61a86ab397c78b0769e9c31a"],
"sig": "a3e6fcda3caafa0083396f24bc5d198e1375ed7e62ba957673104ba7800f88ae6b0420273174f7b22fe82edd00dbc8190de9f0564b5608208e92cefd48d9f408"
},
{
"height": 396,
"prev": "afaccda78033a68bd9298a5d3d5c5040869f7e3cf288909f6c385887c47e84b0",
"ts": 1793113778971,
"txRoot": "13e04bcbfca47659cfd1d1fc07ee93125cc8d67ac5ca889b8578dc559f4d378e",
"count": 1,
"producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
"hash": "3e8c4828eaf513d12d0096afc3db2b8a7833c6eb8e24d913df7dd6a5a66d0d63",
"txs": ["13e04bcbfca47659cfd1d1fc07ee93125cc8d67ac5ca889b8578dc559f4d378e"],
"sig": "a5d4de677c3fecb889b8e0e9a33ea726105da17f7563c0595a1b9dd8bedf74dd12b1183db2b47958a22e91e55331a0ef2e1d3dec1c381fa7429060622703ab0e"
}
]
}Notice that block 397's prev is block 396's hash: that is the link.
Filtered
| Parameter | Type | Notes |
|---|---|---|
sort | new or old | Newest first (default) or oldest first. |
min | integer | Only blocks with at least this many transactions. |
period | 1h, 24h, 7d, 30d or all | Only blocks from that window. |
cursor | integer | The next value of the previous page. |
limit | integer | Page size, clamped like the unfiltered list. |
A filtered call looks at a bounded number of heights and stops, so a page can hold fewer blocks than limit (even none) while next is still set. Keep calling with cursor=<next> until next is null.
{
"head": { "height": 399, "hash": "321578e08b05821326965df711b4d96cbcfafc0960704eac13e49d4167f1e8cd", "ts": 1793116923973 },
"blocks": [
{
"height": 353,
"prev": "80e6d40836464c3f4cba025ec73675d42732c918cb7a8f5b87709a3f00065219",
"ts": 1793034123926,
"txRoot": "bea17547c9147d16e96e7622f6fd086980318eacbf830020c87e456644c26aa0",
"count": 1,
"producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
"hash": "d784c9a78d8be81a83c78b8176154ac5435ef5b7baffe4c489463f60ccc04873",
"txs": ["bea17547c9147d16e96e7622f6fd086980318eacbf830020c87e456644c26aa0"],
"sig": "64b18347c567098599e1a9005b268d5dc2bfbc050540ffd230e082f89845d7792c0f6111395c90a473ea235995dfb076fe722d3653169156d22f754845f61b0e"
}
],
"next": 354,
"scanned": 1
}| Field | Type | Notes |
|---|---|---|
next | integer or null | The height to continue from; null when the scan reached the end. |
scanned | integer | Heights looked at in this call. |
GET /api/block/{height}
https://mlx.app/api/block/1One block and every transaction in it, in order. A sealed block never changes, so the response is cached as immutable.
| Parameter | Type | Notes |
|---|---|---|
height | integer (path) | 0 is the genesis block. |
{
"block": {
"height": 1,
"prev": "b5100a4ff5e14bd01a6240ea2809770985cbb944d83d4a940f87b75efcd1f770",
"ts": 1792312855965,
"txRoot": "a3f53b6b0d575b5540b9310a7fd247ad5c0260e09c61bd9c3faa4ea3f530f1a3",
"count": 1,
"producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
"hash": "73c50d2f0bfd7df198234734ab6e9d6287653b49a6a5e61543ee5a05e80c25d5",
"txs": ["a3f53b6b0d575b5540b9310a7fd247ad5c0260e09c61bd9c3faa4ea3f530f1a3"],
"sig": "30159ff85784b0ead874031d7c848524f2a51f4442d420d8936ba99380e47de799c82c520cac56fbabf6dc4259b04e80995a60002bd9bd44e8b0b0b4333ee80a"
},
"txs": [
{
"hash": "a3f53b6b0d575b5540b9310a7fd247ad5c0260e09c61bd9c3faa4ea3f530f1a3",
"tx": {
"chain": "mlx-1",
"from": "luca.mlx",
"nonce": 0,
"ts": 1792312855563,
"pk": "53fbe3a2e4ee9ee2ad907174562e8f2fc9b2bb3e1074f8ce274935439422c4ab",
"body": { "type": "create_account" },
"sig": "c461d94586ddd6c63c235984332a9508b5f04f1fd8c0f7f32d35377152c4d658a4951ce88ffce0275eb4bbf33bbbeedc9755234ec22cda1b06d85ff6be89fe06"
},
"status": "success",
"receivedAt": 1792312855939,
"effects": [{ "kind": "account", "to": "luca.mlx" }],
"height": 1,
"index": 0
}
]
}The transactions are in the record format described on Transactions, with Mac names hidden the same way.
The block format
| Field | Type | Notes |
|---|---|---|
height | integer | Position in the chain, from 0. |
prev | string | The previous block's hash (64 zeros for the genesis block). |
ts | integer | When the block was sealed, ms. Block times only increase. |
txRoot | string | Merkle root of the transaction hashes, in order (below). |
count | integer | Number of transactions. |
txs | string[] | The transaction hashes, in the order they were applied. |
producer | string | Public key that sealed the block: the sequencer's (sequencer on /api/stats). |
hash | string | SHA-256 of the block header (below), hex. |
sig | string | Ed25519 signature by producer over the 32 bytes of hash, hex. |
Blocks are sealed by the sequencer from its mempool, usually as a transaction is submitted (the submit call waits for it), and otherwise by the sequencer's scheduled run.
Check a block yourself
Everything needed is in two public responses: the block, and chain and sequencer from /api/stats.
Rebuild the header hash
Make the header object
{ chain, height, prev, ts, txRoot, count, producer }, withchainfrom/api/stats. Write it as canonical JSON: keys sorted, no whitespace, standard JSON escaping. Its SHA-256, as lowercase hex, must equalhash.Rebuild the Merkle root
Start with the
txshashes in order. While more than one is left, pair them up: each pair becomes SHA-256 of the two hashes' bytes joined (32 + 32 bytes, not the hex text). An odd one out at the end of a level is paired with itself. The last hash left must equaltxRoot. A block with no transactions would have SHA-256 of the empty string as its root.Check the signature
producermust be thesequencerkey from/api/stats. Verifysigas an Ed25519 signature over the 32 bytes ofhash(decoded from hex), with that key.Follow the link
Fetch the block at
height - 1: itshashmust equal this block'sprev. Walking back to height0checks the whole chain.
// npm i @noble/curves @noble/hashes
import { ed25519 } from "@noble/curves/ed25519";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
const API = "https://mlx.app/api";
const canonical = (v) =>
v === null || typeof v !== "object"
? JSON.stringify(v)
: Array.isArray(v)
? "[" + v.map(canonical).join(",") + "]"
: "{" + Object.keys(v).filter((k) => v[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical(v[k])).join(",") + "}";
const hex = (data) => bytesToHex(sha256(typeof data === "string" ? utf8ToBytes(data) : data));
const merkleRoot = (hashes) => {
if (!hashes.length) return hex("");
let level = hashes;
while (level.length > 1) {
const next = [];
for (let i = 0; i < level.length; i += 2) next.push(hex(hexToBytes(level[i] + (level[i + 1] ?? level[i]))));
level = next;
}
return level[0];
};
const { chain, sequencer } = await (await fetch(`${API}/stats`)).json();
const { block } = await (await fetch(`${API}/block/1`)).json();
const { height, prev, ts, txRoot, count, producer } = block;
console.log("hash", hex(canonical({ chain, height, prev, ts, txRoot, count, producer })) === block.hash);
console.log("root", merkleRoot(block.txs) === txRoot);
console.log("sig ", producer === sequencer && ed25519.verify(hexToBytes(block.sig), hexToBytes(block.hash), hexToBytes(sequencer)));Each transaction in the block can be checked the same way: see Keys and signing. One exception: a Mac link (bind_device) is shown with the kind of Mac in place of its name, so its own hash can't be rebuilt from the public copy, though its hash still sits in txs and txRoot. For proof that a block is in the published log, and of an account's balances at a height, see Integrity.
Errors
| Status | When |
|---|---|
| 400 | /api/block/{height}: the height isn't a whole number ("Block height must be a whole number"). |
| 404 | /api/block/{height}: no block at that height yet ("Block not found"). |
| 500 | The ledger couldn't be read. |
/api/blocks never answers 400: an unknown sort or period falls back to the default, and an out-of-range limit is clamped.