State commitments
Once an hour the sequencer commits to every account's balances in a Merkle sum tree whose root also carries the total USDC the network owes, and any wallet can check its own balance is counted in it.
What is committed
At most once an hour, and only after new blocks, the sequencer takes every account (system accounts and wallets), sorts them by name and builds a Merkle sum tree over them (commitState in lib/chain/integrity.ts). The tree has the same shape as the log; the difference is that every node also carries the USDC below it, so the root commits to the total.
Each leaf is one account:
| Field | Type | Notes |
|---|---|---|
n | string | The account name. |
nonce | integer | Its nonce. |
usdc | string | USDC balance, base units. |
usdt | string | Older USDT balance, base units. |
mlx | string | Unlocked MLX, base units. |
locked | string | MLX still locked at the time of the commitment, base units. |
What the network owes an account is usdc + usdt (both are dollars 1:1).
Hashing rules
lib/chain/tlog.ts, with every sum written as 16 bytes, big-endian:
| Sum | Hash | |
|---|---|---|
| Leaf | usdc + usdt | SHA-256(0x00 ‖ sum ‖ leaf JSON) |
| Inner node | left sum + right sum | SHA-256(0x01 ‖ left hash ‖ right hash ‖ sum) |
The leaf JSON is the leaf's fields with keys in alphabetical order and no whitespace, the numbers as strings except nonce:
{"locked":"0","mlx":"1499995840385794","n":"treasury.mlx","nonce":0,"usdc":"1677000000","usdt":"0"}The root's sum is the commitment's usdc: the total USDC (including older USDT) every account on MLX Network holds.
The signed statement
The sequencer signs each commitment with its Ed25519 key, over this text (commitStatement), one item per line, each line ending in \n:
mlx-state/v1
mlx-1
height 397
state e61dd19f5dffb8ba9b465106faa6a7a2928cfd2bbabd7ef7af13d39d39c5448d
usdc 11285277511
accounts 68
log 398 cb97aebd459faf519f4d967d5b6a98fafff2df0f6a681f4bf89d34560b253d7eheight is the block the balances are after, accounts the number of leaves, and log the log size and root at that block. That last line ties the balances to the history: a consistency proof from that size to any later checkpoint shows they belong to the same log.
Check one wallet
https://mlx.app/api/log/state/treasury.mlxThe account's leaf in a state commitment and the sibling path from the leaf to the root.
| Parameter | Type | Notes |
|---|---|---|
name | path | The account, for example luca.mlx. |
height | integer | Optional. The commitment made after this block (heights are listed in history of GET /api/log); default: the newest. |
{
"commit": {
"height": 397,
"root": "e61dd19f5dffb8ba9b465106faa6a7a2928cfd2bbabd7ef7af13d39d39c5448d",
"usdc": "11285277511",
"accounts": 68,
"size": 398,
"logRoot": "cb97aebd459faf519f4d967d5b6a98fafff2df0f6a681f4bf89d34560b253d7e",
"ts": 1793115783839,
"sig": "78fc7df1a95d357b45aecb195a95c216600cd95f48391f9a604a05e1e89c516c0da09883bd70e5d787d97d11b3eceb11d7a5a9f94fea4b6f0cf39eb869d04704"
},
"leaf": { "n": "treasury.mlx", "nonce": 0, "usdc": "1677000000", "usdt": "0", "mlx": "1499995840385794", "locked": "0" },
"index": 57,
"count": 68,
"path": [
{ "h": "6967c23af794d67ebc045bed87802e3d27c748b55023d396ef07601b235d6d0e", "s": "2000000" },
{ "h": "31b98b772c654e3358947d062d2f7a40745c04e12b56345df9eba4c279e448e0", "s": "0" },
{ "h": "ee827ef10ea9a0bd3d39a8b278d612b426b5b13ceb805f7ebde6e4ffa0cf0ce8", "s": "0" },
{ "h": "1cb09f7643ecdf3c129e22a924b7f255d90011ed30bd653264e646b6233d2458", "s": "116929" },
{ "h": "dd2fbd52953500bb53c8dc1927a00d6e5c4e65ca32c4339ec55933609a51cd47", "s": "50000000" },
{ "h": "b4907a105ef58c08c92a08707be44ae138a61c2d62923a9f3b66fd29f55d9dcf", "s": "9296000000" },
{ "h": "940fe1cee0286e8df785f55070738a31051196d8bc78a00977d24b340a0f3789", "s": "260160582" }
]
}| Field | Type | Notes |
|---|---|---|
commit | object | The commitment: height, root (hex), usdc (total, base units), accounts, size and logRoot (the log at that block), ts, sig (Ed25519 over the statement above). |
leaf | object | The account's leaf. |
index | integer | The leaf's position (accounts sorted by name). |
count | integer | The number of leaves. |
path | array | The siblings from the leaf up to the root, each with its hash h and its USDC sum s. |
Other answers, all 200 with commit:
"absent": true: no account of that name was in the commitment."missing": true: the commitment exists, but the leaves behind it are no longer kept (they are kept for a few days), so no path can be given.
404 No state commitment yet when there is no commitment, or none at the height asked for.
Verify a balance in JavaScript
import { ed25519 } from "@noble/curves/ed25519";
import { sha256 } from "@noble/hashes/sha2";
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
// 16-byte big-endian sum
const u128 = (v) => {
const out = new Uint8Array(16);
for (let i = 15; i >= 0; i--) { out[i] = Number(v & 0xffn); v >>= 8n; }
return out;
};
// The leaf's JSON, keys in this exact order
const leafJson = (l) =>
`{"locked":${JSON.stringify(l.locked)},"mlx":${JSON.stringify(l.mlx)},"n":${JSON.stringify(l.n)},` +
`"nonce":${l.nonce},"usdc":${JSON.stringify(l.usdc)},"usdt":${JSON.stringify(l.usdt)}}`;
const splitPoint = (n) => { let k = 1; while (k * 2 < n) k *= 2; return k; };
function verifyBalance({ commit, leaf, index, count, path }) {
let sum = BigInt(leaf.usdc) + BigInt(leaf.usdt);
let hash = sha256(concatBytes(Uint8Array.of(0x00), u128(sum), utf8ToBytes(leafJson(leaf))));
// which side the sibling is on at each level, found by replaying the tree's splits from the top
const right = [];
for (let lo = 0, hi = count, m = index; hi - lo > 1; ) {
const k = splitPoint(hi - lo);
if (m < k) { right.push(true); hi = lo + k; } else { right.push(false); lo += k; m -= k; }
}
if (right.length !== path.length) return false;
right.reverse(); // the path runs bottom up
path.forEach((step, i) => {
const s = BigInt(step.s);
if (s < 0n) throw new Error("negative sum");
sum += s;
const [l, r] = right[i] ? [hash, hexToBytes(step.h)] : [hexToBytes(step.h), hash];
hash = sha256(concatBytes(Uint8Array.of(0x01), l, r, u128(sum)));
});
return bytesToHex(hash) === commit.root && sum.toString() === commit.usdc;
}
const commitStatement = (c, chain) =>
`mlx-state/v1\n${chain}\nheight ${c.height}\nstate ${c.root}\nusdc ${c.usdc}\naccounts ${c.accounts}\nlog ${c.size} ${c.logRoot}\n`;
const { chain, sequencer } = await (await fetch("https://mlx.app/api/log")).json();
const proof = await (await fetch("https://mlx.app/api/log/state/treasury.mlx")).json();
console.log("balance in the root:", verifyBalance(proof));
console.log("commitment signed:", ed25519.verify(hexToBytes(proof.commit.sig), utf8ToBytes(commitStatement(proof.commit, chain)), hexToBytes(sequencer)));balance in the root: true
commitment signed: trueReplace treasury.mlx with your wallet. Also check that leaf shows the balances you expect: the proof shows that these balances are in the total, not that they are right.
Because every inner node carries a sum and no sum can be negative, a balance can't be hidden or counted twice without changing the root or the total. The explorer's Integrity page runs the same balance check for any name you type, and the MLX Wallet runs it for the open wallet ("Verified · block N").
The total against the reserves
The commitment's usdc is everything the network owes in dollars. Compare it with the reserves on mlx.app/reserves. USDC enters the ledger only through system transactions anyone can read: deposits and seat payments carry their on-chain payment, and every issue names the reserve transfer behind it (system transactions).