Submitting
Send a signed transaction to POST /api/tx; the reply is its record, usually already in a block, and GET /api/tx/<hash> tells you where it stands later.
Send a transaction
https://mlx.app/api/txThe body is the signed transaction under tx. No key or session is needed: the signature is the authorization.
curl -X POST https://mlx.app/api/tx \
-H 'content-type: application/json' \
-d '{"tx":{"chain":"mlx-1","from":"luca.mlx","nonce":4,"ts":1793116800000,"pk":"7812b1435b317df04f45731ac508ae7774d4731ee0445fd46ccdcc5019faa524","body":{"type":"transfer","to":"shop.mlx","token":"USDC","amount":"2500000"},"sig":"ce8c7621b38777895b06107168e08a542bf9d5fbaf2ac53bb1b9a1b1d3ade2635d5c00150a2f76990b49330365d964c2b55d3289bbc9cc3e380550a2715cb10f"}}'How to build and sign tx: Keys and signing. The example above uses a fixed old ts, so production refuses it; sign with Date.now().
| Parameter | Type | Notes |
|---|---|---|
tx | object | The signed transaction: chain, from, nonce, ts, pk, body, sig. |
What happens
- Checks at the door. The server checks the signature, the size, the chain id, the timestamp, that the wallet exists and that
pkcontrols it, and that the nonce is ahead of the wallet's current one. A transaction that fails here is refused with an HTTP error and is not stored. - Mempool. The transaction is stored as
pendingunder its hash and queued. - Block. The server tries to seal a block at once and waits a few seconds for the result. If another request is sealing at the same moment, or the transaction waits for an earlier nonce, it stays
pendingand a later run includes it. - Reply. The record as it stands:
successorfailedonce in a block,pendingif the wait ran out first.
Sending the same signed transaction again is safe: it has the same hash, so the server answers from the stored record and never applies it twice. A record still pending well after it was received goes back into the mempool on a resend.
The reply
{
"hash": "2069d74fed709fd06538433002fa7e331c389f753709a62affc2179be2bf608d",
"tx": {
"body": {
"amount": "5000000",
"to": "luca.mlx",
"token": "USDC",
"type": "transfer"
},
"chain": "mlx-1",
"from": "review.mlx",
"nonce": 7,
"pk": "53fbe3a2e4ee9ee2ad907174562e8f2fc9b2bb3e1074f8ce274935439422c4ab",
"sig": "8c806f69381a9d190528ba2a15e42d6feadf0b184e4b77769fcb1c6413444d28be7196ab1bef8195bc16d904b030bb173dbcf24c8e1d2ed2b214564b28033e04",
"ts": 1793018575027
},
"status": "success",
"receivedAt": 1793018575432,
"meta": { "country": "SE" },
"effects": [{ "kind": "transfer", "token": "USDC", "from": "review.mlx", "to": "luca.mlx", "amount": "5000000" }],
"height": 347,
"index": 0
}GET /api/tx/<hash> returns the same record and adds blockHash and blockTs; review.mlx is a demo wallet.
| Field | Type | Notes |
|---|---|---|
hash | string | The transaction hash, its id everywhere. |
tx | object | The signed transaction as received. |
status | string | pending, success or failed. |
error | string | Why it failed (only when failed). |
height | integer | The block it is in (not while pending). |
index | integer | Its position in that block. |
effects | array | What it changed: kind (transfer, mint_node, reward, account, lock), token, from, to, amount, node, note, until. Empty when failed. |
receivedAt | integer | When the server first received it (ms). |
meta.country | string | Two-letter country of the sender's network, when known. |
sold | object | buy_node paid from the wallet balance only: the MLX sold for the seat, { mlx, usd } in base units. |
Statuses
| Status | Meaning | Nonce |
|---|---|---|
pending | Received and queued, not in a block yet. | Not used yet. |
success | Applied in block height. | The wallet's nonce is now this one. |
failed | Recorded in block height and changed nothing; error says why. | Not used: sign the next transaction with the same nonce. |
A transaction can be valid at the door and still fail in its block, for example when the balance it needs was spent by an earlier transaction in the same block. Such a failure is kept on the ledger for good, and shows in the wallet's history.
{
"hash": "8ea20332f8a534dc257e5f0f60d47a9f4815046c9114254db1492dabe4d69a0c",
"tx": {
"body": { "amount": "5000000", "to": "luca.mlx", "token": "USDC", "type": "transfer" },
"from": "review.mlx",
"nonce": 8
},
"status": "failed",
"error": "review.mlx has insufficient USDC",
"effects": [],
"height": 315,
"index": 0
}Nonces
- Use the wallet's current
nonceplus one. Read it fromGET /api/account/<name>(account.nonce). - At the door, a nonce must be above the wallet's current one and at most a small window ahead of it; older nonces are refused ("Bad nonce: expected N, got M").
- In a block, a wallet's transactions apply in nonce order, whatever order they arrived in.
- A transaction whose nonce is ahead of the next one waits in the mempool for its predecessor, for up to the hold time (
NONCE_HOLD_MSinlib/chain/ledger.ts). If the gap is still there after that, it is applied and fails with "Bad nonce". create_accountalways has nonce0, and system transactions are not held.
To send several transactions quickly, number them N+1, N+2, … and send them in any order. If one fails, every later one fails too ("Bad nonce") because the failed one didn't use its number: sign them again from the failed nonce.
Wait for inclusion
Most replies are already final. When the reply says pending, poll the record:
https://mlx.app/api/tx/2069d74fed709fd06538433002fa7e331c389f753709a62affc2179be2bf608d| Parameter | Type | Notes |
|---|---|---|
hash | path | The transaction hash (64 hex characters). |
It returns the record above plus blockHash and blockTs once it is in a block, and 404 {"error":"Transaction not found"} for a hash the ledger has never seen.
async function waitForTx(hash, { everyMs = 2000, tries = 60 } = {}) {
for (let i = 0; i < tries; i++) {
const r = await fetch(`https://mlx.app/api/tx/${hash}`);
if (r.ok) {
const rec = await r.json();
if (rec.status !== "pending") return rec;
}
await new Promise((ok) => setTimeout(ok, everyMs));
}
throw new Error("still pending");
}If a transaction stays pending, send the same signed transaction again: that puts it back into the mempool without any risk of applying it twice. To prove a block is in the ledger's history, fetch its inclusion proof.
Limits
| Limit | Value | Reply |
|---|---|---|
| From one IP address, on each server instance | 40 a minute | 429 "Too many transactions. Wait a minute and try again." |
| From one IP address, across the network | 60 a minute | 429, the same message |
| From one wallet | 30 a minute | 429 "Too many transactions from this wallet. Wait a minute and try again." |
| Failed transactions from one wallet | 20 in an hour | 429 "Too many failed transactions from this wallet. Try again in an hour." |
| New wallets from one IP address | 10 in an hour | 429 "Too many new wallets from this network. Try again in an hour." |
Only new transactions that fail in a block count toward the failed limit; resending one already on record doesn't. A transaction also has a maximum size ("Transaction too large"), far above any normal transfer. A body that isn't well formed (an unknown type, an amount that isn't a whole number of base units, a field that isn't a plain value) is refused at the door with 400 and the same message a block would give it, so it's never stored. See Rate limits.
Errors
Errors are JSON { "error": "<message>" }. A transaction refused at the door is not stored and uses no nonce.
| Status | When | Example message |
|---|---|---|
400 | Body isn't JSON | Body must be JSON: { tx } |
400 | tx is missing fields, or body.type is missing | Malformed transaction |
400 | A body type a wallet can't sign, or a field that isn't a plain value | Unknown transaction type, Malformed transaction |
400 | amount, amountIn, minOut or maxMlx isn't a whole number of base units | Amount must be a positive integer string in base units |
400 | Too large | Transaction too large |
401 | Not signed by the wallet's own key (the signature doesn't check, pk isn't the wallet's, or the wallet doesn't exist), checked before any per-wallet limit is counted | This transaction isn't signed by luca.mlx's key. |
400 | Another network's chain id | Wrong chain: expected mlx-1 |
400 | ts too far from the server's clock | Transaction timestamp is too far from network time |
400 | Nonce too old or too far ahead | Bad nonce: expected 5, got 9 |
400 | create_account: name rules, nonce, key, a name or key already on record | That name is reserved., New accounts start at nonce 0, That public key can't hold a wallet, luca.mlx is already taken, This key already controls an account |
400 | withdraw to a Solana token account instead of a wallet address | That's a token account's address, not a wallet's. … |
429 | A limit above | see Limits |
Everything else (balances, locks, seat rules, link requests) is checked in the block and comes back as status: "failed" with error. The messages per type are on Transaction types.