<!-- Minima AI skill v1.0.0 — single-file edition.
     This is the SKILL.md plus every reference file concatenated into one document,
     for tools that take a single rules/instructions/knowledge file (Cursor, Windsurf,
     Copilot, ChatGPT, Gemini, ...). In the routing table below, each 'references/NAME.md'
     link corresponds to a '## reference: NAME' section further down this file.
     Install help: see INSTALL.md or https://docs.minima.global . MIT-licensed. -->

---
name: minima
description: "Use when building on or operating the Minima blockchain — writing KISS VM scripts and covenants, building MiniDapps (MDS), constructing manual UTXO transactions, creating tokens/NFTs, setting up or restoring a node, Maxima messaging, integrating a native Android app, or diagnosing a failed/stuck transaction. An on-chain-proven reference with the fund-critical gotchas up front; routes to per-domain reference files."
---

# Minima

Minima is a **complete, decentralized blockchain that runs as a full constructing-and-validating node on a phone**. There are no miners and no staking: every user runs a node, and every transaction carries its own small proof-of-work (**TxPoW** — transaction and proof unified). Old blocks compress into a **cascading chain**, and coins live in a **Merkle Mountain Range (MMR)** proof database so a node can validate spends without storing the whole chain. The native coin is MINIMA (tokenid `0x00`); anyone can issue coloured tokens and NFTs.

On top of the base chain sit three things this skill covers in depth: **KISS VM**, a deliberately simple, loop-free on-chain scripting language that guards coins (Minima's "smart contracts" are spending **covenants**); **MDS (the MiniDapp System)**, an app runtime built into every node that serves web MiniDapps and exposes a `MDS.cmd`/SQL/comms API; and **Maxima**, an off-chain end-to-end-encrypted messaging layer that runs over the node network.

Everything in this skill is grounded in on-chain-proven experience (node v1.0.47–1.0.48 era) plus the official docs. Minima is **quantum-safe** because it signs with **WOTS+ one-time keys** — and that single fact is the root of most of the fund-losing traps below, so read the rules before you write anything that signs or holds funds.

---

## CRITICAL RULES

These are the mistakes that lose or lock real funds. `★` = has caused on-chain fund loss; `★★` = permanent, unrecoverable loss. Load the linked reference for the full treatment.

### Writing scripts (KISS VM)
1. **No underscores in KISS variable names** — the parser fails silently. Keep names short (script size is tight — see rule 2).
2. **Scripts silently fail over ~1200 characters** (hard VM limits: 1024 instructions, 64 stack depth, 32 params, 0–255 state ports). Minify names and move rarely-used branches into **MAST** cold paths.
3. **Time with `@COINAGE` against a stored duration, not `@BLOCK`** (older docs write the global as `@BLKNUM`) — absolute block-height checks are unreliable across synced nodes. Note `txncheck` cannot evaluate `@BLOCK`/`@BLOCKMILLI`/`@COINAGE`.
4. **`AND OR XOR NAND NOR` are BOOLEAN, not bitwise** (the symbol operators `& | ^ ~ << >>` are the bitwise ones, and require HEX operands). **`NUMBER()` overflows on 32-byte hex** — take a slice first, e.g. `SUBSET(0 4 h)` (SUBSET is `(start end hexdata)`).
5. **Match keepstate to intent:** `VERIFYOUT(... TRUE)` + `txnoutput ... storestate:true` for phase transitions that must carry state forward; `(... FALSE)` + `storestate:false` for final payouts. A mismatched pair is silently rejected.
6. ★ **Cardinal covenant rule: every reachable branch must pin its outputs with `VERIFYOUT`.** An unpinned `SIGNEDBY(...) RETURN TRUE`-style path on a fund-holding coin let ~5,000 MINIMA be drained on-chain — the MDS permission model shares wallet-key access across all WRITE apps, so "only the owner can call it" is not a defence. → `references/contracts-covenants.md`

### Funding & posting script addresses
7. ★★ **Prove `parseok` BEFORE funding any script address.** Run the exact bytes the node will store through `runscript`/`newscript` and require `parseok:true`. A coin at an unparseable-script address is **permanently unspendable** — no key, resync, or node rebuild recovers it. A literal containing `/` that Java's `JSONObject.quote` escaped to `\/` cost ~200,000 MINIMA + tokens on mainnet. Python's `json.dumps` does *not* escape `/`, so test the **real** code path, not an RPC/Python harness. → `references/diagnostics-recovery.md`
8. ★ **Diagnose from data, not guesses.** When a covenant spend fails, FIRST do `scripts address:` → `runscript` the exact bytes → check `parseok`, before touching keys, keyuses, or node state. The local `keys` "uses" counter is unrelated to why a spend is rejected.
9. ★ **`txnpost status:true` ≠ on-chain success** — it means accepted to the mempool only. Invalid transactions (e.g. a covenant Script-FAIL, or a state-stripping spend) "succeed" then silently never mine. Confirm from chain state (`coins`/`history`), never from the post result.
10. **`txncheck`: the verdict is boolean `valid.scripts`**; the top-level `scripts` is a COUNT of distinct input scripts. Gate a post on `valid.scripts && validamounts && valid.mmrproofs`, and use a `truthy()` helper — these flags come back inconsistently as bool / int / string.
11. ★ **Owner-key auto-sign trap:** funding an anyone-can-spend covenant transaction from a coin at your own owner address makes `txnsign publickey:auto` add the owner signature → the covenant takes its owner branch. Exclude owner/pool addresses from funding and change selection.
12. **Post sequence is `txnsign → txnbasics → txnpost`** — sign BEFORE basics, never combined; never `txnpost auto:true` for script-address coins; never combine `scriptmmr:true` with `txnbasics`; always `txndelete` on any error path or the input coins stay locked. → `references/transactions-utxo.md`
13. **Canonicalize any literal baked into a covenant** and also stored for re-derivation (`stripTrailingZeros().toPlainString()`) — a one-character difference yields a different script hash, a different address, and an "invisible" empty pool.

### Amounts & tokens
14. ★ **A token coin's real value is `tokenamount`; `amount` is coloured dust (~1e-37).** `c.amount || c.tokenamount` is a BUG (amount is always present). MINIMA (`0x00`) has no `tokenamount` — its `amount` is the value.
15. ★ **Every token amount is FLOORED to the token grain (10^-decimals).** When a covenant pins an output, quantize: reserves round UP, proceeds/change DOWN. MINIMA is full 44-dp. Tokens can never be burned (per-tokenid in==out); only MINIMA can.
16. **State is per-TRANSACTION, not per-output**, and is a LIST of `{port,type,data}` (iterate — never a map); set unused ports to 0. The TxPoW hard cap is 64 KB ≈ 3 token-carrying outputs + 1 signature per transaction.
17. **`history` `difference` nets ALL tracked coins**, including imported anyone-can-spend covenants — recompute wallet nets over your own `simple:true` addresses.

### Keys, restore & node state
18. ★★ **WOTS signatures are ONE-TIME.** On any restore/resync, set `keyuses` HIGHER than the true prior signature count — the node regenerates keys at `uses=0` and cannot know how many leaves were spent. Too low → it re-issues spent leaves → the key becomes recoverable and funds are stealable. → `references/key-security-wots.md`
19. **Seed restore / fast sync = `megammrsync action:resync`** (seconds), NEVER `archive action:resync` (hours). → `references/node-operations.md`
20. **An untracked coin is unspendable even when visible** ("Script Missing from TxPoW"): `newscript trackall:true` → `coinexport coinid:<FULL 66-char id>` (a truncated id gives a false "Coin not found") → `coinimport track:true`, then spend. Rebuild a token from the byte-exact `coinexport` blob — re-serializing the token JSON reorders keys → wrong tokenid → rejected.
21. **`getaddress` rotates through the node's 64 permanent keys** — a different one each call. Persist ONE chosen identity; test "is this coin mine" against all 64 keys from `keys`. In covenants, take addresses from `PREVSTATE`, never `getaddress` at runtime.

### Apps & runtime
22. **`txnpost` mines asynchronously** — `istransaction:false` at return is NOT failure; retrying on it causes a re-post storm that races itself and stalls confirmation.
23. ★ **Bound every heavy query.** `coins` has no count cap (only `depth:`); `history` returns full ~14 KB txpow bodies. Three distinct size limits exist: the node RPC's 256 KB "too long" stub (catchable — comes back EMPTY, not an error), an Android app-side ~256 KB receiver cap, and the native Binder ~1 MB (an UNCATCHABLE process kill). Use adaptive paging (`max:8`→4→2→1). → `references/native-android-ipc.md`
24. **`service.js` is ES5-only** (Nashorn/Rhino: no `let`/`const`, arrow functions, `async`/`await`, `Promise`, `Set`, template literals) — any of them silently prevents the daemon from starting. Page and service share ONLY SQL; SQL columns return UPPERCASE; H2 `varchar(1024)` silently truncates (use `text`); validate every `MDS.cmd` hop; `newaddress` keys regenerate asynchronously after a restore. → `references/mds-development.md`

---

## Which reference to load

| If you are… | Load |
|---|---|
| Understanding how Minima works — TxPoW, cascade, MMR, consensus, tokens, node types | `references/protocol-architecture.md` |
| Writing or reading any KISS script — grammar, functions, globals, limits, MAST, testing | `references/kissvm-language.md` |
| Designing a covenant — multi-phase state machine, AMM, HTLC swap, NFT, orderbook DEX, escrow, payment channel | `references/contracts-covenants.md` |
| Building or posting a manual transaction; creating tokens; reading coin JSON and amounts | `references/transactions-utxo.md` |
| Looking up a wallet / coin / token / txn / script / crypto command's syntax | `references/commands-wallet-transactions.md` |
| Looking up a node / network / backup / archive / megammr / maxima command's syntax | `references/commands-node-network.md` |
| Building a MiniDapp — mds.js, events, SQL, file ops, comms, `service.js`, TS/React, production gotchas | `references/mds-development.md` |
| Installing, running, restoring, resyncing, or hosting a node; private testnet; MEG | `references/node-operations.md` |
| Anything touching seed phrases, keyuses, signing, or key reuse | `references/key-security-wots.md` |
| Integrating a native (non-MDS) Android app over broadcast-Intent IPC | `references/native-android-ipc.md` |
| Doing off-chain, encrypted messaging between nodes | `references/maxima.md` |
| Packaging and shipping a MiniDapp — `.mds.zip`, `dapp.conf`, app stores, sandbox, IPFS | `references/distribution-packaging.md` |
| Mapping the ecosystem, querying a block explorer, or testing scripts offline (minima-vm) | `references/ecosystem-tooling.md` |
| A transaction failed, coins look stuck, a covenant won't spend, or a node misbehaves | `references/diagnostics-recovery.md` |

---

## Quickstart

Run a Minima node (Java 17+; the node is one jar):

```bash
# Public mainnet node with RPC and the MDS app runtime enabled
java -jar minima.jar -mdsenable -rpcenable
#   P2P/Minima on 9001, MDS web UI on 9003 (base+2, https, self-signed cert), RPC on 9005
```

Isolated private chain for development (no peers, instant genesis, 1-billion-MINIMA faucet):

```bash
java -jar minima.jar -data ~/mdev -nop2p -test -genesis
#   add -solo to auto-mine ~1 block / 15s — poll `balance`; do NOT self-send to advance the chain
```

Then drive it from the terminal / RPC / MDS (all three run the same commands):

```bash
runscript script:"RETURN SIGNEDBY(0xFF)"        # try a KISS script (parseok/clean check)
newaddress                                       # a fresh receive address
balance                                          # what the node holds
```

First MiniDapp: a `.mds.zip` needs `dapp.conf` (name/version/permission) as the FIRST zip entry, plus `index.html` and optionally a `service.js` daemon — see `references/mds-development.md` and `references/distribution-packaging.md`.

---

## Facts that supersede older docs

Some circulating notes are stale. Where they conflict, these win:

- **The 256 KB response limit is app/RPC-side, not the core jar.** The node RPC has a catchable "results too long" stub (returns empty); native Android has a separate app-side receiver cap; the only uncatchable one is the Android Binder (~1 MB). A fork lifts the app-side cap via a `content://` FileProvider hand-off.
- **The multi-phase "casino" contract is a PATTERN reference, not a shipped product.** The live productization of that style is a prediction-market / wager covenant.
- **A block explorer's indexer height can run ahead of canonical chain height** (observed ~20k blocks higher) — treat explorer height as advisory and trust your own node's `block`.
- **There are two different "explorers":** the hosted one at `explorer.minima.global` has a tRPC backend (a plain WebFetch of its `/transactions/<hash>` pages false-negatives because it is a client-hydrated SPA — use the tRPC endpoint or the node's own `txpow` command); a "simple explorer" is purely client-side against the visitor's own node and has no queryable backend.

---

*Shareable under MIT. Sources: the official Minima docs (docs.minima.global) and original on-chain research and incident analysis.*


---

## reference: commands-node-network
<!-- (was references/commands-node-network.md) -->

# Minima Command Reference — Part 2: Node, Network, Backup, Maxima & System

Command reference for the node/network/backup/archive/Maxima/system command surface.
Wallet, coin, token, transaction, script and crypto commands are in
`commands-wallet-transactions.md` (Part 1).

Conventions used throughout:

- `[param:]` = required, `(param:)` = optional (same notation as `help`).
- Every command returns a JSON envelope: `{"command": ..., "status": true|false, "pending": false, "response": {...}}`. `status:false` carries an `error` string. `pending:true` means an MDS MiniDapp in READ mode queued the command for user approval (see `checkpending`).
- Over HTTP RPC the command is the URL path: `curl 127.0.0.1:9005/status` (the default RPC port is the base port + 4, i.e. `9005` for a default `9001` install; RPC is **off** by default — see `rpc`).
- Multiple commands can be chained with `;`.

> ⚠️ **Global transport hazard — the 256 KB response cap.** The node refuses to return any single command response larger than 256,000 bytes ("`results too long! max ( 256000 )`"). Over RPC/MDS the oversized reply comes back **empty**, not as an error object, so callers silently get no data. Commands that return full TxPoW bodies (`history`, `txpow`, deep `printtree`) hit this easily. Pattern that works: **adaptive paging** — start with a small page (`max:8`), and on any empty/over-limit reply halve the page size (8 → 4 → 2 → 1) and retry the same offset. Never assume a fixed page size is safe.

---

## Node status & chain

### status

```
status (clean:true)
```

General node status: version, uptime, memory, chain info, stored TxPoW units, network and P2P connections, traffic. `clean:true` additionally clears RAM.

Key response fields: `version`, `uptime`, `locked` (true = Vault password-locked), `length`, `weight`, `minima` (total supply), `coins`, `data` (data-folder path), `memory{ram, disk, files}`, `chain{block, time, hash, speed, difficulty, size, length, weight, branches, cascade{start, length}}`, `txpow{mempool, ramdb, txpowdb, archivedb}`, `network{...connections, traffic...}`.

```
status
→ response.chain.block      current top block height
  response.locked           is the vault password-locked
  response.network          peer/connection details
```

This is the cheapest "is the node alive and synced" probe — poll `status` (or `block`), never `history`.

### block

```
block
```

Return just the current top block. Response fields: `block`, `hash`, `timemilli`, `date`. Much lighter than `status` when you only need the height.

```
block
→ {"block":"850123","hash":"0x0000..","timemilli":"1722..","date":"..."}
```

### history

```
history (action:list|size|customsize|transactions) (max:) (offset:) (depth:) (relevant:) (where:)
```

Return TxPoW relevant to you (default `action:list`, default `max` 100 — can be slow).

- `action:size` — count of your transactions (`relevant:false` counts all).
- `action:transactions` — how many transactions in chain over `depth:` blocks.
- `action:customsize where:"..."` — SQL WHERE clause against the TxPoWDB, e.g. `where:"isblock=1 AND timemilli>1728037509020"`.
- `relevant:` — your transactions (default true) or all.

Each list entry is a **full TxPoW**: `txpows[].body.txn.inputs[]/outputs[]` (with `coinid`, `address`, `miniaddress`, `amount`, `tokenid`), `header.block`, `header.timemilli` — enough to classify sent/received/self client-side.

```
history max:20 offset:45
history action:size relevant:false
```

> ⚠️ **IPC/RPC hazard — the single most dangerous command for transports.** `history action:list` always returns FULL TxPoW bodies (~14 KB each, much more for contract-heavy transactions; there is no "lite" mode). The default `max:100` easily exceeds 1 MB on an active wallet, and even `max:25` can exceed the node's 256 KB response cap, in which case the reply comes back empty. Use small adaptive pages (`max:8`, halve on empty, page with `offset:`), fetch only on demand (e.g. when a history view is visible), and never call `history` in a per-block loop.

### txpow

```
txpow (txpowid:) (onchain:) (block:) (address:) (relevant:) (max:)
```

Search for a specific TxPoW in the unpruned chain or your mempool.

- `txpowid:` — return the full TxPoW details.
- `onchain:` — check a txpowid is on chain; returns block info and confirmations.
- `block:` — return the block TxPoW at that height (header + `body.txnlist` of transaction txpowids).
- `address:` — TxPoWs containing this 0x/Mx address.
- `relevant:true`, `max:` (default 100) — restrict/paginate.

```
txpow txpowid:0x000..
txpow block:200
txpow onchain:0x000..
```

> ⚠️ **Hazard — the unpruned window.** `txpow address:` (and `block:`/`onchain:`) only search the **unpruned** chain — roughly the last ~13,000 blocks from tip. An empty result does NOT mean "no history", just "nothing recent". Older data needs an archive node — and even there, pruned archive blocks keep only the block-level TxPoW with its `txnlist`: the per-transaction `body.txn.inputs`/`outputs` come back as `[]` (that data is gone from the archive format itself). Coin creation/spend history survives via the MMR — use `archive action:addresscheck` for old activity.

### printtree

```
printtree (depth:) (cascade:true|false)
```

Print a text-art tree of the blockchain. Default depth 32 blocks from tip; `cascade:true` also shows the cascading chain.

```
printtree depth:500
```

> ⚠️ Large `depth:` values produce very large text output — over a size-capped RPC/IPC transport this can hit the 256 KB cap and return empty. A terminal diagnostic, not an API.

### checkmode

```
checkmode
```

Show whether the calling MiniDapp is in READ or WRITE mode (MDS context). Useful for a dapp to know up front whether writes will go to the pending queue.

```
checkmode
```

### checkpending

```
checkpending uid:0xFF..
```

Check whether a pending-command UID (returned when a READ-mode MiniDapp issues a write command) is still in the pending list awaiting user accept/deny.

```
checkpending uid:0xFF..
```

---

## Network

### network

```
network (action:list|reset|recalculateip)
```

Show network status. `list` (default) lists your direct peers with their `uid`, host/port, direction and connection state — the `uid` here is what `disconnect` and `message` take. `reset` restarts the traffic counter; `recalculateip` re-detects your IP (e.g. after changing WiFi).

```
network action:list
```

### connect

```
connect [host:ip:port]
```

Connect to another Minima node — to join the main network or to build a private test net.

```
connect host:spartacusrex.com:9001
```

### disconnect

```
disconnect [uid:uid|all]
```

Disconnect from one connected/connecting host (uid from `network`) or all of them.

```
disconnect uid:CVNPMLPOCQ0HQ
disconnect uid:all
```

### message

```
message [data:message] (uid:uid)
```

Send a plain string message over the P2P network to one direct peer (or all peers if `uid:` is omitted). This is **node-to-peer** messaging — for user-to-user messaging use `maxima action:send` (see the Maxima section).

```
message data:"hello" uid:CVNPMLPOCQ0HQ
```

### rpc

```
rpc (enable:) (ssl:) (password:) (action:adduser|removeuser|listusers) (username:) (mode:read|write)
```

Enable/disable the HTTP RPC server (default **off**; port = base port + 4, i.e. `9005` on a default install). Default user `minima` has write access; you can add read-only users.

```
rpc enable:true ssl:true password:minimarpcpassword
rpc action:adduser username:rpcuser password:rpcpassword mode:read
```

> ⚠️ The Basic-Auth password travels in headers and is **only secure when combined with SSL** (`ssl:true` for self-signed, or your own stunnel/reverse proxy). Always firewall the RPC port; never expose it raw to the internet.

### webhooks

```
webhooks (action:list|add|remove|clear) (hook:url) (filter:)
```

Register URLs that receive Minima events as HTTP POSTs as they happen (NEWBLOCK, NEWBALANCE, MINING, ...). `filter:` restricts which events get posted.

```
webhooks action:add hook:http://127.0.0.1/myapi.php filter:MINING
```

### p2pstate

```
p2pstate
```

Print full details of the internal P2P state: in/out connections and total known peers. (There is no command literally named `p2p` — the P2P surface is `p2pstate` + `peers`.)

```
p2pstate
```

### peers

```
peers (action:list|addpeers) (peerslist:)
```

Print or extend the peers list (P2P must be enabled). `action:addpeers` takes a CSV `peerslist:ip:port,ip:port,..`. Returns `peers-list`, `havepeers`, `p2penabled`.

```
peers action:addpeers peerslist:spartacusrex.com:9001
```

### nodecount

```
nodecount (file:) (complete:false|true)
```

Enumerate the network by recursively pinging peers starting from the default node list, to count all participating nodes. `file:` writes a CSV; `complete:true` does a full crawl.

```
nodecount
```

> ⚠️ This is a network **crawl** — it pings peer after peer and can take a long time and generate real traffic. Run it deliberately from a terminal; never poll it or call it from an app loop.

### healthcheck

```
healthcheck
```

Sanity-check that chain, cascade and Maxima all add up. Response: `chain{tip, root, chainlength}`, `cascade{tip, tipcorrect}` (`tipcorrect:true` = cascade tip meets the root of the TxPoW tree), `maxima{hosts, contacts}`.

```
healthcheck
```

---

## Backup & restore

### backup

```
backup (password:) (file:) (auto:) (maxhistory:)
```

Back up the node. Uses a timestamped filename in the Minima data folder by default. `password:` encrypts it (letters/numbers only); `auto:true` schedules an unencrypted backup every 24 h; `maxhistory:` caps how many relevant TxPoW are included.

```
backup password:Longsecurepassword456 file:my-backup-01-Jan-22.bak
backup auto:true
```

### restore

```
restore [file:] (password:)
```

Restore the node from a backup. Wipes and replaces current node state. You MUST wait until all the node's original keys have been created before this is allowed.

```
restore file:my-full-backup-01-Jan-22 password:Longsecurepassword456
```

### restoresync

```
restoresync [file:] (password:) (host:) (keyuses:)
```

Restore from a backup **and** sync to the top block from an archive node — use when the backup is old (falls back to a plain `restore` if the backup is under 2 days old). `keyuses:` *increments* (not sets) the per-key use counter.

```
restoresync file:my-full-backup-01-Jan-22 password:Longsecurepassword456 host:<archive-host>:9001
```

> ⚠️ **IPC hazard.** `restoresync` (like `archive action:resync` and `megammrsync`) opens a raw socket to the host synchronously inside the command. On embedded/IPC transports that forbid network on the calling thread (notably Android's broadcast-Intent IPC), the socket call is killed and the internal catch-all swallows it — you get a **misleading `"Could not connect to Archive host!"` even though the host is fine and was never contacted**. Run these commands from the node's own terminal or HTTP RPC on desktop.

### checkrestore

```
checkrestore
```

Check whether the node is currently mid-restore. Poll this (not `status`) after kicking off a restore; response says if restoring is complete and whether a shutdown is pending.

```
checkrestore
```

### decryptbackup

```
decryptbackup [file:] (password:) (output:)
```

Decrypt an encrypted backup file to a plain one without restoring it.

```
decryptbackup file:my-full-backup-01-Jan-22 password:Longsecurepassword456
```

### reset

```
reset [archivefile:] [action:chainsync|seedsync|restore] (file:) (password:) (phrase:) (keys:) (keyuses:)
```

Reset the entire system from an archive export file:

- `chainsync` — re-sync all blocks from the archive file to get on the right chain; keys unchanged, no seed phrase needed.
- `seedsync` — wipe the wallet, regenerate keys from `phrase:"24 WORDS.."`, restore coins.
- `restore` — restore a backup (`file:`/`password:`) then re-sync the whole chain from the archive file.

```
reset archivefile:archiveexport-jul23.gz action:seedsync keyuses:1000 phrase:"ENTER 24 WORDS HERE"
```

> ⚠️ Minima signatures are **stateful**: every time you seed-resync you must set `keyuses:` *higher* than any previous value (default 1000, max 262144 per key), or you risk reusing signature slots.

### vault

```
vault (action:seed|wipekeys|restorekeys|passwordlock|passwordunlock) (seed:) (phrase:) (password:)
```

BE CAREFUL. Manage your private keys:

- `seed` (default) — show your seed phrase and seed. Never share these.
- `wipekeys seed:0x..` — delete private keys from this node (public keys remain) — for turning an online node into a watch-only node.
- `restorekeys phrase:"24 WORDS.."` — reinstate private keys.
- `passwordlock` / `passwordunlock` — encrypt/decrypt private keys with a password.

```
vault action:passwordlock password:your_password confirm:your_password
```

> ⚠️ While the vault is locked, `status` shows `locked:true` and anything needing the seed fails (e.g. `seedrandom` throws "DB locked!"); signing commands accept a one-shot `password:` parameter that unlocks, signs, and relocks. Ensure you have a secure record of the passphrase BEFORE locking — there is no recovery without it.

### archive

```
archive [action:] (host:) (file:) (phrase:) (anyphrase:) (keys:) (keyuses:) (address:) (statecheck:) (logs:) (maxexport:)
```

Archive-node operations — chain/seed re-sync and archive-db inspection:

- `integrity` — check your archive db integrity (no host needed).
- `inspect file:` — inspect an archive export (`last:1` means it can resync any node from genesis).
- `export` / `exportraw` — export archive db to `.gzip` / raw `.dat` (raw recommended).
- `resync host:` — chain re-sync (no phrase; wallet untouched) or seed re-sync (`phrase:"24 WORDS.."` — wipes wallet, restores coins) from an archive node.
- `import file:` — same, from an export file.
- `addresscheck address:` — search the archive for spent/unspent coins at an address; add `statecheck:` (a pubkey/address in the coin state) to find coins locked in a contract for a particular user.

```
archive action:resync host:<archive-host>:9001
archive action:addresscheck address:0xFED.. statecheck:0xABC..
```

> ⚠️ **Hazards (several, all verified in practice):**
> - `archive action:integrity` takes **3+ minutes** — run once, NEVER poll it in a loop.
> - `addresscheck` with no `statecheck:` on a busy contract address frequently runs 300 s+ and times out with no result.
> - **Concurrent archive queries on one node can OOM the JVM**, especially in Docker containers with a bounded heap. Serialize archive work.
> - `action:resync` opens a raw socket inline — over Android-style IPC it fails with the misleading "Could not connect to Archive host!" (see `restoresync`).
> - Pruned archive blocks keep coin creation/spend data (via MMR) but NOT transaction inputs/outputs — see the `txpow` hazard.

---

## MegaMMR & sync

### megammr

```
megammr (action:info|export|import) (file:)
```

Info on, or import/export of, the MegaMMR data (the complete coin set + proofs). The node must be running with `-megammr`.

```
megammr action:export file:thefile
```

### megammrsync

```
megammrsync [action:mydetails|resync] [host:] (phrase:) (anyphrase:) (keys:) (keyuses:) (file:) (password:)
```

Fast chain or seed re-sync from a MegaMMR node (much faster than a full archive resync):

- Wrong chain, wallet intact → just `action:resync host:`.
- Fresh node, restore a wallet → add `phrase:"24 WORDS.."` (and a raised `keyuses:`).
- Old backup → `file:` + `password:` restores it, then syncs to tip.
- `action:mydetails` shows which addresses/public keys a resync would search for.

The host MUST be running `-megammr`.

```
megammrsync action:resync host:<your-megammr-host:9001> phrase:"YOUR 24 WORD SEED PHRASE" keyuses:2000
```

> ⚠️ **IPC hazard.** Same socket-inline behavior as `archive`/`restoresync`: over broadcast-Intent style IPC it fails with a misleading "Could not connect" error even when the host is reachable. Run from terminal/HTTP RPC.

---

## MySQL archive

### mysql

```
mysql (host:) (database:) (user:) (password:) (readonly:) (logs:) [action:] (phrase:) (keys:) (keyuses:) (address:) (enable:) (file:)
```

Mirror the archive data into a MySQL server (for scale-out archive/explorer backends). Actions:

- `setlogin` / `clearlogin` — store login details so you don't retype them (subsequent calls then need only `action:`).
- `info` — compare blocks in the node's archive db vs the MySQL db.
- `integrity` — verify block order/parents in MySQL.
- `update` — push the latest syncblocks from the node's archive db into MySQL.
- `autobackup enable:true` — continuously save archive data (and all TxPoW the node sees) to MySQL.
- `findtxpow txpowid:` — look up an individual TxPoW (requires autobackup).
- `addresscheck address:` — full spent/unspent coin history for an address.
- `resync` — chain or seed re-sync **from** the MySQL db (`phrase:` wipes + restores the wallet). Shuts the node down when done — you must restart it.
- `wipe` — careful: wipes the MySQL db.
- `h2export` / `h2import` / `rawexport` / `rawimport` — move data between MySQL and archive gzip / raw .dat files (raw is faster).

```
mysql host:127.0.0.1:3306 database:archivedb user:archiveuser password:archivepassword action:setlogin
mysql action:update
```

### mysqlcoins

```
mysqlcoins (host:) (database:) (user:) (password:) (readonly:) (logs:) [action:info|wipe|update|search] (where:) (query:) (address:) (spent:) (limit:) (maxcoins:)
```

Build and query a searchable **coins** table from your MySQL archive data (same database as `mysql`, new table). `action:search` accepts `where:` (SQL WHERE, string values in single quotes), a full `query:`, or a simple `address:` check with `spent:true/false` and `limit:`.

```
mysqlcoins action:search where:"address='0x791E..' spent:false limit:1"
```

> ⚠️ `action:update` can take a VERY long time on a full archive — bound it with `maxcoins:` and run it incrementally.

---

## Maxima

Maxima is Minima's off-chain information transport layer (user-to-user messaging over the node network).

### maxima

```
maxima [action:info|setname|seticon|hosts|send|sendall|refresh] (name:) (icon:) (id:)|(to:)|(publickey:) (application:) (data:) (poll:) (delay:)
```

Your Maxima identity and messaging:

- `info` — your name, publickey, staticmls, mls, local identity and current contact address.
- `setname` / `seticon` — set your display name/icon.
- `hosts` — your Maxima hosts (publickey, contact address, last seen, connected).
- `send` — send to one contact by `id:` (from `maxcontacts`), `to:` (contact address) or `publickey:`, with an `application:` string (which app should process it) and `data:` (HEX or JSON). `poll:true` retries until delivered.
- `sendall` — send to all contacts (`delay:` ms between sends).
- `refresh` — ping all contacts with a network message.

```
maxima action:send id:1 application:appname data:0xFED5.. poll:true
```

### maxcontacts

```
maxcontacts [action:list|add|remove|search|export|import] (contact:) (id:) (publickey:) (contactlist:)
```

Manage Maxima contacts. `add contact:MxG18H..` uses the other node's contact address (from their `maxima action:info`); `remove id:` also removes you from their side; `export`/`import contactlist:Mx..,Mx..` migrate contacts — import quickly, Max addresses change constantly.

```
maxcontacts action:add contact:MxG18H..
```

### maxextra

```
maxextra [action:] (publickey:) (maxaddress:) (enable:) (host:)
```

Advanced Maxima: static MLS (Maxima Location Service) hosting and public contactability.

- `staticmls host:Mx..@<your-server-ip>:9001` — pin an always-on node as your MLS (`host:clear` to unset).
- `addpermanent` / `removepermanent` / `listpermanent` / `clearpermanent` — on the MLS node, allow `getaddress` requests for a publickey.
- `getaddress maxaddress:MAX#<pubkey>#<staticmls>` — resolve someone's current contact address via their permanent maxaddress.
- `mlsinfo` — who is using you as their MLS.
- `allowallcontacts enable:false`, `addallowed publickey:`, `listallowed`, `clearallowed` — control who may add you as a contact (allowed list is held in RAM).

```
maxextra action:getaddress maxaddress:MAX#0x3081..#Mx..@<static-mls-host>:9001
```

### maxcreate

```
maxcreate
```

Create a standalone 128-bit RSA public/private key pair (independent of your Maxima ID) for use with `maxsign`/`maxverify`. Returns the key pair as HEX.

```
maxcreate
```

### maxsign

```
maxsign [data:] (privatekey:)
```

Sign HEX data with your Maxima ID key, or with a `maxcreate` private key.

```
maxsign data:0xCD34.. privatekey:0x30819..
```

### maxverify

```
maxverify [data:] [publickey:] [signature:]
```

Verify a Maxima signature. Returns `valid: true|false`.

```
maxverify data:0xCD34.. publickey:0xFED5.. signature:0x4827..
```

---

## System & other

### mds

```
mds (action:list|install|update|uninstall|download|pending|accept|deny|permission|publicmds) (file:) (uid:) (trust:read|write) (enable:)
```

MiniDapp System management:

- `list` (default) — installed MiniDapps with their uid and permission.
- `install file:wallet_1.0.mds.zip (trust:write)` — install (default permission READ).
- `update uid: file:` / `uninstall uid:` / `download uid:` — lifecycle.
- `pending` / `accept uid:` / `deny uid:` — approve or reject commands queued by READ-mode dapps.
- `permission uid: trust:read|write` — change a dapp's mode.
- `publicmds enable:true` — enable the public MDS endpoint.

```
mds action:install file:wallet_1.0.mds.zip trust:write
```

> ⚠️ Do NOT give WRITE permission to MiniDapps you do not trust — WRITE means they can move funds and change node state without per-command approval.

### mine

There is **no standalone `mine` command**. Transaction mining is controlled per-command via the `mine:true` parameter on `send` / `txnpost` / `tokencreate` (mine synchronously instead of async). An `automine` command exists in the source but its body is commented out — it is a no-op; don't rely on it.

> ⚠️ Related async gotcha: posting a transaction without `mine:true` mines in the background — an immediately-returned "not a transaction yet" state is NOT a failure; the tx confirms a few blocks later. Never retry-post on that signal.

### burn

```
burn
```

View burn metrics: number of burn transactions and max/median/average/min burn over the last 1, 10 and 50 blocks (`1block`, `10block`, `50block`, each with `txns`, `max`, `med`, `avg`, `min`). Use to pick an appropriate `burn:` for your own transactions when the network is busy.

```
burn
```

### magic

```
magic (kissvm:) (txpowsize:) (txnsperblock:)
```

View (no params) or vote on the network "Magic" capacity numbers: max KISS-VM opcodes, max TxPoW size, max transactions per block. Response shows `lastblock` (the magic values actually in force at the tip) and `desired` (what your node votes for).

```
magic
```

### mempool

```
mempool
```

Check the mempool. Response: `txpow[]` (each with `txpowid`, `transaction`, `block`), plus counts — `total`, `onchain`-eligible entries, and how many are `cascade`-only.

```
mempool
```

### trace

```
trace [enable:true|false] (filter:) (network:)
```

Stream the internal Minima engine message stacks to the terminal, optionally filtered by a case-sensitive string (`MAIN`, `MINER`, `MDS`, `NOTIFYMANAGER`, `TXPOWPROCESSOR`, ...). `network:true` adds low-level network messages. **Only works on the terminal** — the output goes to stdout, not to the RPC response.

```
trace enable:true filter:MDS
```

### logs

```
logs (scripts:) (mining:) (maxima:) (networking:) (blocks:) (ibd:) (peerschecker:) (txpowdb:)
```

Toggle detailed logging per subsystem (script errors, mining start/end, Maxima, network messages, blocks, IBD processing, TxPoWDB adds). Each flag is `true`/`false`.

```
logs scripts:true mining:false
```

### quit

```
quit (compact:)
```

Shut down Minima safely. `compact:true` compacts the databases on the way down. Ensure you have a backup first.

```
quit compact:true
```

### seedrandom

```
seedrandom [modifier:]
```

Generate a deterministic random value from your wallet SEED hashed with a modifier string. Same node + same modifier → same value; different nodes → different values. Useful for deriving app-specific secrets without storing them.

```
seedrandom modifier:"Hello you"
```

> ⚠️ Fails with "DB locked!" while the node is Vault password-locked (the seed is unavailable).

### incentivecash

```
incentivecash (uid:)
```

Legacy Incentive Program command: show your rewards balance (daily/invite/community breakdown), or set your incentive UID to start receiving daily rewards. Tied to the (historical) incentive.minima.global program — of little use on modern nodes but still present.

```
incentivecash uid:00d11b34-7b47-45f3-775c-a37cbe4c9ff3
```

### help

```
help (command:)
```

`help` alone lists every command with its one-line signature (`[]` = required, `()` = optional). `help command:<name>` prints the full per-command documentation — the authoritative source for the exact flags your node version supports. Remember commands can be chained with `;`.

```
help command:archive
```

---

## Commands you may see referenced that do NOT exist in current core

Verified against the current command source tree — don't burn time hunting for these:

- **`coinsimple`** — not a command. Use `coins` (Part 1) with filters, or `balance simple:true` for a minimal balance view.
- **`sshtunnel`** — existed in old releases; removed from current core. Use a normal SSH tunnel / reverse proxy in front of the RPC port instead.
- **`taskwait`** — not a command. For "wait until a queued write is approved", poll `checkpending uid:`; for send-until-confirmed semantics use `sendpoll` (Part 1).
- **`mine`** — see the note in System & other above: it's a parameter (`mine:true`), not a command.


---

## reference: commands-wallet-transactions
<!-- (was references/commands-wallet-transactions.md) -->

# Minima Command Reference — Part 1: Wallet, Coins, Tokens, Transactions, Scripts, Crypto

Post/construction gotchas live in the skill's CRITICAL RULES and references/transactions-utxo.md — this file is the command lookup.

Notation: `[param:]` = required, `(param:)` = optional. Commands are typed into the Minima terminal, MDS `cmd` handler, or RPC. Chain multiple commands with `;`. `help command:<name>` on a live node prints the same detail.

---

## Wallet, Keys and Addresses

### getaddress

```
getaddress
```

Returns one of your 64 default Minima addresses — used to receive funds and as change addresses. Each address can be used securely 262144 (64^3) times. After heavy use you can wipe private keys from an online node with `vault`.

Key response fields: `address` (0x hex), `miniaddress` (Mx form — interchangeable with 0x when sending), `publickey`, `script` (always `RETURN SIGNEDBY(<publickey>)` for default addresses), `default:true`, `simple:true`.

```
getaddress
```

### newaddress

```
newaddress
```

Creates a brand-new address that is NOT one of the 64 default change addresses. Use for a specific purpose (e.g. one address per invoice) or improved privacy. Same response shape as `getaddress` but `default:false`.

```
newaddress
```

### keys

```
keys (action:list|new|checkkeys|genkey) (publickey:)
```

List all your public keys or create a new key pair. Each key can sign securely 262144 times.

- `action:list` — list existing public keys (default)
- `action:checkkeys` — verify your public and private keys match
- `action:new` — create a new key pair stored and managed in your wallet DB
- `action:genkey` — generate a key NOT stored in the DB (you keep the private key yourself)
- `publickey:` — search for one specific key

Key response fields per key: `publickey`, `script`, `address`/`miniaddress`, uses count vs max uses.

```
keys action:list publickey:0xFFEE56..
keys action:new
```

### vault

```
vault [action:seed|wipekeys|restorekeys|passwordlock|passwordunlock] (seed:) (phrase:) (password:) (confirm:)
```

BE CAREFUL. View, wipe, restore, encrypt or decrypt your private keys. Ensure you have a backup and a secure record of your passphrase before locking.

- `action:seed` — show your seed phrase and seed (default). DO NOT SHARE.
- `action:wipekeys seed:0x..` — delete private keys, keep public (cold-wallet an online node)
- `action:restorekeys phrase:"24 WORDS.."` — reinstate private keys from passphrase
- `action:passwordlock password:.. (confirm:..)` — encrypt private keys with a password
- `action:passwordunlock password:..` — decrypt and reinstate private keys

While password-locked, `send` / `txnsign` / `consolidate` / `multisig` accept a one-shot `password:` that unlocks, signs and relocks.

```
vault
vault action:wipekeys seed:0xDD4E..
vault action:passwordlock password:your_password confirm:your_password
```

### printtree

```
printtree (depth:) (cascade:true|false)
```

Print an ASCII tree representation of the blockchain / TxPoW tree. Default depth 32 blocks from the tip; `cascade:true` also shows the cascading chain (default false).

```
printtree depth:500
printtree cascade:true
```

---

## Balance and Send

### balance

```
balance (address:) (tokenid:) (confirmations:) (simple:) (coinlist:) (tokendetails:) (megammr:)
```

Show your total balance of Minima and all tokens.

- `address:` — balance for a specific 0x or Mx address
- `tokenid:` — one token only; Minima is `0x00`
- `confirmations:` — blocks before a coin counts as confirmed (default 3)
- `simple:true` — just name + confirmed amount
- `coinlist:true` — list the underlying valid coins
- `tokendetails:true` — full token metadata
- `megammr:true` — also search the MegaMMR for coins

Key response fields per token: `token` (string or metadata object), `tokenid`, `confirmed`, `unconfirmed` (pending — UTXO locked, can't spend yet), `sendable` (ready to spend now; excludes coins locked in contracts), `coins` (count), `total` (total supply for tokens).

```
balance
balance tokenid:0xFED5.. confirmations:10
balance simple:true
```

### send

```
send (address:Mx..|0x..) (amount:) (multi:[..]) (tokenid:) (state:{}) (burn:) (split:)
     (coinage:) (mine:) (password:) (fromaddress:) (signkey:) (storestate:) (debug:) (dryrun:)
```

Send Minima or custom tokens to a wallet or script address. The node picks input coins, signs, builds change and posts in one step.

- `address:` + `amount:` — recipient and amount (amount supports up to 44 decimals; min 0.000001)
- `multi:["addr:amt","addr2:amt2",..]` — multiple recipients in one transaction
- `tokenid:` — custom token id; defaults to Minima `0x00`
- `state:{"0":"value","1":"value"}` — state variables when sending to a script address
- `burn:` — Minima to burn as a priority fee
- `split:` — 1..20; splits the sent amount into N equal output coins. Send to your own address to split your own UTXOs so you can transact without waiting for change to confirm
- `coinage:` — minimum age (blocks) of input coins
- `mine:true` — mine the TxPoW immediately/synchronously
- `password:` — one-shot Vault unlock if the wallet is password locked
- `fromaddress:` — only use input coins from this address; `signkey:` — sign with only this key (use together)
- `storestate:false` — output coins do not store the state (they still appear in NOTIFYCOIN events); default true
- `dryrun:true` — simulate without posting; `debug:true` — verbose logs

Response: the full TxPoW JSON (`txpowid`, `transaction.inputs[]`, `transaction.outputs[]`, `state`, etc.). Spending the same UTXO twice before confirmation fails — wait or pre-`split`.

```
send address:0xFF.. amount:10 tokenid:0xFED5.. burn:0.1
send multi:["0xFF..:10","0xEE..:10","0xDD..:10"] split:20
send amount:1 address:0xFF.. state:{"0":"0xEE..","1":"0xDD.."}
```

### sendpoll

```
sendpoll (action:add|list|remove) (uid:) + all `send` parameters
```

Queue a `send` that is retried every 30 seconds until it succeeds (status true) — survives temporarily locked coins or unconfirmed change. Accepts all `send` parameters.

- `action:list` — show the polling list (returns each entry's `uid` and command)
- `action:remove uid:` — cancel a queued send

```
sendpoll address:0xFF.. amount:10 tokenid:0xFED5..
sendpoll action:list
sendpoll action:remove uid:0x..
```

### sendnosign

```
sendnosign (address:) (amount:) (multi:[..]) (tokenid:) (state:{}) (burn:) (split:) (file:) (debug:)
```

Construct a transaction but do NOT sign it — writes an unsigned `.txn` file to the node's base folder (or `file:`). Must run on an ONLINE node (it attaches the input coins' MMR proofs). The file is then signed offline with `sendsign` and posted with `sendpost`. Ideal for cold-wallet setups where the online node's keys are wiped or locked. Same send-shaping params as `send`.

Response: the path/name of the created unsigned `.txn` file.

```
sendnosign address:0xFF.. amount:10 tokenid:0xFED5.. burn:0.1
```

### sendview

```
sendview [file:]
```

View the JSON details of a `.txn` file (signed or unsigned) created by `sendnosign`/`sendsign` — inputs, outputs, state, signatures.

```
sendview file:unsignedtransaction-1674907380057.txn
```

### sendsign

```
sendsign [file:] (password:)
```

Sign an unsigned `.txn` file from `sendnosign`. Can run on an offline node holding the keys. If the node is Vault password-locked, pass `password:` — keys are re-encrypted after signing. Outputs a new signed `.txn` file for `sendpost`.

```
sendsign file:unsignedtransaction-1674907380057.txn password:your_vaultpassword
```

### sendpost

```
sendpost [file:]
```

Post a signed `.txn` file (from `sendnosign` → `sendsign`) to the network. Must be posted from an online node within ~24 hours of creation so the MMR proofs are still valid.

```
sendpost file:signedtransaction-1674907380057.txn
```

### sendfrom

```
sendfrom [fromaddress:] [address:] [amount:] [script:] [privatekey:] [keyuses:] (tokenid:) (state:) (split:) (burn:) (mine:)
```

Send Minima or tokens from a specific address using an explicitly provided script and private key — for keys managed outside the node wallet (see also `keys action:genkey` and the `createfrom`/`signfrom`/`postfrom` family). `keyuses:` must track how many times the key has signed (Minima signatures are stateful).

```
sendfrom fromaddress:0xFF.. address:0xEE.. amount:5 script:"RETURN SIGNEDBY(0xFF..)" privatekey:0x.. keyuses:12
```

### multisig

```
multisig [action:create|getkey|list|spend|sign|post|view] (id:) (amount:) (publickeys:[..])
         (required:) (root:) (coinid:) (address:) (file:) (password:)
```

Built-in n-of-m multisig coins: spendable only by a txn signed by `root` OR `required` of the listed `publickeys`.

- `action:getkey` — returns one of your default public keys to contribute to a multisig
- `action:create id: amount: publickeys:["0x..",..] required:n (root:0x..)` — lock funds in a multisig coin. The `id` is hashed into state variable 0 (cannot be retrieved later — keep it)
- `action:list (id:)` — list multisig coins you track
- `action:spend id:|coinid: amount: address: (file:)` — build an unsigned `.txn` spend file
- `action:sign file: (password:)` — sign with whichever relevant keys this node holds; outputs a new `signed_*.txn` — pass the file between signers until the threshold is met
- `action:view file:` — inspect a multisig `.txn` file
- `action:post file:` — post the fully signed transaction

```
multisig action:create id:2of3multisig amount:100 publickeys:["0xFF..","0xEE..","0xDD.."] required:2
multisig action:spend id:2of3multisig amount:5 address:0xFF.. file:multisig.txn
multisig action:sign file:multisig.txn
multisig action:post file:signed_multisig.txn
```

---

## Coins (UTXOs)

### coins

```
coins (relevant:true) (sendable:true) (coinid:) (amount:) (address:) (tokenid:)
      (coinage:) (depth:) (checkmempool:) (order:asc|desc) (megammr:)
```

Search for coins — yours (`relevant:true`) or anywhere in the unpruned chain (`relevant:false`, the default when other params are given).

- `sendable:true` — filter out coins locked in contracts
- `coinid:` — one specific coin
- `amount:` / `address:` (0x or Mx, incl. script addresses) / `tokenid:` (`0x00` = Minima) — filters
- `coinage:` — minimum age in blocks; `depth:` — how far back from the tip to search
- `checkmempool:true` — flag coins already spent in the mempool
- `order:asc|desc`; `megammr:true` — also search the MegaMMR

Each returned coin object:

```json
{
  "coinid": "0xFF..", "amount": "91.815999", "address": "0xFF..",
  "miniaddress": "MxFF..", "tokenid": "0x00", "token": null,
  "storestate": false, "state": [], "mmrentry": "728",
  "spent": false, "created": "203728"
}
```

`token` is null for Minima, or the token metadata object for custom tokens. Use `coinid` + `amount` + `address` when building manual transactions.

```
coins relevant:true sendable:true
coins relevant:true address:0xCEF6.. tokenid:0x00
```

### coincheck

```
coincheck [data:]
```

Check that a coin exists and is valid (unspent coins only). `data:` is the export blob from `coinexport`. Returns the coin details plus whether its MMR proof is valid (`proofblock`, proof validity flag).

```
coincheck data:0x00000..
```

### coinexport

```
coinexport [coinid:]
```

Export a coin together with its MMR proof as a single HEX blob. Another node can `coinimport` it to know of (and optionally track) the coin. Export does not enable spending — only knowledge of existence. The blob is also usable as `coindata:` in `txninput` and `data:` in `coincheck`.

```
coinexport coinid:0xCD34..
```

### coinimport

```
coinimport [data:] (track:true|false)
```

Import a coin (with MMR proof) exported from another node. `track:true` creates an MMR entry and adds it to your relevant coins so you see when it is spent. Import does not enable spending the coin.

```
coinimport data:0x00000.. track:true
```

### cointrack

```
cointrack [enable:true|false] [coinid:]
```

Track (keep its MMR proof up to date, know when spent) or untrack (remove from relevant coins) a coin already known to your node.

```
cointrack enable:true coinid:0xCD34..
```

### coinnotify

```
coinnotify [action:add|remove|check] [address:]
```

Listen for coins at a specific address WITHOUT adding a script for it — a NOTIFYCOIN event fires when a matching coin appears in the chain. Not persisted: re-add on every startup (e.g. from a MiniDapp's service.js).

```
coinnotify action:add address:0xFFEEDD..
coinnotify action:check address:Mx12ABGF56..
```

### consolidate

```
consolidate [tokenid:] (coinage:) (maxcoins:) (maxsigs:) (burn:) (password:) (debug:) (dryrun:)
```

Merge multiple small coins into one by sending them back to yourself. Requires at least 3 coins of the token.

- `tokenid:` — `0x00` for Minima or a custom token
- `coinage:` — minimum confirmations per input coin (default 3)
- `maxcoins:` — 3..20 coins per run, smallest-value first
- `maxsigs:` — up to 5 signatures; coins sorted by address to minimize signers
- `burn:` / `password:` / `dryrun:true` (simulate) / `debug:true`

```
consolidate tokenid:0x00 coinage:10 maxcoins:8 maxsigs:3 burn:1 dryrun:true
```

---

## Tokens

### tokens

```
tokens (tokenid:) (action:import|export) (data:)
```

List all tokens in the unpruned chain, or share token metadata between nodes: `action:export tokenid:` produces a HEX blob; `action:import data:` loads it. Response per token: `name` (string or JSON), `tokenid`, `total`, `decimals`, `script`, `coinid` (creation coin), `totalamount`, `scale`.

```
tokens tokenid:0xFED5..
tokens action:export tokenid:0xFED5..
tokens action:import data:0x000..
```

### tokencreate

```
tokencreate [name:] [amount:] (decimals:) (script:) (state:{}) (signtoken:) (webvalidate:) (burn:) (mine:)
```

Create (mint) custom tokens or NFTs. Tokens are "colored coins" — a fraction of 1 Minima is consumed as the base, so you need some sendable Minima.

- `name:` — a string OR a JSON object with arbitrary metadata (common app-level fields: `name`, `description`, `link`/`url`, `icon`, `color` — not enforced by protocol)
- `amount:` — total supply, 1 to 1 trillion
- `decimals:` — default 8, max 16; use `0` for NFTs
- `script:` — token-level script that must also return TRUE whenever ANY coin of this token is spent (in addition to the coin's own script)
- `state:{}` — state variables when adding a script
- `signtoken:0x..` — sign the token with a public key to prove creatorship (verify with `tokenvalidate`)
- `webvalidate:` — URL of a public .txt file you host that will contain the tokenid
- `burn:` / `mine:true`

Response structure (the minting TxPoW):

```
inputs[0].tokenid  = "0x00"     <- Minima consumed as the base
outputs[0].tokenid = tokenid    <- the new token coin
outputs[0].token.tokenid        <- THE real tokenid: use this for send/balance
outputs[0].token.name           <- your metadata
outputs[0].token.total / .decimals / .script ("RETURN TRUE" by default)
outputs[0].tokenamount          <- human-readable supply
outputs[1].tokenid = "0x00"     <- Minima change back to you
```

```
tokencreate name:newtoken amount:1000000
tokencreate amount:10 decimals:0 name:{"name":"mynft","description":"one of ten"}
tokencreate name:charitycoin amount:1000 script:"ASSERT VERIFYOUT(@TOTOUT-1 0xFF.. 1 0x00 TRUE)"
```

### tokenvalidate

```
tokenvalidate [tokenid:]
```

Validate a token's `signtoken` signature and its `webvalidate` link (fetches the URL and checks the tokenid is listed). Returns validity of both.

```
tokenvalidate tokenid:0xFED5..
```

---

## Manual Transaction Construction

The precise-control pipeline. Canonical simple flow:
`txncreate` → `txninput` → `txnoutput` (+ `txnstate`) → `txnsign` → `txnbasics` → `txnpost` → (`txndelete`).
Inputs minus outputs is burned — always add a change output. All commands return/print the current transaction JSON so you can inspect progress.

### txnlist

```
txnlist (id:) (transactiononly:)
```

List your custom in-progress transactions (including previously posted ones), with full details — inputs, outputs, state, and witness (signatures, mmrproofs, scripts).

```
txnlist id:multisig
```

### txncreate

```
txncreate [id:]
```

Create a new empty custom transaction under an id. First step before adding inputs/outputs. Delete with `txndelete` on failure paths so its input coins aren't left locked.

```
txncreate id:simpletxn
```

### txnauto

```
txnauto [id:] [amount:] [address:] (tokenid:) (sign:) (burn:) (mmrscript:)
```

Shortcut: build a complete simple transaction automatically — picks input coins for `amount`, adds output to `address` and change, optionally signs (`sign:`) and adds MMR/scripts (`mmrscript:`).

```
txnauto id:quick amount:10 address:0xFF..
```

### txnaddamount

```
txnaddamount [id:] [amount:] (address:) (onlychange:) (tokenid:) (split:) (burn:)
```

Add input coins to cover a given amount and calculate the change. Either outputs `amount` to `address`, or with `onlychange:` only adds the change output (when you've already added the main output manually). Useful for preparing multiple offline transactions.

```
txnaddamount id:simpletxn amount:10 address:0xFF..
```

### txnbasics

```
txnbasics [id:]
```

Automatically attach the MMR proofs and input scripts to a transaction. Run only when the transaction is ready to post (after signing; proofs are tip-relative). Populates `witness.mmrproofs` and `witness.scripts`.

```
txnbasics id:simpletxn
```

### txndelete

```
txndelete [id:]
```

Delete a custom transaction — or `id:all` to clear every one. Always delete abandoned transactions so their input coins are released.

```
txndelete id:simpletxn
txndelete id:all
```

### txncheck

```
txncheck [id:]
```

Show transaction details and verify inputs, outputs, signatures, proofs and scripts. Key response fields: `coins[].input` / `.output` / `.difference` per token, `burn` (what will be destroyed = inputs − outputs), and `valid.basic` / `valid.signatures` / `valid.mmrproofs` / `valid.scripts`. Note: script checks that depend on chain globals (e.g. `@BLKNUM`, `@COINAGE`) cannot be evaluated here.

```
txncheck id:simpletxn
```

### txninput

```
txninput [id:] (coinid:) (coindata:) (floating:) (address:) (amount:) (tokenid:) (scriptmmr:true)
```

Add a coin as a transaction input.

- `coinid:` — a concrete coin (find via `coins relevant:true`)
- `coindata:` — coin blob from `coinexport` or the `outputcoindata` of another transaction (chained/offline txns)
- `floating:true address: amount: tokenid:` — an unspecified ELTOO floating input: attachable later to any existing coin with the same address/amount/tokenid but any coinid
- `scriptmmr:true` — also attach the coin's script and MMR proof in this step (then skip `txnbasics` — doing both duplicates proofs)

```
txninput id:simpletxn coinid:0xD0BF..
txninput id:eltootxn floating:true address:0xFED5.. amount:10 tokenid:0x00
```

### txnoutput

```
txnoutput [id:] [amount:] [address:] (tokenid:) (storestate:)
```

Add an output — a new coin (UTXO). If sum(inputs) > sum(outputs) the difference is burned, so add your own change output. `storestate:` (default true) controls whether the transaction's state variables are stored in the new coin — must agree with any `VERIFYOUT(... keepstate)` in the controlling script.

```
txnoutput id:simpletxn amount:10 address:0xFED5..
txnoutput id:simpletxn amount:4.5 address:0xFF.. storestate:false
```

### txnstate

```
txnstate [id:] [port:] [value:]
```

Add a state variable to the transaction. `port:` 0–255; `value:` HEX, number or quoted string. Scripts read these via `STATE(port)`; output coins with `storestate:true` carry them, becoming the next spend's `PREVSTATE(port)`.

```
txnstate id:simpletxn port:0 value:0xFED5..
txnstate id:simpletxn port:1 value:"string"
```

### txnscript

```
txnscript [id:] (auto:false) (scripts:{})
```

Attach input scripts to a transaction. `scripts:` is a JSON map of `{script:proof}` — proof empty (`""`) for a plain script, or the MMR proof for a MAST script node from `mmrcreate`. `auto:true` adds the scripts your node already knows (useful in multi-party transactions).

```
txnscript id:txnmast scripts:{"RETURN TRUE":"0x000.."}
```

### txnmmr

```
txnmmr [id:]
```

Add MMR proofs for the inputs, keeping any proofs already present (unlike `txnbasics`, which sets everything).

```
txnmmr id:simpletxn
```

### txnsign

```
txnsign [id:] [publickey:0x..|auto] (password:) (txnpostauto:) (txnpostburn:) (txnpostmine:) (txndelete:)
```

Sign a transaction. `publickey:auto` signs with whatever the simple wallet inputs require; pass an explicit `publickey:0x..` for keys named in custom scripts (run once per required key). `password:` for a Vault-locked node. The `txnpost*` params optionally post in the same call (`txnpostauto` behaves like `txnpost auto:`), and `txndelete:true` deletes after sign-and-post.

```
txnsign id:simpletxn publickey:auto
txnsign id:multisig publickey:0xFD8B..
```

### txnclear

```
txnclear [id:] (scripts:) (mmr:) (signatures:)
```

Clear the witness data — signatures, MMR proofs and script proofs (each flag defaults to true, so a bare call clears all three). Use before re-signing or re-proving a stale transaction.

```
txnclear id:multisig
```

### txnpost

```
txnpost [id:] (auto:true) (burn:) (mine:) (txndelete:)
```

Post the transaction to the network. `auto:true` sets scripts and MMR proofs at post time (only safe for simple wallet inputs — for script-address coins run `txnsign` → `txnbasics` → `txnpost` explicitly). `burn:` adds a burn; `mine:true` mines it immediately; `txndelete:true` deletes the txn after posting. Note `status:true` means "accepted and broadcast", NOT "valid on-chain" — confirm by watching the coins/chain.

```
txnpost id:simpletxn
txnpost id:simpletxn burn:0.1 mine:true
```

### txnimport

```
txnimport (id:) (file:) (data:)
```

Import a transaction from exported HEX `data:` or a `.txn` `file:`, optionally assigning a new `id:`. Counterpart of `txnexport` — the transfer vehicle for multi-party / offline signing.

```
txnimport id:multisig file:multisig.txn
txnimport data:0x0000..
```

### txnexport

```
txnexport [id:] (file:)
```

Export a transaction as HEX (returned in `data`) or to a `.txn` file, to be `txnimport`ed on another node (e.g. for signing).

```
txnexport id:multisig file:multisig.txn
```

### txnview

```
txnview (file:) (data:)
```

View an exported transaction (from `.txn` file or HEX data) as JSON without importing it.

```
txnview file:multisig.txn
```

### txnmine

```
txnmine (id:) (data:)
```

Mine a transaction (perform its TxPoW) WITHOUT posting it — from a stored txn `id:` or exported `data:`. The pre-mined result can be posted later with `txnminepost` (e.g. mine on a powerful machine, post from a weak one).

```
txnmine id:simpletxn
```

### txnminepost

```
txnminepost [data:]
```

Post a pre-mined transaction produced by `txnmine`.

```
txnminepost data:0x0000..
```

### rawtxnfrom

```
rawtxnfrom [inputs:] [outputs:] [scripts:] (state:)
```

Create an unsigned transaction in one shot from JSON: `inputs:` array of `{coinid, script}` objects, `outputs:` array of `{address, amount, tokenid, storestate}` objects, `scripts:` for the input coins, `state:` JSON state object. For programmatic construction with externally managed keys.

```
rawtxnfrom inputs:[{"coinid":"0xFF..","script":"RETURN SIGNEDBY(0xEE..)"}] outputs:[{"address":"0xDD..","amount":"9.9996","storestate":false,"tokenid":"0x00"}] state:{"0":"98","1":"[MESSAGE]"}
```

### createfrom

```
createfrom [fromaddress:] [address:] [amount:] (tokenid:) [script:] (burn:)
```

Create an unsigned transaction spending from a specific address whose script you supply — for coins controlled by keys outside the node wallet. Returns the unsigned txn data for `signfrom`.

```
createfrom fromaddress:0xFF.. address:0xEE.. amount:5 script:"RETURN SIGNEDBY(0xDD..)"
```

### signfrom

```
signfrom [data:] [privatekey:] [keyuses:] (post:)
```

Sign a `createfrom` transaction with an explicitly provided private key. `keyuses:` MUST be higher than any previous signing count for that key (Minima signatures are stateful). `post:true` posts immediately after signing.

```
signfrom data:0x0000.. privatekey:0x.. keyuses:12
```

### postfrom

```
postfrom [data:] (mine:true|false) (mmr:true|false)
```

Post a transaction signed with `signfrom`. `mmr:` attaches MMR proofs at post time; `mine:true` mines it immediately.

```
postfrom data:0x0000.. mine:true
```

---

## Scripts

### scripts

```
scripts (address:)
```

List all scripts/addresses your node is tracking, or look one up by 0x/Mx address. Response per entry: `script` (KISS VM source), `address`/`miniaddress`, `simple` (basic wallet address?), `default`, `publickey` (for simple addresses — default addresses are `RETURN SIGNEDBY(<publickey>)`), `track`.

```
scripts address:0xFED5..
```

### newscript

```
newscript [script:] [trackall:false|true] (clean:false|true)
```

Register a custom script with your node. Returns the script's address — the node then recognizes coins at that address. `trackall:true` tracks ALL coins at the address; `false` only coins whose state variables are relevant to you. `clean:true` normalizes the script to its minimal correct representation before hashing (default false — cleaning changes the address!).

```
newscript trackall:true script:"RETURN SIGNEDBY(0x1539..) AND SIGNEDBY(0xAD25..)"
```

### runscript

```
runscript [script:] (state:{}) (prevstate:{}) (globals:{}) (signatures:[]) (extrascripts:{})
```

Dry-run a script off-chain with mocked context. Scripts are auto-cleaned.

- `state:{"0":"val",..}` — transaction state values (`STATE(n)`)
- `prevstate:{"0":"val",..}` — input coin state (`PREVSTATE(n)`)
- `globals:{"@BLOCK":"101","@COINAGE":"23",..}` — global variable values
- `signatures:["0xFF.."]` — pubkeys treated as having signed (`SIGNEDBY`)
- `extrascripts:{script:proof,..}` — MAST leaf scripts and proofs

Key response fields: `clean` (cleaned script + address), `parseok`, `monotonic`, `success` (the boolean result), `variables` (final LET values). Cannot evaluate `VERIFYOUT`/`VERIFYIN`/`SAMESTATE` meaningfully — no real transaction context.

```
runscript script:"RETURN SIGNEDBY(0xFF..) AND @BLOCK GT 100" globals:{"@BLOCK":"101"} signatures:["0xFF"]
runscript script:"MAST 0x0E3.." extrascripts:{"RETURN TRUE":"0x000.."}
```

### removescript

```
removescript [address:]
```

Remove a custom script from your node's DB by its 0x/Mx address. BE CAREFUL — coins at that address stop being tracked.

```
removescript address:0xFFE678768CDE..
```

### tutorial

```
tutorial
```

Print the complete KISS VM grammar — every statement, function and global for Minima scripting.

```
tutorial
```

---

## Crypto and Data Utilities

### hash

```
hash (data:) (file:) (type:sha2|sha3)
```

Hash HEX (`0x..`) or quoted-string data, or a file (path absolute or relative to the node base folder). Default algorithm SHA3 (Keccak); `type:sha2` also supported. Returns the input and the resulting hash.

```
hash data:"this is my secret" type:sha2
hash file:myfile.txt
```

### random

```
random (size:) (type:sha3|sha2)
```

Generate a random hash value; default 32 bytes. Handy for commit-reveal secrets and nonces.

```
random size:64
```

### convert

```
convert [from:] [to:] [data:]
```

Convert data between types: `String`, `HEX`, `Mx`, `Base64`. Returns the converted value.

```
convert from:String to:HEX data:hello
convert from:HEX to:Mx data:0xFFFF
```

### maths

```
maths [calculate:] (logs:)
```

Evaluate an arithmetic expression with Minima's arbitrary-precision MiniNumber math (the same precision used on-chain) — use to precompute exact amounts. `logs:true` shows working. KISS functions like `SIGDIG` are available.

```
maths calculate:"1+2 * (3/4)"
maths calculate:"1+2 * SIGDIG(1 3/4)" logs:true
```

### sign

```
sign [publickey:] [data:]
```

Sign 0x HEX data with one of your wallet public keys (the node uses the corresponding private key). Returns the signature. Remember: Minima wallet signatures are stateful — each key has a finite number of secure uses.

```
sign publickey:0xFF.. data:0xCD34..
```

### verify

```
verify [publickey:] [data:] [signature:]
```

Verify a signature over 0x HEX data against a public key. Returns valid `true`/`false`.

```
verify data:0xCD34.. publickey:0xFED5.. signature:0x4827..
```

### mmrcreate

```
mmrcreate [nodes:[..]]
```

Build an MMR tree from a JSON array of string/HEX leaf nodes — the basis of MAST contracts (leaves as alternative scripts executed via `MAST <root>` + `txnscript`/`extrascripts`) or key-set membership proofs. Returns each leaf's `data` + `proof` and the tree `root` hash. Save the proofs — you need them to spend.

```
mmrcreate nodes:["RETURN TRUE","RETURN FALSE"]
mmrcreate nodes:["0xFF..","0xEE.."]
```

### mmrproof

```
mmrproof [data:] [proof:] [root:]
```

Check an MMR proof — for coins, MAST scripts, or custom trees from `mmrcreate`. Returns `true` if the leaf `data` + `proof` hash up to `root`.

```
mmrproof data:0xCD34.. proof:0xFED5.. root:0xDAE6..
```


---

## reference: contracts-covenants
<!-- (was references/contracts-covenants.md) -->

# Proven covenant patterns for the Minima KISS VM

Real, on-chain-tested covenant designs for Minima's KISS (Keep It Simple, Stupid)
scripting VM, presented as teaching material. Every pattern here has been run,
broken, or fixed on a live chain. The scripts are sketches meant to teach the
*shape* of a safe covenant — read the safety rules first, then the pattern that
fits your problem.

All owner/creator public keys and wallet addresses below are shown as
`0xFF…`-placeholders. `0x00` is the real Minima (native coin) token id and is kept
literal. Amounts are rounded and anonymised.

Where an official worked tutorial exists for a pattern, the canonical version lives
in the Minima docs (see "Official tutorials" at the end) — this file is the
hard-won-lessons companion to those.

---

## Covenant safety rules (read this first)

These are the rules that separate a covenant that protects funds from one that
loses them. Every rule below traces to a real on-chain event.

1. **Prove `parseok` before you fund any script address.** A script that fails to
   parse produces coins that are **unspendable forever** — no key, no node rebuild,
   no proof can spend them. Always run the exact covenant bytes through the node's
   own `runscript` and confirm `parseok == true` (and ideally that it re-derives to
   the same address you are about to fund) *before* a single coin moves there.
2. **Every reachable branch must pin its outputs with `VERIFYOUT`.** Never leave a
   `SIGNEDBY(...) RETURN TRUE`-style path with no output restriction on a coin that
   holds funds. An unpinned owner-cancel path let ~5,000 MINIMA be drained on-chain.
3. **Canonicalise every literal baked into a covenant.** If a value is both baked
   into the script *and* stored elsewhere for re-derivation, both must be in the
   exact canonical form the node uses (`stripTrailingZeros().toPlainString()`),
   or the addresses silently diverge and funds land at an address nobody watches.
4. **Never let `/` get JSON-escaped into a script.** Java's `JSONObject.quote`
   turns `*5/1000` into `*5\/1000` → `parseok=false` → permanently unspendable
   coins. Quote scripts escaping only `"` and `\`.
5. **Do money math the way the VM does it.** MiniNumber is arbitrary-precision,
   rounds DOWN. Mirror it with `BigDecimal` / `decimal.js` (precision 40, ROUND_DOWN,
   no scientific notation) — never JS floats or `parseFloat`. A rounding-unit drift
   makes settlement un-signable. Round pool-favourable: reserves UP, proceeds DOWN.
6. **Exclude owner-key coins when funding an anyone-can-spend transaction.** If the
   covenant has a `SIGNEDBY(ownerkey)` branch and you fund a public spend from a coin
   at the owner address, `txnsign auto` signs with the owner key and the covenant
   takes its *owner* branch — rejecting your public spend.
7. **Sign with an explicit public key, never `auto`, near covenants.** Auto key
   selection lets a crafted transaction conscript wallet keys into signing inputs
   you never meant to spend, and trips owner branches unintentionally.
8. **`txnpost status:true` is NOT on-chain success.** It means "accepted to the
   mempool." A covenant Script-FAIL is silent — the coin never moves while the app
   logs success. Verify the contract-output *shape* against an offline VM
   (`minima-vm` / `runscript` / `txncheck`), not the post result.
9. **Read the covenant verdict, not the count.** Gate posting on
   `response.valid.scripts` (the boolean covenant pass/fail) — NOT top-level
   `scripts` (a count of distinct input scripts). Use a truthy helper accepting
   `true / 1 / "1" / "true"`; these flags are inconsistently typed.
10. **Move cold branches to MAST to stay under the size limit.** The script size
    limit is ~1150–1200 chars (undocumented, silently rejected). Rarely-used paths
    (dispute/claim/reclaim/void) go into MAST leaves with precomputed MMR proofs.
11. **All addresses come from coin STATE (`PREVSTATE`), never `getaddress` at
    runtime.** `getaddress` rotates keys; an address it returned today can become
    `relevant:false` after a vault/key-state change, stranding funds.
12. **Diagnosis-first on any covenant-spend failure.** When a spend fails on-chain
    (`Script FAIL` / `valid.scripts:false`), the FIRST move is
    `scripts address:X` → `runscript` the exact bytes → check `parseok`. Do not
    chase keys, keyuses, or node rebuilds first.

---

## 1. The cardinal covenant rule

> **Never leave a `SIGNEDBY(...) RETURN TRUE` path with no output restriction on a
> coin that holds funds.**

This is the single most expensive mistake you can make on Minima. A fixed-exchange
(limit-order) DEX shipped an owner-cancel path of the form:

```
IF SIGNEDBY(PREVSTATE(0)) THEN RETURN TRUE ENDIF
```

`PREVSTATE(0)` is the order owner's public key. The intent was "the owner can cancel
their own order." But there is **no `VERIFYOUT`** — an owner-key signature can send
the coins *anywhere*.

The attack didn't even need a hostile external party. Minima's MDS permission model
**shares wallet-key access across every app that holds WRITE permission**. A node
"hygiene" MiniDapp with write permission ran a `sweepCoins()` routine, saw the active
order coins as junk, signed them with the owner key, and sent ~5,000 MINIMA to
addresses obtained from `getaddress`. After a later vault/key-state change those
addresses showed `relevant:false, simple:false` — the funds were effectively lost.

Forensic tells worth remembering:

- **Only native-MINIMA (`0x00`) sell orders were swept; token buy orders survived.**
  The sweeper used `c.amount` directly — human-readable for `0x00`, but
  Minima-internal-scale for tokens — so token spends dusted or failed. The token
  orders survived by accident, not by design.
- **The token buy orders returned safely** via a contract-enforced expiry path
  (`@COINAGE GT 1500` → `VERIFYOUT(@INPUT PREVSTATE(1) @AMOUNT @TOKENID FALSE)`),
  which cannot be redirected. **This is proof that a trustless timeout-return path is
  worth building** — it protected funds while the unpinned cancel path lost them.

**Why it's unsafe:** any reachable `SIGNEDBY` path with no `VERIFYOUT` delegates full
custody to whoever holds that key — and on MDS that is every write-permission app,
not just your app.

**The fix** (see the DEX pattern, section 5): even the owner-cancel path pins its
output back to the maker's own address:

```
IF SIGNEDBY(PREVSTATE(0)) THEN ASSERT VERIFYOUT(@INPUT PREVSTATE(1) @AMOUNT @TOKENID FALSE) RETURN TRUE ENDIF
```

Now the owner can *trigger* a cancel, but the coins can only return to the maker's
own address. Custody is enforced by the chain, not the key.

---

## 2. Multi-phase state-machine covenant

**Pattern reference — a proven multi-phase contract shape, not a shipped product.**
The live productization of this style is a prediction-market / wager covenant
(section 7); this section teaches the *phase machine* with a provably-fair coin-flip /
dice / roulette contract as the reference implementation.

A multi-phase covenant keeps the coin at the *same* script address across several
transitions, carrying its progress in the coin's STATE ports. Each spend either
advances the phase (recreate the coin at the same address, `VERIFYOUT(... TRUE)` +
`storestate:true`) or pays out (send to a wallet address, `VERIFYOUT(... FALSE)` +
`storestate:false`).

### State-port layout

```
0=housepk   1=houseaddr   2=housecommit  3=range   4=payout   5=bet
6=phase     7=timeout     8=playerpk     9=playeraddr  10=playercommit
11=playerpick  12=housesecret  13=playersecret
```

`phase` (port 6) drives the machine: 0 = open, 1 = taken, 2 = revealed.

### Script (minified 4-path, ~1120 chars — under the size limit)

```
LET hpk=PREVSTATE(0) LET ha=PREVSTATE(1) LET hc=PREVSTATE(2) LET rng=PREVSTATE(3)
LET po=PREVSTATE(4) LET bt=PREVSTATE(5) LET ph=PREVSTATE(6) LET to=PREVSTATE(7)

/* A: Owner cancel — only while still open */
IF ph EQ 0 AND SIGNEDBY(hpk) THEN RETURN TRUE ENDIF

/* B: Take — recreate coin at phase 1 */
IF ph EQ 0 THEN
  ASSERT SAMESTATE(0 5) ASSERT STATE(6) EQ 1 ASSERT STATE(7) EQ to
  LET pp=STATE(11) ASSERT pp GTE 0 AND pp LT rng
  LET tt=@AMOUNT+bt
  ASSERT VERIFYOUT(@INPUT @ADDRESS tt @TOKENID TRUE)
  RETURN TRUE
ENDIF

LET ppk=PREVSTATE(8) LET pa=PREVSTATE(9) LET pc=PREVSTATE(10) LET pk=PREVSTATE(11)

/* C: Reveal — house reveals secret, recreate at phase 2 */
IF ph EQ 1 AND SIGNEDBY(hpk) THEN
  ASSERT SAMESTATE(0 5) ASSERT STATE(6) EQ 2 ASSERT SAMESTATE(7 11)
  LET hs=STATE(12) ASSERT SHA3(hs) EQ hc
  ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)
  RETURN TRUE
ENDIF

/* D: Resolve — compute winner, pay out */
IF ph EQ 2 AND SIGNEDBY(ppk) THEN
  LET ps=STATE(13) ASSERT SHA3(ps) EQ pc
  LET hs=PREVSTATE(12)
  LET h=SHA3(CONCAT(hs ps)) LET r=NUMBER(SUBSET(0 4 h))%rng
  IF r EQ pk THEN
    LET w=bt*po
    ASSERT VERIFYOUT(@INPUT pa w @TOKENID FALSE)
    IF @AMOUNT GT w THEN ASSERT VERIFYOUT(@INPUT+1 ha @AMOUNT-w @TOKENID FALSE) ENDIF
  ELSE
    ASSERT VERIFYOUT(@INPUT ha @AMOUNT @TOKENID FALSE)
  ENDIF
  RETURN TRUE
ENDIF

RETURN FALSE
```

Note the **owner-cancel path (A) is only reachable at `phase 0`** — before any player
has committed funds — so an unpinned `RETURN TRUE` there is acceptable: the coin only
holds the house's own stake at that point. Once a player takes the bet, every path is
`VERIFYOUT`-pinned. (If you want to be maximally safe, pin path A too.)

### Per-path transaction construction

**Create** (the house opens the game):

```
send amount:<stake> address:<scriptAddr> state:{"0":"0xFF..hpk","1":"0xFF..ha","2":"<commit>","3":"<range>","4":"<payout>","5":"<bet>","6":"0","7":"<timeout>"}
```

**Take (path B)** — player commits, coin advances to phase 1:

```
txncreate → txninput(game coin) → txninput(player funding coin)
→ txnoutput(total=@AMOUNT+bet, scriptAddr, storestate:true)
→ txnoutput(change, playerWallet, storestate:false)
→ txnstate(preserve ports 0-5, set 6=1, 7=timeout, 8-11=player info)
→ txnsign(auto) → txnbasics → txnpost
```

**Reveal (path C)** — house reveals its secret into port 12:

```
txncreate → txninput(phase-1 coin)
→ txnoutput(same amount, scriptAddr, storestate:true)
→ txnstate(preserve 0-5, set 6=2, preserve 7-11, set 12=housesecret)
→ txnsign(housepk) → txnbasics → txnpost
```

**Resolve (path D)** — player reveals its secret, contract computes the winner:

```
txncreate → txninput(phase-2 coin)
→ txnoutput(winnings → winner, storestate:false)
→ [optional txnoutput(remainder → loser, storestate:false)]
→ txnstate(set 13=playersecret)
→ txnsign(playerpk) → txnbasics → txnpost
```

### Why it's safe / unsafe

- **Safe:** phase transitions are pinned with `VERIFYOUT(... TRUE)` and re-assert the
  immutable ports with `SAMESTATE`, so a counterparty building the next transaction
  cannot rewrite the proposition, bet, range, or payout. `SAMESTATE(0 5)` +
  `SAMESTATE(7 11)` (split because port 6 changes) pins the whole immutable region.
- **Safe:** payouts are pinned to addresses that came from `PREVSTATE` (coin state at
  creation), never `getaddress`.
- **Unsafe if you skip `SAMESTATE`:** pinning only the values you *read* lets whoever
  builds the next txn silently rewrite the rest — assert the whole region unchanged.
- **VM gotchas that silently break this contract:** no underscores in variable names;
  `@BLKNUM` is broken on synced nodes (use `@COINAGE`); unset STATE ports crash the
  Java VM (set unused ports to 0); `VERIFYOUT(... FALSE)` with `storestate:true` (or
  the reverse) is silently rejected; `NUMBER()` overflows on a 32-byte hash, so you
  MUST `SUBSET(0 4 h)` first.

---

## 3. Commit-reveal randomness

Provably-fair randomness for a 2-party game without a trusted oracle. Both sides
commit to a secret (publish `SHA3(secret)`), then both reveal; the outcome is a
deterministic function of the two secrets that neither could predict at commit time.

### The on-chain KISS math

```
LET h=SHA3(CONCAT(houseSecret playerSecret))
LET r=NUMBER(SUBSET(0 4 h))%range
```

- `CONCAT` joins the two hex secrets.
- `SHA3` (Keccak-256) produces a 32-byte hash.
- `SUBSET(0 4 h)` takes the first 4 bytes — because `NUMBER()` overflows on anything
  bigger than ~4 bytes.
- `NUMBER(...)%range` maps to `0 … range-1` (2 = coin flip, 6 = dice, 36 = roulette).

### The JS that MUST match the on-chain math exactly

```javascript
// 1. Each side generates a secret and publishes only its hash (the commit)
MDS.cmd("random", function(resp){
    var secret = resp.response.random;         // 64 hex chars, 0x-prefixed
    MDS.cmd("hash data:" + secret, function(h){
        var commit = h.response.hash;          // SHA3 — this is what you publish
    });
});

// 2. After BOTH commits are locked into coin state, both reveal their secrets.

// 3. Compute the outcome — MUST reproduce the KISS VM byte-for-byte:
var combined = houseSecret + playerSecret.substring(2);  // CONCAT, strip 0x off 2nd
MDS.cmd("hash data:" + combined, function(h){
    var hash   = h.response.hash;
    var first4 = hash.substring(2, 10);        // SUBSET(0 4 hash) — skip 0x, 8 hexits
    var num    = parseInt(first4, 16);         // NUMBER()
    var result = num % range;                  // % range
    var winner = (result === playerPick) ? "player" : "house";
});
```

The two subtle traps: **strip the `0x` prefix off the second secret before
concatenating** (KISS `CONCAT` joins the raw bytes), and **take exactly 8 hex chars
(4 bytes) after the `0x`** for the `SUBSET`. Get either wrong and the JS prediction
diverges from what the chain computes — the dispute path will disagree with your UI.

### Why pessimistic commit kills the free-option attack

Deduct the player's bet from the channel/coin balance **before** the house reveals.
If the player loses and walks away, the already-deducted balance is the correct final
state — there is nothing to gain by disappearing. If instead you settled optimistically
(bet only deducted on a loss), a player could reveal, see they lost, and abort to
avoid paying — a free option. Pessimistic commit removes the option entirely.

### Why it's safe

Neither side knows the other's secret at commit time, and the combined hash is
deterministic, so both parties (and an on-chain MAST dispute path) compute the same
result independently. The randomness is jointly sourced — one dishonest party cannot
bias it.

---

## 4. MAST dispute / branch pattern

MAST (Merkelized Abstract Syntax Tree) lets you keep rarely-used branches **off** the
main script, so the on-chain script the common path pays for stays under the
~1150-char size limit. The cold branches live as separate scripts committed to by
hash; you reveal and prove only the branch you actually spend through.

Every ELTOO-style channel or long-lived covenant should carry dispute branches at
escalating block deadlines:

```
Block 32:    DISPUTE — a party proves they won, before the counterparty can claim
Block 256:   CLAIM   — counterparty claims after the other walked away
Block 1024:  RECLAIM — recover funds if the counterparty vanished without revealing
```

### Rules every MAST branch must follow

1. **`SIGNEDBY(key from PREVSTATE)`** — authorisation comes from the *coin state*
   (immutable, set at creation), never from a runtime key lookup.
2. **`VERIFYOUT(@INPUT addr amount @TOKENID FALSE)`** — every branch pins its exact
   payout. No branch pays out without a `VERIFYOUT`.
3. **No unguarded `SIGNEDBY` path anywhere** — the cardinal rule (section 1) applies
   to MAST leaves too. An unpinned `SIGNEDBY … RETURN TRUE` leaf is the same drain.
4. **Precompute the MMR proofs** for each leaf so the disputing party can spend
   without recomputing chain state under time pressure.

### Testing a MAST spend offline

```
runscript script:"MAST 0x<root>" extrascripts:{"<the branch script>":"0x<proof>"}
```

The `extrascripts` map pairs each revealed branch's source with its MMR proof — the
node checks the branch hashes into the committed root before running it.

### Why it's safe / unsafe

- **Safe:** the common (co-operative) path never reveals the dispute logic, so it
  stays small and cheap; a griefer cannot spend a cold branch early because the
  branch's own `@COINAGE` deadline and `SIGNEDBY` gate it.
- **Unsafe if a branch skips `VERIFYOUT`:** a 1222-char contract once silently failed
  (over the size limit) and lost ~343 MINIMA — the reason cold paths *must* be MAST'd
  off rather than inlined. And an unpinned dispute leaf reintroduces the section-1
  drain on the disputed funds.

---

## 5. Fixed-exchange / limit-order DEX covenant

A limit order locks the maker's coin in a covenant that allows exactly three things:
the maker cancels (funds return to the maker), the order expires (funds return to the
maker), or someone fills it (maker receives the exact want-amount/token/address). This
is the **V4** design — the incident-hardened version.

### The covenant (V4 — every path VERIFYOUT-pinned)

```
LET u=0xFF..allowedToken
IF @TOKENID NEQ 0x00 THEN IF @TOKENID NEQ u THEN RETURN FALSE ENDIF ENDIF
IF PREVSTATE(3) NEQ 0x00 THEN IF PREVSTATE(3) NEQ u THEN RETURN FALSE ENDIF ENDIF
IF SIGNEDBY(PREVSTATE(0)) THEN ASSERT VERIFYOUT(@INPUT PREVSTATE(1) @AMOUNT @TOKENID FALSE) RETURN TRUE ENDIF
IF @COINAGE GT 1500 THEN ASSERT VERIFYOUT(@INPUT PREVSTATE(1) @AMOUNT @TOKENID FALSE) RETURN TRUE ENDIF
ASSERT VERIFYOUT(@INPUT PREVSTATE(1) PREVSTATE(2) PREVSTATE(3) FALSE) RETURN TRUE
```

State ports: `0` = owner pubkey, `1` = want (maker's own) address, `2` = want amount,
`3` = want tokenid.

Three paths, **every one pinned with `VERIFYOUT`**:

- **Owner-cancel** — `SIGNEDBY(PREVSTATE(0))` but the output is pinned back to
  `PREVSTATE(1)` (the maker's own address). This is the fix for the section-1
  incident: the owner can trigger a cancel but cannot redirect the coins.
- **`@COINAGE GT 1500` expiry** — anyone can trigger it (no signature), but the funds
  can only go to `PREVSTATE(1)`. This trustless timeout-return path is what saved the
  token orders in the incident.
- **Fill** — the taker must produce the exact `PREVSTATE(2)` amount of the
  `PREVSTATE(3)` token to `PREVSTATE(1)`. A token allow-list (`u`) is enforced up front.

### Ship all versions simultaneously

You cannot upgrade a covenant coin in place — a coin created under V1 can only be
spent by satisfying the V1 script. So register **all** versions at startup
(`newscript` V1..V4) so that orders created under older versions stay spendable while
new orders use the hardened script.

### The silent Script-FAIL lesson

After the owner branch gained its `VERIFYOUT(@INPUT PREVSTATE(1) @AMOUNT @TOKENID
FALSE)` requirement, an auto-refresh routine kept sending its refreshed output back to
the **contract address** instead of `PREVSTATE(1)`. Result: **every auto-refresh
silently Script-FAILed at mining for weeks** — `txnpost` returned `status:true`
(accepted to mempool) and the dapp logged "Refreshed," while the coin never moved.

> `txnpost status:true` means "accepted to mempool," NOT "will mine." A covenant
> Script-FAIL is silent. Reproduce the exact contract-output **shape** offline
> (`minima-vm` / `runscript`): the refresh shape returns `ASSERT failed`; the collect
> shape passes. Verify shape, never trust the post result.

### GTC (good-till-cancelled) without refreshing a covenant

You cannot "refresh" a covenant coin (any spend must satisfy the covenant). So GTC is
a **two-transaction CANCEL (funds → wallet) then RECREATE (fresh re-lock)**,
coordinated through a shared SQL table with `PRIMARY KEY(orderid)` so a page and a
service (separate JS contexts sharing only SQL) can't double-fire. **Persist before
broadcast**; treat "funds missing" as "the order was filled — never re-lock." Carry
the GTC flag in an unused state port (e.g. port 7 = "1") that the covenant ignores
(ports > 3 are unused) → interop-safe and invisible to the contract.

### Why it's safe / unsafe

- **Safe:** the trustless `@COINAGE` expiry return path means funds are recoverable
  even if the app dies, the maker loses their key-access, or a write-permission app
  goes rogue — the chain returns them to the maker after the deadline.
- **Unsafe (the V1/V2 lesson):** the unpinned owner-cancel path is exactly the
  section-1 drain. The **trustless timeout-return path saved funds where the unpinned
  cancel lost them** — build the timeout path even if you also have a cancel path.

---

## 6. Constant-product AMM on the UTXO model

A stateless constant-product AMM where **a pool is just two reserve coins** — a native
MINIMA coin (reserve `x`) and a token coin (reserve `y`) — sitting at one covenant
address. Anyone swaps by spending both coins and recreating them, provided the
constant-product invariant (minus a fee) still holds. The fee stays in the pool, so
`x·y` grows with volume — that growth is the LP's reward. No LP token, no batcher, no
shared custody.

### Why the params are hardcoded literals

A Minima transaction has **one shared STATE array**; every `keepstate` output
receives it. Two pools routed through one transaction could not each pin their own
params via coin state. So the four params are **baked into the script as literals**:

- `$OPK` — owner pubkey (a *dedicated* key, never the LP's spending key)
- `$OADR` — owner payout address
- `$TOK` — the paired token id
- `$KMIN` — the product floor (see below)

Because the params are literals, **each pool's address = its script hash**, the pool
carries **zero state**, and everything uses `storestate:false`. Multi-pool routing in
one transaction "just works" because each leg self-pins by coin index.

### The covenant

```
IF SIGNEDBY($OPK) THEN
  IF VERIFYOUT(@INPUT $OADR @AMOUNT @TOKENID FALSE) THEN RETURN TRUE ENDIF
  RETURN GETOUTADDR(@INPUT) EQ @ADDRESS AND GETOUTTOK(@INPUT) EQ @TOKENID AND GETOUTAMT(@INPUT) GTE @AMOUNT
ENDIF
IF @TOKENID EQ 0x00 THEN
  ASSERT @INPUT % 2 EQ 0 LET s=@INPUT+1
  ASSERT GETINADDR(s) EQ @ADDRESS AND GETINTOK(s) EQ $TOK
  ASSERT GETOUTADDR(s) EQ @ADDRESS AND GETOUTTOK(s) EQ $TOK
  LET x=@AMOUNT LET y=GETINAMT(s) LET nx=GETOUTAMT(@INPUT) LET ny=GETOUTAMT(s)
  ASSERT VERIFYOUT(@INPUT @ADDRESS nx 0x00 FALSE)
ELSE
  ASSERT @TOKENID EQ $TOK AND @INPUT % 2 EQ 1 LET s=@INPUT-1
  ASSERT GETINADDR(s) EQ @ADDRESS AND GETINTOK(s) EQ 0x00
  ASSERT GETOUTADDR(s) EQ @ADDRESS AND GETOUTTOK(s) EQ 0x00
  LET y=@AMOUNT LET x=GETINAMT(s) LET ny=GETOUTAMT(@INPUT) LET nx=GETOUTAMT(s)
  ASSERT VERIFYOUT(@INPUT @ADDRESS ny $TOK FALSE)
ENDIF
LET dx=nx-x LET dy=ny-y LET fx=MAX(dx 0)*5/1000 LET fy=MAX(dy 0)*5/1000
RETURN (nx-fx)*(ny-fy) GTE MAX(x*y $KMIN)
```

### The invariant (0.5% fee, fail-closed)

```
LET dx=nx-x LET dy=ny-y LET fx=MAX(dx 0)*5/1000 LET fy=MAX(dy 0)*5/1000
RETURN (nx-fx)*(ny-fy) GTE MAX(x*y $KMIN)
```

Accept a swap iff `(nx − fee·dx)·(ny − fee·dy) ≥ max(x·y, KMIN)`, with the fee only on
the side that grew (`MAX(d,0)`). The fee is **divided first** so the largest
intermediate is `(nx−fx)·ny` rather than a cross-multiplied form ~1000× larger that
would overflow much sooner. MiniNumber's magnitude ceiling is `2⁶⁴−1 ≈ 1.846e19`; any
intermediate past it makes the script throw, which the VM turns into a **clean swap
rejection** (`success=false`) — never a false-accept, never a crash. The invariant is
self-limiting.

### Legs paired by coin-index parity

The MINIMA coin sits at an **even** input index `2i`, its token coin at `2i+1`,
outputs mirrored (`@INPUT % 2`). Each leg binds its sibling by address + token id (on
both input and output), pins its own recreation with a 5-param
`VERIFYOUT(... storestate:false)`, and the final line enforces the fee-loaded product
floor. Because a signature is transaction-wide, a signed transaction takes the owner
branch on *both* legs — it can never mix owner pinning on one leg with swap logic on
the other.

### KMIN — the one non-obvious defense unique to the 2-coin UTXO model

Because a pool is two coins with **no unforgeable cross-coin binding**, anyone can
send a **dust token coin** to the pool address and pair it with the pool's real MINIMA
coin. Without a floor, the invariant `nx·ny ≥ x·y` anchors to `x·y_dust ≈ 0` and the
MINIMA leg **drains for dust**. The fix is a hardcoded floor:
`KMIN = SIGDIG(20, x0·y0)` (the creation product to 20 sig-figs, rounded down). A
forged-dust pairing must then *restore the full creation product* to extract
anything — strictly worse than swapping honestly. Constraint: **`KMIN ≥ 2⁶⁴` won't
compile**, so pools have a max size (`x0·y0 < 2⁶⁴`); split large pools into several.

### Rounding

All rounding is DOWN and pool-favourable: round recreate-reserves **UP** to token
grain; round proceeds / change **DOWN**. Every truncation favours the pool.

### THE FUND-CRITICAL TRAP: a `/` in a covenant literal

The invariant contains `*5/1000`. Java's `JSONObject.quote` (and `JSONStringer`)
**escapes forward slashes**: `*5/1000` becomes `*5\/1000`. Passed to
`newscript`/`runscript`, the node keeps the backslash literally and the KISS parser
fails (`parseok=false`) — and because the failure is at parse/load time, **no branch,
not even the owner-exit, can ever execute → any coin sent to that address is
PERMANENTLY UNSPENDABLE.**

This is not theoretical. A pre-fix build created a live pool whose on-chain covenant
literally stored the escaped fee literal `*5\/1000`; a large reserve balance plus a later mistaken direct
`send` to that dead address are permanently stuck. Proven unrecoverable: even on a
node holding the exact tracked script, correct key-uses, full MMR proofs, and a valid
owner signature, `txncheck` returned `valid.scripts:false` and every `txnpost` was
rejected — because the tracked script's `parseok=false`. **No key, keyuses, node, or
seed can spend an unparseable covenant.**

Defenses:

- **Quote scripts escaping only `"` and `\`, never `/`.** Python's `json.dumps` does
  *not* escape `/`, so an RPC/Python harness passes while the Java IPC path fails —
  test script-passing on the ACTUAL path funds will move through.
- **Prove `parseok` via the node's own `runscript` BEFORE funding any script
  address.** This turns a permanent fund-loss into a harmless "aborted, nothing moved."
- **Canonicalise numeric literals** (`stripTrailingZeros().toPlainString()`) so the
  covenant, any stored-state copy, and the re-derived address all agree
  byte-for-byte — else discovery re-derives a *different* (empty) address than where
  reserves landed.
- **Keep `x·y < 2⁶⁴`** so KMIN compiles.

### Post-gating

Every post is gated on the covenant's own `txncheck` verdict: post only if **all** of
`response.valid.scripts` AND `response.validamounts` AND `response.valid.mmrproofs`
are truthy; else delete the txn. Read `valid.scripts` (the covenant verdict), NOT
top-level `scripts` (a count of distinct input scripts).

### Funding trap (owner-key auto-sign)

A swap is anyone-can-spend and must NOT be signed by `$OPK`. But if the taker funds
the swap from a coin at `$OADR` (the owner payout address, whose key is `$OPK`),
`txnsign auto` adds `$OPK`'s signature → the covenant takes its **owner** branch →
rejects the reserve-shrinking swap. Fixes: swap funding selection must **exclude** the
pool's owner address(es), and create/deposit/migrate change must go to a *non-owner*
wallet address.

### Transaction templates (all `storestate:false`, no state ports)

```
CREATE   — send x0 MINIMA + y0 token to derived pool address A; post an announce coin.
SWAP M→T — inputs [pool-M@A, pool-T@A, taker MINIMA funding…];
           outputs [(A,nx,0x00),(A,ny,$TOK),(taker,dy,$TOK),(taker change,0x00)]. Sign taker inputs only.
SWAP T→M — taker funds a token coin; outputs [(A,nx,0x00),(A,ny,$TOK),(taker,dm,0x00)].
ROUTED   — pool i's MINIMA coin at input 2i, token at 2i+1, outputs mirrored; ~8-10 pools per 64KB TxPoW.
DEPOSIT  — owner-signed grow-in-place: [(A,x+Δx),(A,y+Δy)], amounts ≥ inputs.
MIGRATE  — owner-signed: exit old reserves to $OADR AND fund new pool coins at new address A' (new KMIN).
CLOSE    — owner-signed: [(A→$OADR,x,0x00),(A→$OADR,y,$TOK)].
```

Pools are discovered via a shared sentinel address (an announce coin whose reclaim key
is `PREVSTATE(0)`), but the client **trusts nothing** — it re-derives the pool script
from `{tokenid, $OADR, $OPK, $KMIN}`, gets the clean address via `runscript`, and only
then scans that address for reserves.

### Why it's safe / unsafe

- **Safe:** owner-exit path runs *before any arithmetic that could throw*, so a
  pool is always owner-recoverable; KMIN blocks the dust-pairing drain; overflow
  fails closed; every truncation favours the pool.
- **Unsafe:** a `/`-escaped literal or a non-canonical KMIN strands funds forever —
  which is exactly why the `parseok` pre-flight in safety rule 1 is non-negotiable.

---

## 7. Prediction-market / escrow covenant

The live productization of the multi-phase style (section 2), but arbiter-based rather
than hash commit-reveal — for outcomes that can't be settled by cryptographic
randomness (real-world events). An 8+ port state machine (port 4 = phase 0 open /
1 matched, port 11 = outcome) with these paths:

- **owner-cancel** — while still open (unmatched), returns the maker's stake.
- **anyone-fill** — a counterparty matches the market, combining both stakes.
- **two-party self-settle (0% fee)** — both parties agree the outcome and split
  directly, no arbiter fee.
- **arbiter-resolve (fee % of pot)** — if the parties disagree, a named arbiter
  decides; the covenant enforces the payout and takes a fixed % of the pot.

Plus **MAST leaves** for refresh / timeout / void, each with a precomputed MMR proof.

### Escrow lock enforced by paired VERIFYOUT

Each side locks `bet × 1.25` (escrow = lock / 5). The self-settle escrow math is
enforced by **paired outputs** — `VERIFYOUT(@INPUT ...)` for one party and
`VERIFYOUT(@INPUT+1 ...)` for the other — so the settlement can only pay both sides
their contract-correct amounts. A `@COINAGE` timeout-refund path is the trustless
backstop if the arbiter or a party disappears.

### Pin the proposition

Pin every state variable a counterparty must not rewrite with `SAMESTATE` over the
range — the proposition, funding amount, arbiter identity, outcome port. Never key
application logic on **free-form text** (a proposition string or token name is
spoofable / duplicable); key on a fixed-width random nonce pinned on-chain, and
resolve parties by their on-chain-pinned pubkeys.

### The social-vs-cryptographic arbiter-trust caveat

A covenant can enforce that the arbiter's key differs from both parties' keys — it
**cannot** enforce that the arbiter is *independent*. Cryptography cannot fix a social
trust assumption. Surface the arbiter identity in the UI and state the residual trust
plainly: this is trust-minimised, not trustless, and the trust that remains is that
the arbiter is honest and independent.

### Why it's safe / unsafe

- **Safe:** the 0%-fee self-settle path means honest parties never pay the arbiter;
  the timeout-refund path recovers funds if anyone vanishes; paired `VERIFYOUT`
  makes an unequal / rewritten settlement un-signable.
- **Unsafe:** if the arbiter path lacks a `VERIFYOUT` pin on the payout, or the
  proposition ports aren't pinned with `SAMESTATE`, a matched market can be settled to
  the wrong party or the wrong amount.

---

## 8. StateNFT — a collection sharing ONE token id

A whole NFT collection under **one** Minima token id, where each coin's STATE carries
its own identity — enforced by consensus so it survives transfers and cannot be
stripped or forged. (Proven on-chain, mainnet.)

Minima tokens are coloured coins: token metadata (name/url/description) is fixed at
`tokencreate` for *every* coin of the token id, so per-NFT images cannot live there.
Instead:

1. **One `tokencreate amount:N decimals:0`** → N indivisible units, one token id.
   Metadata holds collection-level data (base image URL, extension, size).
2. **Per-coin STATE = identity.** Each unit coin carries state port 0 = item index
   (1..N). Image URL = `base + index + ext`.
3. **The immutable token script** validates every spend of every coin:

```
IF SIGNEDBY(0xFF..creatorpubkey) THEN RETURN TRUE ENDIF
RETURN VERIFYOUT(@INPUT GETOUTADDR(@INPUT) @AMOUNT @TOKENID TRUE)
```

A non-creator spend must recreate the coin at the same output index with the same
amount, token id, and **identical state** (`keepstate TRUE`). Identity travels with
the coin; a spend that strips it is rejected by the chain. The creator key is a
deliberate escape hatch (the mint/stamp path) — the standard "trust the minter"
assumption.

### Proven facts (mainnet)

- **State is per-TRANSACTION, not per-output** → stamping N distinct identities takes
  **N transactions** (one coin each; parallel-safe). You cannot mint N differently-
  stated coins in one transaction because a transaction has one shared state array.
- **TxPoW has a 64 KB cap** → ~3 token-carrying outputs + 1 signature per transaction
  is the practical ceiling. Keep coins at the creator address so one signature covers
  both the coin and the token script.
- **Script-token coins show `sendable:0`** in the wallet → plain `send` refuses them
  (which protects users from accidentally invalid transfers). All moves go through
  manually-constructed transactions.
- **A state-stripping spend posts with `status:true` but is REJECTED on-chain** (the
  coin never moves) → **never trust `txnpost` status alone.** This is the same lesson
  as the DEX silent Script-FAIL (section 5): mempool acceptance is not mining.

### Why it's safe / unsafe

- **Safe:** the `VERIFYOUT(@INPUT GETOUTADDR(@INPUT) @AMOUNT @TOKENID TRUE)` clause
  forces identity preservation on every non-creator spend — the chain, not the app,
  guarantees the image can't be swapped or stripped.
- **Unsafe (by design tradeoff):** the creator key can bypass enforcement forever —
  acceptable for a mint escape hatch, but it means holders trust the minter not to
  re-stamp. Surface this.

---

## 9. HTLC cross-chain atomic swap

A Hashed Time-Lock Contract swaps a coin on Minima for an asset on another chain with
**no trusted intermediary**. The same SHA-256 hashlock is used on both chains' legs:
claiming the far leg reveals the preimage, which then unlocks the Minima leg (or vice
versa). Each leg has a timelock refund path so nobody's funds get stuck if the swap is
abandoned.

### The Minima leg as a covenant

```
IF SHA256(secret) EQ hashlock AND SIGNEDBY(0xFF..claimant) THEN
  ASSERT VERIFYOUT(@INPUT 0xFF..claimantaddr @AMOUNT @TOKENID FALSE) RETURN TRUE
ENDIF
IF @COINAGE GT timeout AND SIGNEDBY(0xFF..refunder) THEN
  ASSERT VERIFYOUT(@INPUT 0xFF..refunderaddr @AMOUNT @TOKENID FALSE) RETURN TRUE
ENDIF
RETURN FALSE
```

- **Claim path:** whoever knows `secret` (such that `SHA256(secret)` matches the
  agreed `hashlock`) and holds the claimant key sweeps the coin — and in doing so
  publishes `secret` on-chain, which the counterparty uses to claim the other leg.
- **Refund path:** after `@COINAGE GT timeout` the funder (refunder) reclaims. The
  refund timeout on the Minima leg must be **longer** than on the leg the counterparty
  claims first, or the counterparty could claim and then let their own refund fire.

Both payout addresses are pinned with `VERIFYOUT`, so neither path can redirect funds.
Order boards / swap offers are published over **shared sentinel addresses** (the same
re-derive-and-verify discovery pattern as the AMM registry) rather than a central
server.

### Why it's safe / unsafe

- **Safe:** the hashlock links the two legs cryptographically — either both settle or
  both refund; no party can take one leg without exposing the secret that settles the
  other.
- **Unsafe if timeouts are mis-ordered:** if the Minima-leg refund fires before the
  far-leg claim window closes, a counterparty can claim your coin *and* refund theirs.
  Order the timelocks so the party who reveals the secret first has the *later* refund.

(The canonical worked version is the official HTLC tutorial — see below. Keep your
implementation genericised: no exchange- or token-specific assumptions in the leg.)

---

## 10. ELTOO / floating coins & payment channels

An off-chain payment channel where the two parties exchange signed states off-chain
and only settle on-chain when they disagree or close. ELTOO lets a newer state
override an older one (by sequence number), so you don't need a full penalty-based
justice system — the latest state always wins.

### Channel state-port layout

```
Port 100: settlement flag (TRUE/FALSE)
Port 101: sequence number (increments with each state update)
Port 102: game phase (0=idle, 1=active)
Port 103: bet / update amount
Port 104: range (2=flip, 6=dice, 36=roulette)   -- for game-aware channels
Port 105: player commit hash (SHA3 of secret)
Port 106: house commit hash
Port 107: player's pick
Port 108: bettor indicator (1=user1, 2=user2)
Port 109: user1 payout address (FROM PREVSTATE, never getaddress)
Port 110: user2 payout address (FROM PREVSTATE, never getaddress)
Port 111: user1 pre-bet amount
Port 112: user2 pre-bet amount
Port 115: user1 public key (for MAST SIGNEDBY)
Port 116: user2 public key
Port 200: channel hashid (for payout tracking)
```

### "All addresses come from PREVSTATE, never getaddress at runtime"

This is the load-bearing rule. `getaddress` **rotates keys** — an address it returns
today can become `relevant:false` after a vault restore, key regeneration, or resync,
and any funds pinned to it are lost. Store both parties' payout addresses in the coin
STATE at channel creation and read them with `PREVSTATE` in every branch. The ~5,000
MINIMA loss traces directly to `getaddress`-derived destinations going unrecognised.

### Floating coins / precoin

A **floating** transaction (`@FLOATING`) is one whose input coin isn't pinned to a
specific coinid yet — it can attach to whichever coin ultimately carries the channel
state. **Precoins** let you pre-sign channel updates for coins that don't exist yet,
which is what makes off-chain ELTOO state exchange possible: you sign the *next*
state before the *current* one is even on-chain.

### Why it's safe / unsafe

- **Safe:** MAST dispute branches (section 4) at escalating deadlines mean either
  party can force settlement of the latest agreed state on-chain; sequence numbers
  ensure a stale state can't override a newer one.
- **Unsafe:** any `getaddress`-at-runtime address, or a channel over the ~1150-char
  size limit (the 343-MINIMA loss), or a missing `SAMESTATE` pin on the payout
  addresses/pubkeys, reintroduces a fund-loss path. Check `wc -c` on the main script
  plus MAST root before deploying.

---

## Covenant safety checklist

Before you fund a covenant or ship a change, walk this list:

- [ ] **Prove `parseok`** via the node's own `runscript` on the exact bytes, and
      confirm re-derivation to the funding address, BEFORE any coin moves.
- [ ] **Every reachable branch is `VERIFYOUT`-pinned** — no unguarded
      `SIGNEDBY … RETURN TRUE` on a coin holding funds.
- [ ] **Literals are canonical** (`stripTrailingZeros().toPlainString()`) and match
      any stored-state copy byte-for-byte.
- [ ] **No `/` gets JSON-escaped** into the script; test on the actual command path
      that funds will move through, not just a Python harness.
- [ ] **Grain rounding is pool/contract-favourable** — reserves UP, proceeds DOWN;
      all money math in `BigDecimal`/`decimal.js`, never floats.
- [ ] **Owner-key coins are excluded** from funding an anyone-can-spend transaction;
      sign with an explicit key, never `auto`, near covenants.
- [ ] **Output SHAPE verified offline** (`minima-vm` / `runscript` / `txncheck`) — not
      the `txnpost` result. `txnpost status:true` ≠ on-chain success.
- [ ] **Post-gate reads `valid.scripts`** (the verdict) with a truthy helper, not
      top-level `scripts` (a count).
- [ ] **Cold paths are MAST'd off** under the ~1150-char size limit, each with a
      precomputed MMR proof, each pinned and `SIGNEDBY`-gated from `PREVSTATE`.
- [ ] **All addresses come from `PREVSTATE`**, never `getaddress` at runtime.
- [ ] **`SAMESTATE` pins the whole immutable region** a counterparty must not rewrite.
- [ ] **`txndelete` on every path, including failures**, so a half-built transaction
      never leaks into the next operation.
- [ ] **Keep negative test vectors** (wrong-split settlement, rewritten proposition,
      under-funded fill, dust-pairing drain) and prove the chain rejects them.
- [ ] **On any spend failure, diagnose from data first**: `scripts address:X` →
      `runscript` → check `parseok`. Do not chase keys/keyuses/rebuilds.

---

## Official tutorials (canonical worked versions)

The Minima docs ship official, worked layer-1 and layer-2 tutorials that are the
canonical reference implementations for these patterns. Consult them for the reference
version; use *this* file for what breaks in production:

**Layer 1 (on-chain covenants):** basic-contract, coinflip, complex-multisig,
exchange-contract, flashcash, hashed-timelock-contract (HTLC), mast-contract,
mofnmultisig, multisig, multisig-multicoin, slowcash, thevault, time-lock-contract.

**Layer 2 (channels & scaling):** bi-directional-payments, coinflipv2, eltoo-channel,
eltoo-floating-coin, eltoo-full-sequence, eltoo-precoin, state-chains,
uni-directional-channel.

These cover multisig, HTLC, time-lock, vault, exchange, coinflip, ELTOO, and
state-chains as official worked tutorials — the canonical versions of the patterns
taught above.


---

## reference: diagnostics-recovery
<!-- (was references/diagnostics-recovery.md) -->

# Diagnostics & Recovery

The "something is wrong" runbook. Load this when a transaction failed, funds look stuck, a
covenant won't spend, or a node misbehaves. Every claim here comes from a real on-chain
failure. Sections go symptom → cause → fix, and point at the deeper reference file.

## First move: diagnose from data, not guesses

When a fund-critical operation fails — especially a covenant spend — the single most
expensive mistake is to start *guessing* causes and taking irreversible actions (restore
from backup, re-import keys, full seed-resync, stand up a second node) before you have read
the actual on-chain data.

A real case cost **hours** and an unnecessary seed-resync chasing a WOTS key-use / leaf
theory, when the true cause was readable in **seconds** from the exact stored script bytes.

The discipline:

1. **Get the runtime evidence first.** Read the real state — `scripts address:X`,
   `coins`, `history`, the node logs — before hypothesizing.
2. **Treat a discriminating clue as a precise signal.** "1000 works, 3000 fails" points
   straight at a size-dependent path (coin selection), not identity or balance. "It only
   happens on the new install" points at what that build changed.
3. **State confidence honestly.** "The trace proves X; I have no data on Y so I won't
   guess" beats another confident-but-wrong theory. If you can't tell from the data, go
   get the data.
4. **On likely fund loss, say so early.** "This is probably gone; one check to confirm"
   is kinder and cheaper than hours of false hope followed by the same conclusion.

For a failing covenant spend the very first command is **always**:

```
scripts address:<the covenant/script address>     # get the EXACT stored bytes
runscript script:"<those exact bytes>"             # does it even parse?
# check parseok in the response
```

If `parseok` is **false**, the coin is **permanently unspendable** — no key, keyuses, node
rebuild, or seed resync can help. Stop, and prevent it next time (prove `parseok` before
funding — see `references/transactions-utxo.md`). The node's local `keys` "uses" counter is
a local tally and is **unrelated** to why a spend is rejected — do not chase it.

---

## Script-FAIL taxonomy

A transaction that "posts fine" but never mines, or a `txncheck` returning
`valid.scripts:false`, has one of a few distinct causes. Tell them apart before acting.

### 1. Parse failure — `parseok:false` (the dead end)
The script the node stored does not parse, so **no branch can ever run** — not even an
owner-exit. The classic cause is a covenant literal containing `/` that a JSON quoter
escaped to `\/` (Java's `org.json.JSONObject.quote` does this; Python's `json.dumps` does
not — which is why an RPC/Python harness passes while the real app fails). The failure is at
parse/load time, so it fires regardless of which branch you intended to hit.

- **Diagnose:** `scripts address:X` → `runscript` the exact bytes → `parseok:false`.
- **Fix:** none — the coins are gone. Audit any sibling coins funded by the same buggy
  code. Quote scripts without escaping `/` (escape only `"` and `\`), and **prove `parseok`
  before funding** so a bad covenant aborts with nothing moved.

### 2. Wrong-address / canonical-literal mismatch
A covenant literal that differs by even one character from what a registry or discovery
beacon stored produces a **different script hash → a different address**. Reserves land at
one address; discovery re-derives another (empty) one. The pool looks "invisible."

- **Diagnose:** derive the address from both the canonical and the as-stored literal and
  compare. Node-normalized numeric forms (e.g. trailing zeros stripped) are the usual
  culprit.
- **Fix:** canonicalize every literal that is both baked into a covenant and stored for
  re-derivation (`stripTrailingZeros().toPlainString()`), so covenant, stored state, and
  re-derivation all agree.

### 3. Branch failure — script parses but `RETURN` is FALSE / no branch matched
The covenant ran and *refused*. Causes: wrong state values, the wrong signer, a keepstate
mismatch, or — most commonly — the transaction's **output shape doesn't match what the
covenant requires** (e.g. an app refreshing output to the contract address while the
covenant demands output to `PREVSTATE(1)`; that Script-FAILs silently while `txnpost`
returns `status:true`, so it can run "successfully" for weeks).

- **Diagnose:** verify the output **shape** offline against `minima-vm` (see
  `references/ecosystem-tooling.md`), not via the post result. Check every `VERIFYOUT`
  the covenant enforces against what your transaction actually produces.
- **Fix:** correct the output address/amount/tokenid/keepstate to satisfy every reachable
  branch's `VERIFYOUT`. See `references/contracts-covenants.md`.

### 4. "Script Missing from TxPoW for address 0x… → TxPoW FAILS Basic checks"
The coin's address is **not tracked on this node** — the untracked-coin problem, common on
megammr/fresh nodes. The coin is safe on-chain, just unspendable here.

- **Fix:** track it, then spend — see **Untracked-coin recovery** below and
  `references/node-operations.md`.

### 5. "TokenID … doesn't match token"
You rebuilt a token descriptor by re-serializing its JSON, which reordered keys → different
bytes → a different (wrong) tokenid.

- **Fix:** take the **byte-exact** token from the binary `coinexport` CoinProof; never
  reconstruct it from a JSON object.

---

## "txnpost said status:true but the coin never moved"

`txnpost status:true` means **accepted to the mempool**, not mined and not valid. A covenant
Script-FAIL is **silent**: the post succeeds, the transaction never mines, and the coin stays
put (while a naive app logs "success"). This is the single most misleading signal in Minima.

- **Confirm the real outcome** by re-querying `coins`/`history` for the spent input and the
  expected output — not by the post result.
- **`istransaction:false` on a `txnpost` return is asynchronous mining in progress, NOT
  failure.** Do **not** retry on it — retrying causes a re-post storm that races itself and
  stalls confirmation for minutes.
- `txnsign txnpostauto:true` returns a txpowid that is **not** the final on-chain id;
  resolve the real one after mining by matching the spent input coinids against `history`.

---

## Locked inputs & "insufficient funds" that shouldn't be

Once a coin is added to a transaction with `txninput`, it is **locked** to that pending
transaction. If the build fails partway and you don't clean up, the coin stays locked and
later spends fail with "insufficient funds" even though a balance clearly exists.

- **Diagnose:** `txnlist` shows pending transactions still holding inputs.
- **Fix:** `txndelete id:<txnid>` — and always run it on every error path of any manual
  build. See the post sequence in `references/transactions-utxo.md`.

Related UTXO-lock symptoms: a coin is locked until the previous spend of it confirms; two
simultaneous spends of the same coin cross and one fails (use a single-writer lock); and
change returns to a **fresh** address on every send, so never assume it came back to the
same address.

---

## Node returned empty (not an error) / oversized reply

A query whose reply exceeds the node's ~256 KB "results too long" stub comes back **empty**,
not as an error — so an unbounded `history` or `coins` scan silently yields nothing.

- `history` returns full txpow bodies (~14 KB each); `coins` has **no count cap**, only
  `depth:` (a time bound), so a dust-flooded public address can overflow.
- **Fix:** bound every scan — e.g. `history max:4 depth:400` — and use **adaptive paging**:
  start `max:8`, halve on any empty/over-limit/errored page (8→4→2→1), keep the smaller size.
- On native Android IPC an oversized reply can **kill the app uncatchably** (the Binder
  ~1 MB limit) — you must return less up front. See `references/native-android-ipc.md`.

---

## Untracked-coin recovery

Symptom: the coin is visible in `coins` (or via `coins megammr:true`) but any spend fails
with "Script Missing from TxPoW". `txnbasics` and `coinexport` read only the node's
**tracked** MMR/script DB; `coins megammr:true` returns coin *data*, not a spendable proof.

Fix, in order, before `txnbasics`/`txnpost`:

```
newscript trackall:true script:"RETURN SIGNEDBY(0x<your-pubkey>)"   # register your script
coinexport coinid:<FULL 66-char coinid>                            # pull the proof blob
#   a TRUNCATED coinid gives a false "Coin not found"
coincheck data:<blob>                                              # optional validate
coinimport data:<blob> track:true                                 # insert into tracked set
#   coin is now coins relevant:true → txnbasics has script + proof → spend broadcasts
```

For a **token** coin, rebuild the Token from the byte-exact `coinexport` CoinProof, never by
re-serializing the token JSON (see Script-FAIL #5). Balance display: for an untracked
address, `balance megammr:true address:X` shows `sendable:0` while `confirmed` holds the real
amount (`total` is the token *supply*) — display `confirmed`. Full runbook:
`references/node-operations.md`.

---

## Wallet & key symptoms

- **"signing failed: Public Key not found" right after a seed restore** — the node's keys
  are still regenerating asynchronously. Wait / ensure keys exist before signing; do not
  bulk-regenerate keys on launch (it triggers a key-creation and pending-approval storm).
- **`publickey:auto` can't sign a retired (hardened) address** — after you retire an address
  by nulling its script-row pubkey to `0x00`, `auto` no longer finds a key. Extract the real
  key from the script and pass it explicitly. See `references/key-security-wots.md`.
- **Address case mismatch (`0x…` vs `0X…`)** → silent `0` uses / a lookup that finds nothing.
  Normalize case before comparing addresses.
- **A spend from your own owner address unexpectedly trips a covenant's owner branch** —
  `txnsign publickey:auto` signed with the owner key. Exclude owner/pool addresses from
  funding and change selection.

---

## Reading-the-chain pitfalls

- **`coins relevant:true` is not a full ownership view.** It includes imported watch-only
  and anyone-can-spend covenant coins; `track:false` coins are absent. Ownership is
  per-coin — check the coin's own state/script, not just relevance.
- **State is a LIST** of `{port,type,data}`, never a map — always iterate to find a port.
- **`history difference` nets ALL tracked coins**, including imported covenants, so a real
  swap can net to `{0,0}` and look like a self-transfer. Recompute over your own
  `simple:true` addresses.
- **The archive retains coins, not witnesses.** Signatures/inputs/outputs of deep-history
  spends are discarded, so you **cannot** determine an old spend's destination from the
  archive alone; `txpow address:` only searches roughly the last ~13k blocks.
- **Don't poll `archive action:integrity`** (3+ minutes), and don't fire concurrent archive
  queries — they can OOM a bounded-heap JVM.

---

## Case study: the permanently-stuck covenant (why "prove parseok" exists)

A pre-fix build created a live pool whose on-chain covenant literal contained `*5/1000`,
which a JSON quoter had escaped to `*5\/1000`. The node stored the backslash literally, so
the script's `parseok` was **false**. Around **~200,000 MINIMA plus tokens** — the small
original reserves plus a later mistaken direct `send` of a large amount to that same dead
address — became **permanently unspendable**.

It was proven unrecoverable the hard way: on a backup-restored node holding the exact tracked
script, the correct keyuses, full megammr proofs, and a valid owner signature, `txncheck`
still returned `valid.scripts:false` and every `txnpost` was rejected — because the tracked
script does not parse. No key, keyuses, node rebuild, or seed can spend an unparseable
covenant.

The lesson is rule #7 of the skill: **never send funds to a derived script address without
first proving, via the node's own `runscript`, that the covenant `parseok == true`** (ideally
that it re-derives to the exact funding address too). A non-parsing script's coins are gone
forever; a parsing covenant with an owner branch is always owner-recoverable. Gate any
create/fund/migrate on `parseok` **before** building a transaction, and a bad covenant aborts
with nothing moved.

---

## Fast triage table

| Symptom | Most-likely cause | First command | Open |
|---|---|---|---|
| Covenant spend rejected / `valid.scripts:false` | Unparseable stored script (`\/` etc.) | `scripts address:X` → `runscript` → check `parseok` | `references/transactions-utxo.md` |
| `txnpost status:true` but coin never moved | Silent Script-FAIL or async mining | re-query `coins`/`history` for the input | `references/contracts-covenants.md` |
| "Script Missing from TxPoW" | Address not tracked on this node | `newscript trackall:true` → `coinexport` → `coinimport track:true` | `references/node-operations.md` |
| "TokenID doesn't match token" | Token rebuilt from re-serialized JSON | use byte-exact token from `coinexport` | `references/node-operations.md` |
| "insufficient funds" but balance exists | Inputs locked to a failed pending txn | `txnlist` → `txndelete id:X` | `references/transactions-utxo.md` |
| Query returns empty inexplicably | Reply exceeded the ~256 KB stub | re-run bounded: `history max:4 depth:400` | `references/native-android-ipc.md` |
| "Public Key not found" after restore | Keys still regenerating async | wait / ensure keys before signing | `references/key-security-wots.md` |
| App killed / node crash on a big query | Android Binder ~1 MB overflow | return less up front (`depth:`, per-token) | `references/native-android-ipc.md` |
| Covenant took the wrong (owner) branch | `auto` signed with the owner key | exclude owner/pool addresses from funding | `references/contracts-covenants.md` |
| Pool/covenant "invisible" | Literal differs from stored → different address | compare derived addresses; canonicalize literals | `references/contracts-covenants.md` |


---

## reference: distribution-packaging
<!-- (was references/distribution-packaging.md) -->

# Distribution & Packaging — Shipping MiniDapps

How to package a `.mds.zip`, install it (UI / CLI / programmatic), publish it through an
app-store manifest, run the same store front-end across three capability tiers, and mirror
a catalog to IPFS. All patterns are generic and reusable.

## Packaging gotchas

Read these first — each one has silently broken a real install:

- **`dapp.conf` MUST be the first entry in the `.mds.zip`.** If it isn't, MiniHub
  *silently* fails to install — no error, the dapp just never appears. Build the zip so
  `dapp.conf` is added before anything else (see below).
- **Pending is signalled inconsistently.** A read-permission install lands in "Pending"
  for user approval, but the response sometimes carries `pending:true` and sometimes
  `status:false` with the word `"pending"` inside `error`. Detect both.
- **The sandbox `NetGET` returns a STRING.** In the web-embed sandbox, `miniweb_NetGET`'s
  `response` field is a JSON *string*, not an object — `JSON.parse` it.
- **File mode 644 or 403.** Every published file must be world-readable (mode 644, dirs
  755). A wrong-mode upload returns HTTP 403 and looks like "the store is broken."
- **Manifest fields have real type inconsistencies.** In the richer manifest dialect,
  `description`/`about`/`update` can each be a string *or* an array. Normalize before use.

---

## Packaging a `.mds.zip`

A MiniDapp is a plain web app (HTML/CSS/JS) zipped with the `.mds.zip` extension. Minimum
directory contents:

```
dapp.conf      # config/metadata (JSON) — MUST be zipped first
index.html     # entry page
mds.js         # the MDS JS library (MDS.* API)
service.js     # OPTIONAL background service that handles Main messages
icon.png       # app icon (name must match dapp.conf "icon")
assets/        # OPTIONAL folder of images, css, fonts, etc.
```

### Build command (dapp.conf first)

`zip -r` walks the directory in filesystem order, which does **not** guarantee
`dapp.conf` is the first archive entry. Force it explicitly:

```bash
# 1) add dapp.conf as the very first entry (-j = junk paths, no leading dir)
zip -j out.mds.zip dapp.conf
# 2) append everything else (-r recurse, -j flatten, or drop -j to keep asset paths)
zip -rj out.mds.zip index.html mds.js service.js icon.png assets
```

Verify the order — `dapp.conf` should be listed first:

```bash
unzip -l out.mds.zip | head
```

### `dapp.conf` format

JSON object of MiniDapp metadata read by the MiniDapp Hub:

```json
{
  "name": "Hello World",
  "icon": "favicon.ico",
  "version": "1.0",
  "description": "My Hello World MiniDapp",
  "browser": "internal"
}
```

Fields:

- **name** — the MiniDapp's display name and install identity. Stores match this
  case-insensitively against the installed list to decide Install vs Update.
- **icon** — filename of the icon in the same folder (e.g. `icon.png`, `favicon.ico`).
- **version** — dotted version string; compared numerically for update detection. Use
  semver (`MAJOR.MINOR.PATCH`).
- **description** — plain-text description.
- **browser** — `internal` (launch inside the app's webview) or `external` (open in the
  device browser). Mobile-interface hint.
- **permission** — some tooling records the requested trust level (`READ` / `WRITE`);
  actual permission is granted at install time, not from the file.
- **category** — optional store-categorization hint.

---

## Install paths

### 1. Via the MiniHub / Dapp Store UI

- **WRITE mode:** one-click install/update straight from a store row.
- **READ mode:** download the `.mds.zip` to the device first, then Home → `+` →
  **Choose File** → **Install**. (On Safari, disable "Open safe files after downloading"
  so the zip is not auto-unpacked.)

### 2. Via the terminal

```bash
mds action:install file:/path/to/out.mds.zip
# with an explicit trust level:
mds action:install file:/path/to/out.mds.zip trust:read
```

With `trust:read` the install lands in **Pending** and waits for the user to approve it
(the store only holds read permission, so it can stage but not authorize the mutation).

### 3. Programmatic install — `download → resolve path → action:install`

The canonical three-step recipe from JS. Download the zip, take the download response's
**full `path`** directly, and only fall back to `MDS.file.getpath` on the relative
`file`:

```javascript
MDS.file.download(dapp.file, function (res) {
  if (!res || res.status === false) { /* download failed */ return; }
  var full = res.response.download.path || "";  // FULL fs path — use this directly
  var rel  = res.response.download.file || "";  // relative /Downloads/<name>
  if (full) return runInstall(full);
  if (!rel) rel = "/Downloads/" + String(dapp.file).split("/").pop().split("?")[0];
  if (rel.charAt(0) !== "/") rel = "/" + rel;
  MDS.file.getpath(rel, function (p) {          // fallback: relative → absolute
    var path = (p && p.response && p.response.getpath.path) || "";
    if (!p || p.status === false || !path) { /* couldn't locate file */ return; }
    runInstall(path);
  });
});

function runInstall(path) {
  MDS.cmd("mds action:install file:" + path + " trust:read", function (r) {
    if (r && r.pending) { /* sent to Pending — user must approve */ }
    else if (r && typeof r.error === "string" && /pending/i.test(r.error)) {
      /* pending signalled as status:false + "pending" in error */
    }
    else if (r && r.status) { /* installed */ }
    else { /* install failed: r.error || r.message */ }
  });
}
```

Notes:

- Prefer the download response's **full `path`**; `getpath` on the relative `file` is only
  a fallback. (Earlier code used `getpath` unconditionally and mis-located files.)
- Delete the temp zip after install.
- **Pending is signalled inconsistently** — check `r.pending` *and* a `"pending"` substring
  in `r.error`.

### Installed-state detection

Query the installed set and compare versions:

```javascript
MDS.cmd("mds", function (r) {
  var installed = (r.response && r.response.minidapps) || [];
  installed.forEach(function (m) {
    // m.conf.name, m.conf.version
  });
  // match by lowercased name; compareSemver(catalogVersion, installedVersion)
  //   >0 → Update available;  ==0 → Installed;  not present → Install
});
```

---

## Update / versioning conventions

- Use **semver** in `dapp.conf` `version`.
- MiniDapps **do not auto-update** — the user decides. A store marks an update available
  when the catalog version is greater than the installed `conf.version` for a matching
  (lowercased) name.
- Always **Update** rather than delete-then-reinstall: update migrates the MiniDapp's
  database to the new version; delete destroys that data (on-chain coins are unaffected,
  but app-local data is lost).

---

## Two manifest dialects for app stores

Both dialects share a top-level shape:
`{ name, description, icon, version, dapps: [...] }`.

### Minimal dialect — lean and consistent

Every dapp entry has **exactly five string fields, all required, all present** — safe to
consume naively:

| field | meaning |
|---|---|
| `file` | absolute HTTPS URL to the `.mds.zip` |
| `icon` | absolute HTTPS URL to the icon |
| `name` | install identity (matched case-insensitively vs installed list) |
| `description` | plain string (always a string in this dialect) |
| `version` | dotted version, compared numerically |

### Mirror dialect — richer and inconsistent

The same 5 core fields **plus** optional metadata with real type inconsistencies a client
must tolerate:

- `description` — **string OR array-of-strings**.
- `about` — list on most entries, **string on a few**.
- `update` — list on most, **string on some**.
- `date` — string (e.g. `"April 14, 2025"`).
- `history[]` — heterogeneous: empty `{}`, or `{version, file:"", update:<str>}`, or
  `update` as a nested array; inner `file` is usually `""`.
- `category`, `gallery` (few); `dependencies` (rare, e.g. `["MiniFS"]`).

Keep a normalizer in any shared consumer:

```javascript
/* mirror-dialect quirk: description/about/update are string OR array */
function normDesc(d) { if (Array.isArray(d)) d = d.join(' '); return d || ''; }
```

Also **force-upgrade `http://`→`https://`** on `file`/`icon` before use, and carry a
`sha256` per entry so clients can verify the downloaded zip.

---

## Three front-end execution contexts = three capability tiers

The same store UI can run in three places with very different powers. Detect which you're
in and degrade gracefully:

1. **Inside a sandboxed web-embed iframe** — **no `MDS` object**. Every privileged call is
   brokered by the parent frame via `postMessage` through a `miniweb.js` shim. Can read
   node state and install (brokered), nothing else.
2. **As a first-class MDS MiniDapp** — direct `MDS.*` access, direct install. Full power.
3. **On a bare IPFS/HTTP gateway** — plain `fetch()`, **no node present**. Download-only;
   there is no install path at all.

---

## The web-embed sandbox API (`miniweb_*`)

For front-ends running inside the sandboxed iframe. Load the `miniweb.js` shim and **call
`miniweb_Init()` first** — it installs the `window.onmessage` reply listener. Each call
mints a `randid`, posts `{action, data, randid, ...}` to the parent, and the parent
replies `{action:"MINIWEB_RESPONSE", randid, data}`. **Requests have a ~2-minute TTL.**

| function | callback receives (`msg.data`) |
|---|---|
| `miniweb_Init()` | — (sets up listener; call once, first) |
| `miniweb_NetGET(url, cb)` | `{status, response}` — **`response` is a STRING → `JSON.parse` it** |
| `miniweb_MdsCmd(cmd, cb)` | raw command response (read-only allowlist) |
| `miniweb_InstallDapp(fileUrl, name, cb)` | `{status, installed, pending}` or `{status:false, error}` |
| `miniweb_JumpToURL(minifile, cb?)` | cross-site navigation |

```javascript
miniweb_Init();
miniweb_NetGET("https://example.org/store.json", function (msg) {
  var data = JSON.parse(msg.response);   // response is a STRING
  // ... render data.dapps
});
```

### The trust boundary is an ALLOWLIST, not a denylist

The parent enforces, from the outside:

- **`NET_GET`:** HTTPS only; **blocks loopback/LAN** (`127.0.0.1`, `localhost`, `0.0.0.0`,
  `[::1]`, `192.168.*`, `10.*`).
- **`MDS_CMD`:** a **read-only allowlist** — roughly
  `mds, block, status, balance, coins, maxima, checkmode, history, tokenvalidate, keys,
  getaddress, random, convert, hash` — and it **refuses** any string containing
  `action:install / uninstall / remove / send / sendall`. The iframe can *read* node state
  but cannot mutate through the generic cmd proxy.
- **`INSTALL_DAPP`** is the one sanctioned mutation path.

The front-end also **force-upgrades `http://`→`https://`** on the file URL before handing
it up.

---

## Server hosting hygiene

- **Filenames lowercase-hyphenated** (`mydapp-1.2.0.mds.zip`). Local builds are often
  mixed-case with underscores — rename on upload so the filename matches the URL in the
  manifest exactly. Case-sensitive servers turn a wrong-case name into a **404**.
- **Every published file mode 644, dirs 755.** A wrong-mode (e.g. 700) upload returns
  **HTTP 403** and looks like a broken store. Verify — the following should print nothing:

  ```bash
  find <webroot> -type f ! -perm -o=r    # any file NOT world-readable → fix it
  ```

- **`sha256` in catalogs.** Publish a checksum per zip/asset so clients verify integrity
  after download.

---

## IPFS / IPNS mirror pattern (reusable design patterns)

A content-addressed mirror gives a catalog a DNS-stable address that survives origin
outages. The following are reusable patterns, not any specific pipeline:

- **Single-instance flock guard** — `exec 9>/some/lock; flock -n 9 || exit 0`. A slow run
  (large downloads) must not overlap the next scheduled fire.
- **Self-contained snapshot** — copy the catalog + local dapps from the origin, then pull
  every externally-hosted zip/asset into a **persistent download cache** so the snapshot
  has no live external dependencies. Treat `latest`-style URLs as always-refetch (their
  content changes without the name changing).
- **Dual catalog rewrite** — emit each catalog twice: one with **root-relative** paths
  (works at any gateway or raw `/ipfs/<CID>/` path) and one with **absolute** URLs for
  clients that require absolute `file`/`icon`. Rewrite only the `file`/`icon` fields.
- **Asset integrity check** — verify each asset against the catalog `sha256`, **refetch
  once** on mismatch, and **prune** assets no longer referenced (snapshot stays
  current-versions-only).
- **Change detection** — hash the whole stage
  (`find | sort | sha256sum | sha256sum`) against a stored hash; skip the `ipfs add`/publish
  if unchanged **but still reconcile the remote pin**.
- **Idempotent remote-pin reconciliation** — on both the publish and the no-change paths:
  skip if the CID is already queued/pinned, otherwise pin, then **rotate** — unpin any
  remote pin that isn't the current CID. (Skipping pinning on no-change runs means a failed
  pin never retries.)
- **Publish + DNSLink** — `ipfs add -r --cid-version 1 -Q` → `ipfs name publish` →
  remote-pin rotation → write the CID somewhere clients can read it. DNSLink is a one-time
  TXT record (`_dnslink.<host> → dnslink=/ipns/<key>`), so publishes never touch DNS again.

Access surface for such a mirror: your own gateway, any public gateway via
`/ipns/<host>/`, or the DNS-free raw IPNS key `/ipns/<k51-key>`.


---

## reference: ecosystem-tooling
<!-- (was references/ecosystem-tooling.md) -->

# Ecosystem & Tooling — patterns built on Minima, and how to look things up

A map of the Minima ecosystem: the application patterns that have been proven on-chain, the
official resources, the explorer/lookup tooling a developer or agent actually uses, and the
offline KISS-VM bench for testing contract logic. Contract mechanics referenced below are
described in full in `references/contracts-covenants.md`; node commands live in
`references/node-operations.md` and `references/commands-node-network.md`.

## Tooling gotchas

Read these before you trust a lookup or a test result:

1. **The hosted explorer's human page gives FALSE NEGATIVES — never WebFetch
   `explorer.minima.global/transactions/<hash>`.** That page is a client-hydrated SPA (Next.js);
   the HTML is empty until JavaScript runs, so a plain GET returns near-empty markup and every
   transaction looks "not found". For programmatic lookups use the explorer's tRPC endpoint (see
   below) or, better, your own node's `txpow` command.
2. **An explorer's block height is advisory, not canonical.** A hosted explorer's indexer can run
   *ahead* of the real chain (observed ~20k blocks higher) and can also be *missing* large spans
   of blocks. Treat any height or "confirmed" status from an explorer as a lead, and confirm
   settlement against your own node's `block` / `txpow` / archive before declaring anything final.
3. **The offline KISS-VM bench does not do real cryptography or chain state.** It validates script
   *logic*, not signatures, UTXO existence, MMR proofs, fees, or multi-transaction flows. A script
   that returns TRUE on the bench can still be rejected on-chain. Use it for logic; verify
   signature and chain behaviour on a live node.

---

## Proven application patterns gallery

These are ecosystem examples — categories of things that have been built and run on Minima. Each is
a UTXO + KISS-VM covenant pattern; the contract mechanics are detailed in
`references/contracts-covenants.md`.

**Constant-product AMM on UTXO.** A liquidity pool is represented as two reserve coins sitting at a
single *stateless* covenant address (the same script guards both reserves; pool identity is in the
coins, not in mutable script state). Swaps are validated by a constant-product invariant
(`x * y` must not decrease across the transaction), pinned with `VERIFYOUT` so the output reserves
land back at the pool address. A **minimum-product floor** is enforced alongside the invariant to
defeat dust-drain attacks, where an adversary repeatedly nibbles tiny amounts and rounds the
reserves down. Stateless design means anyone can construct a valid swap without permission.

**Cross-chain atomic swaps via HTLC.** A hash-time-locked contract links two legs (on Minima and on
another chain, or two coins on Minima) by a shared SHA-256 hashlock: revealing the preimage to claim
one leg exposes it to claim the other, so either both legs settle or neither does. A timelock
refund path returns funds to the maker if the taker never reveals. Orders are advertised on shared
**sentinel addresses** that act as a public order board — makers post offers to a well-known
address, takers scan it and fill.

**StateNFT — a collection under one tokenid.** Instead of minting a distinct token per item, an
entire collection shares ONE `tokenid`, and each coin carries per-item identity in its own coin
state (state ports). The covenant enforces that identity travels with the coin as it is spent and
recreated, so "which NFT" is a function of coin state rather than of a unique token. This keeps a
whole series under a single mintable asset while preserving per-item uniqueness and provenance.

**On-chain orderbook / fixed-exchange DEX covenants.** A resting offer is a coin at an exchange
covenant that encodes an exact price. Typical branches: an **owner-cancel** path (`SIGNEDBY` the
maker reclaims the coin), an **expiry-return** path (after a block height the offer can be returned
to the maker), and an **exact-fill** path where a taker consumes the offer only by paying the
precise counter-amount. Every payout branch is pinned with `VERIFYOUT` so the taker cannot redirect
proceeds or fill at the wrong price. This is the "fixed-exchange" style: no continuous curve, just
discrete take-it-or-leave-it orders.

**Prediction-market / escrow covenants.** Funds are locked pending an outcome, with an **arbiter**
key empowered to release to one party, and **MAST** branches (a Merklised set of spend conditions)
selecting the payout path — win, lose, refund on timeout, or arbiter override. MAST keeps the
on-chain script small: only the branch actually taken is revealed and executed.

**Time-lock "future cash" guardian vaults.** A coin is locked until a target block, after which
anyone can push it to a predetermined payout address. Because the same WOTS key must not sign twice,
these guardian/vault designs favour a **collect-and-sweep** pattern that consolidates matured coins
to a fresh address in a WOTS-safe way, avoiding key reuse when many locked coins mature and are
swept together. (See `references/key-security-wots.md` for the one-time-signature constraint.)

**Payment channels (ELTOO-style).** Two parties transact off-chain by exchanging successively newer
signed states; only the latest agreed state needs to settle on-chain. The ELTOO structure lets a
newer state supersede an older one without the punitive complexity of earlier channel designs,
which suits Minima's every-user-runs-a-node model where both parties hold the chain.

---

## Official resources

- **docs.minima.global** — the official documentation: node install, the terminal command
  reference, KISS scripting, MDS (MiniDapp System) APIs, and Maxima.
- **github.com/minima-global/Minima** — the reference node implementation (Java). This is the
  canonical protocol source; other language ports aim for parity against it.
- **build.minima.global** — the developer/build portal for MiniDapp and node builders.
- **Community** — the Minima Discord and community channels are where node operators and MiniDapp
  authors coordinate; official links are on docs.minima.global.
- **MEG (Minima Enterprise Gateway)** — an HTTP interface that fronts a Minima node for enterprise
  integration, so back-office systems can talk to a node over HTTP rather than the raw terminal.
  Useful when a hosted service needs node access (e.g. block/coin indexing behind a web backend).

---

## Explorer & lookup tooling — two different explorers

There are two distinct things called "the explorer", and they behave oppositely. Getting them
straight avoids the "the explorer has no backend" vs "the explorer has a tRPC backend" confusion:
they are different applications.

### 1. The hosted block explorer (has a tRPC backend)

`explorer.minima.global` (in its hosted form) exposes a tRPC API you can query directly:

```
GET https://explorer.minima.global/api/trpc/txpow.findById?batch=1&input={"0":{"json":{"id":"0x<txpowid>"}}}
```

- **`batch=1` is MANDATORY.** Without it the server responds `Streaming requests must be batched`.
- The `input` value is a **URL-encoded JSON object** of the form `{"0":{"json":{"id":"0x..."}}}`.
  Passing a bare string yields `Expected object, received string`.
- Send **browser-like headers** — a `User-Agent` (e.g. `Mozilla/5.0`) and a
  `Referer: https://explorer.minima.global/` — or the request may be refused.
- Response shape is `[{"result":{"data":{"json": {...}}}}]`.

**Found vs not-found is discriminated by whether `block_number` is present** — the API always
returns HTTP 200 with a JSON body even for an unknown hash:

| Field          | On-chain | Not on-chain |
|----------------|----------|--------------|
| `block_number` | integer  | absent       |
| `datetime`     | ms epoch | `null`       |
| `header.date`  | UTC date | absent       |
| `body`         | tx body  | `null`       |

Related procedures on the same tRPC surface: `txpow.findByBlockNumber`, `txpow.onchain`,
`txpow.stats` (returns chain height), and a top-level `search`.

Minimal lookup (Python), with retry and browser headers:

```python
import urllib.request, urllib.parse, json

def lookup(h):
    inp = json.dumps({"0": {"json": {"id": h}}}, separators=(',', ':'))
    qs  = urllib.parse.urlencode({"batch": "1", "input": inp})
    url = f'https://explorer.minima.global/api/trpc/txpow.findById?{qs}'
    req = urllib.request.Request(url, headers={
        'User-Agent': 'Mozilla/5.0',
        'Referer':   'https://explorer.minima.global/',
    })
    j = json.loads(urllib.request.urlopen(req, timeout=20).read().decode())
    d = j[0].get('result', {}).get('data', {}).get('json') or {}
    return d.get('block_number')  # truthy = on-chain, None/absent = not found
```

Throttle to a few requests per second and retry transient errors. **But remember gotcha #2:** the
hosted indexer is not canonical — it can be ahead of, or missing, real chain state. Use it to *find*
a lead; confirm settlement on your own node.

### 2. The "simple explorer" (client-side, NO backend)

A separate breed of Minima explorer is a **purely client-side MiniDapp** — a React app that renders
by calling `MDS.cmd('txpow ...')` against the *visitor's own local node / MEG* (localhost). It has
no hosted backend at all. Visited as a bare web URL without a running local node, it shows "no data"
everywhere, because there is nothing behind it but your own node. **You cannot query this kind of
explorer as a remote data source** — there is no server to hit; the "data source" is whoever's
browser is open. This is the flip side of gotcha #1: one explorer *has* a queryable tRPC API, the
other is just a viewer for your own node.

### 3. Your own node (the trustless option)

The node's own terminal commands are the trustless local alternative and the source of truth:

- `txpow txpowid:0x<id>` — fetch a specific TxPoW.
- `txpow block:<n>` — fetch by block height.
- `txpow address:0x<addr>` — find TxPoWs touching an address. **Note the ~13k-block search
  window**: address search only reaches back over the recent unpruned range, not the full history
  (cross-ref `references/node-operations.md`).

When correctness matters, prefer the node over either explorer.

---

## Offline contract testing — the minima-vm bench

**minima-vm** is a native C++ command-line bench that runs the KISS VM offline. It executes any KISS
script against a *simulated* transaction context — state variables, inputs, outputs, signatures,
block number, coin age — and returns TRUE/FALSE plus an instruction count and an error trace, with
no live node and no chain. It is fast and ideal for iterating on script *logic*.

Typical usage:

```bash
minima-vm --script "RETURN TRUE"
minima-vm --file contract.kiss --prevstate "0:0xPUBKEY,1:0.5,2:0xTOKENID,5:0xADDR"
minima-vm --file contract.kiss --prevstate "0:0xKEY" --signatures "0xKEY"    # cancel/owner path
minima-vm --file contract.kiss \
  --prevstate "0:0xKEY,1:0.5,2:0xTOKEN,5:0xADDR" \
  --outputs '[{"address":"0xADDR","amount":"4.5","tokenid":"0xTOKEN"}]' \
  --amount 9                                                                  # fill path
```

Flags include `--script`/`--file`, `--prevstate`/`--state` (PREVSTATE / STATE ports),
`--inputs`/`--outputs` (coins as JSON), `--signatures` (signing pubkeys), `--amount`, `--block`,
`--coinage`, and `--verbose`. Exit code 0 = TRUE, 1 = FALSE, so it scripts cleanly:
`minima-vm --script "..." && echo PASS || echo FAIL`.

### What it CANNOT simulate — critical

The bench validates logic, not chain reality. Do not trust it for anything below:

- **No real signature cryptography.** `SIGNEDBY(pub)` merely checks whether `pub` appears in the
  simulated witness list — it does NOT verify a WOTS signature. On-chain, `SIGNEDBY` requires a
  valid Winternitz one-time signature. So the bench will happily "pass" a signature path that would
  fail (or be a key-reuse hazard) on-chain. Verify all signing behaviour on a live node.
- **No chain / UTXO / MMR / mempool / fee context.** There is no real coin set, no MMR proofs, no
  mempool, and no fee accounting. Whether the input coins actually exist, are unspent, and carry
  valid MMR proofs is exactly what the bench cannot check — and exactly what a node enforces.
- **No multi-transaction flows.** Each run evaluates one script against one simulated transaction.
  Sequences that span several transactions (channel updates, order-fill-then-sweep, etc.) must be
  reasoned about run-by-run or tested on a node.
- **Fixed instruction ceiling.** The KISS VM caps execution (1024 instructions, 64 stack depth); the
  bench enforces a fixed ceiling too. A script that blows the limit fails — good to catch early, but
  confirm the exact limits against the reference node.
- **`GETOUTTOK` is an alias for `GETOUTTOKEN`.** The bench accepts `GETOUTTOK` as a convenience
  alias; treat them as the same function.
- **Watch bench-vs-node divergences.** Argument order and edge cases can differ from the reference
  Java VM — for example, a `SUBSET` argument-order difference has been observed between a bench port
  and the real VM (`SUBSET(start, length, hex)` on-chain). When a script uses functions like
  `SUBSET`, confirm against the reference implementation or a live RPC node before deploying.

**Parity:** the KISS-VM interpreter is self-rated at roughly **95% parity** with the reference —
all functions used in production contracts are implemented (SIGNEDBY, PREVSTATE, STATE, GETOUTADDR,
GETOUTAMT, GETOUTTOK, VERIFYOUT, VERIFYIN, SUMINPUTS, SUMOUTPUTS, ASSERT, SHA2, SHA3, CONCAT,
SUBSET, LEN, and 40+ more). That 95% is for *logic*. The remaining gap — and the whole of signature
and chain behaviour — is why the bench is a logic sandbox, not a deployment gate.

---

## awesomeMinima

**awesomeMinima** is a community-curated "awesome list" of Minima resources — links to docs, tools,
MiniDapps, tutorials, and libraries collected by the community. It is a good discovery entry point
when you are looking for existing tooling or example projects before building your own. As with any
community list, verify that a linked project is current and matches the official protocol before
relying on it.


---

## reference: key-security-wots
<!-- (was references/key-security-wots.md) -->

# Key Security: WOTS+ and Stateful Signatures

This is the single most fund-critical area of Minima engineering. Read it before writing
anything that signs, re-syncs a wallet, audits spend history, or holds funds on a payout
address. Everything below is protocol- and engineering-level knowledge whose purpose is to
**protect funds** — to help you never reuse a one-time key, and to detect and evacuate any
address whose key has already been reused.

## Key-safety rules

> - **A WOTS leaf is one-time.** One leaf key signs exactly one message, ever. A second,
>   different signature under the same leaf weakens that key until its address can no longer
>   be considered secure. Treat any reused leaf as compromised.
> - **On every restore/resync, set `keyuses` HIGHER than the true prior count.** The node
>   regenerates keys at `uses = 0` and cannot know how many leaves were really spent. Too low
>   → it re-issues already-spent leaves → instant, self-inflicted reuse.
> - **When detecting reuse, filter to the leaf tier.** Root and intermediate tiers repeat by
>   design. A query that does not filter to the top (leaf) tier drowns in false positives.
> - **Land funds on covenant-pinned or fresh-key addresses.** Plain default-wallet addresses
>   are the exposed surface; covenant coins are resistant. The danger is value *landing* on a
>   reused-key address.
> - **Any code that increments a key-use counter must be atomic/serialized.** A non-atomic
>   read-modify-write of the counter hands out the same leaf twice under concurrency.

## Why Minima keys are stateful

Minima signs with **WOTS+ — Winternitz One-Time Signatures** (Winternitz parameter 8), a
hash-based scheme built entirely on SHA3-256 and considered quantum-resistant. Unlike the
ECDSA used by Bitcoin and Ethereum (which Shor's algorithm threatens on a sufficiently large
quantum computer), WOTS+ derives all of its security from hash functions. The cost of that
security is that each Winternitz key is genuinely **one-time-use**: reusing a single leaf key
to sign two different messages progressively leaks the private key.

To turn a one-time primitive into a reusable identity, Minima stacks WOTS leaves into a
**Merkle Signature Scheme** — a "tree of trees." By default each of a node's root keys is a
3-level structure, 64 keys per level, giving 64³ = **262,144** one-time leaves under a single
root public key. Each level's parent key signs the root of the tree below it, so a full
signature proves a path from a spent leaf up to the shared root public key that receivers use
as an address.

The node tracks how many leaves a key has consumed in a per-key **`uses`** counter, visible via:

```
keys action:list
```

**One leaf = one signature.** Every signature advances `uses` to the next leaf. The entire
security model rests on that counter being correct and monotonic. If a leaf is ever signed
twice over different data, that leaf's private key becomes recoverable, and every coin sitting
on that address is at risk. Treat a reused leaf as compromised, full stop.

## The tier rule — the most important detection filter

A full Minima signature carries **multiple tiers**, one per level of the tree of trees. Only
one of them is one-time:

- **Root tier** — the top-level key. Appears in *every* signature from an address, by design.
  Repetition here is expected and is **not** a vulnerability.
- **Intermediate tier(s)** — the connecting level(s) between root and leaf. Shared across
  groups of signatures, expected to repeat. **Not** a vulnerability.
- **Leaf tier (top tier)** — the actual one-time WOTS key that signs the transaction. This is
  the only tier where reuse is exploitable.

> Every reuse-detection query MUST restrict to the leaf tier. Without that filter you get
> massive false positives from the by-design root/intermediate repeats, and the real signal is
> buried. Filtering to the leaf tier is not an optimization — it is the difference between a
> working detector and noise.

## How reuse actually happens (almost always operational, not malicious)

In practice, reuse is overwhelmingly **self-inflicted through operations**, not an attack.
Three mechanisms account for nearly all of it:

**1. Re-syncing from seed with `keyuses` set too low.** When you restore a wallet from its seed
phrase, the node regenerates every key starting at `uses = 0`. It has **no way to know** how
many leaves were actually consumed on-chain before the restore. You must tell it, via the
restore/resync `keyuses:N` parameter. If `N` is lower than the true prior consumption, the node
happily re-issues leaves it has *already spent on-chain* → instant reuse → those keys are
compromised the moment they next sign. Always set `keyuses` **higher** than the true previous
count. Signatures are stateful and cumulative; over-counting only wastes headroom (the ceiling
is 262,144 per key), while under-counting destroys security. See
`references/node-operations.md` for the resync runbook.

**2. Running the same seed on two nodes.** Each node maintains its **own independent** `uses`
counter. Two nodes on one seed will each think they own the full leaf budget and will hand out
overlapping leaves.

**3. State rollback.** Any rollback that rewinds the counter below the true on-chain consumption
re-exposes already-spent leaves, exactly like an under-set `keyuses`.

The common thread: the `uses` counter is the node's only memory of what has been spent, and
anything that resets or diverges that counter without accounting for real on-chain history
re-issues spent leaves.

## How to detect reuse — two independent methods

### Method 1 — spend-block counting (the archive method, no witnesses needed)

Critical data-model fact: **an archive retains coins, not witnesses.** A stored block keeps the
coins it created and spent but *discards the signatures*, so you cannot recover exact leaf
signatures over deep history. You can, however, reconstruct each key's default address exactly
as the protocol does and count how many times it spent:

1. For each public key, derive its default address the way Minima does:
   `RETURN SIGNEDBY(<publickey>)`, then take that script's address.
2. Count the **distinct spend-blocks** for that address on-chain.
3. Verdict:

> **reuse suspected  ⇔  on-chain distinct `spend_blocks` > the node's local `uses`** for that key.

The rule is intuitive: `uses` is how many times the node *believes* it signed; distinct
spend-blocks is how many times the chain shows that address *actually* spent. If the chain shows
more distinct signing events than the counter admits, leaves were reused.

> Count **distinct spend-blocks**, NEVER a raw spent-coins count. A single signature can spend
> many coins in one transaction/block, so a coins count over-counts wildly. Collapsing to
> distinct blocks ≈ distinct signatures ≈ `uses`, which is what makes the comparison valid.

### Method 2 — on-chain signature index (heavyweight, exact)

For a signature-exact audit, index the WOTS signatures observed on-chain keyed by
`(address, tier, publickey, block)`, then detect reuse by grouping on **address + leaf tier**
and flagging any group with `count > 1`. This is a large offline dataset and is overkill for
most needs — Method 1 is sufficient for a routine key-use audit. Reserve Method 2 for when you
need signature-exact confirmation.

> Gotcha: group **per coin address**, not per canonical/root address. Grouping by the canonical
> address collapses sibling addresses and inflates counts. One row per coin address is correct.

## Reuse ≠ compromised-in-practice

A key flagged by an audit does **not** automatically mean funds were lost or moved improperly.
Owners frequently reuse a key by accident and then move their own funds out using a *fresh* key.
That pattern reads as "at-risk, then emptied" but is a benign self-resolve. **Before declaring a
loss, check which key the emptying transaction actually used.** If the coins left the address
under a fresh leaf, the owner tidied up safely and nothing was exploited. Only reuse of the leaf
that *still guards live funds* is an active danger.

## Mitigation — the time-lock guardian (collect-and-sweep)

The reusable defensive pattern is a time-lock "guardian" covenant that collects funds to a
pre-committed address and, when needed, sweeps them onward to a fresh key. A representative
time-lock contract:

```
RETURN (@BLOCK GTE PREVSTATE(1) OR @COINAGE GTE PREVSTATE(4))
       AND VERIFYOUT(@INPUT PREVSTATE(2) @AMOUNT @TOKENID FALSE)
```

The load-bearing insight: **a COLLECT to a `VERIFYOUT`-pinned address is inherently WOTS-safe.**
The covenant's `VERIFYOUT` constrains the spend so the output can only ever go to the address
pre-committed in state — a collect literally cannot pay anywhere else. So the collect itself is
never the risk. The *only* risk is that the funds then **land** on a payout address whose leaf
key has already been reused. That gives a clean two-case policy:

- **SAFE payout** (the destination key has not been reused): just COLLECT. Done.
- **AT-RISK payout** (the destination key was reused): COLLECT, then **immediately SWEEP** the
  collected coin to a fresh-key address that has never signed — in one signed transaction, at
  the earliest confirm-depth the coin becomes spendable, to minimize the time value sits on the
  weakened key.

Generalize the principle: **design fund flows so value lands on covenant-pinned or fresh-key
addresses.** A covenant coin whose spend is constrained by `SIGNEDBY` against a key held in
state plus a `VERIFYOUT` output pin does not expose funds to a reused-key weakness the way a
plain default-wallet address does. Plain default-wallet addresses are the exposed surface;
covenant coins are resistant.

### The load-bearing trick — signing a *retired* address

A common hardening step is to *retire* a reused address so change never routes back to it —
done by nulling its script-row public key to `0x00` (turning it watch-only, non-default). But
that retirement breaks automatic signing: with the pubkey nulled, `publickey:auto` reads `0x00`
and can no longer sign that address, even though the real key still exists in the keys table.

To sweep funds off a retired address you must **extract the real key from the script** and pass
it explicitly. The address's script still contains its `SIGNEDBY(<pubkey>)`, so pull the pubkey
out and hand it to the signer:

```javascript
// srow.script looks like: RETURN SIGNEDBY(0xFF..) ...
var pkm     = (srow && srow.script) ? /SIGNEDBY\((0x[0-9a-fA-F]+)\)/i.exec(srow.script) : null;
var signKey = (pkm && pkm[1]) ? pkm[1] : "auto";
// then sign the txn with publickey:signKey instead of publickey:auto
```

The explicit key works whether or not the address was retired; `publickey:auto` does not once
the row's pubkey has been nulled.

## Programmatic-signing bugs that bite transaction builders

These are the practical failure modes when you build and sign transactions in code. Each one
can silently corrupt a signed set or a use-counter:

- **`newaddress` fails silently on a locked vault.** Minting a fresh address *creates a key
  pair*, which a password-locked or keys-wiped vault cannot do — so the call just fails. A
  locked vault then looks identical to a node error. Surface the node's own `r.error` and check
  vault-locked state before minting.
- **Use `token.totalamount`, not a truncated total field.** Serializing a token amount from a
  truncated/scaled field can overflow the 64-bit MiniNumber range ("MiniNumber too large"). Use
  the full-precision `token.totalamount`.
- **Always serialize and sign on the node.** If you serialize a transaction yourself (e.g. in
  Python), its TransactionID differs from the node's, so the signature will not match
  ("SIGNATURE FAIL"). Build/serialize on the node and sign the node's own bytes.
- **Normalize address case.** A store holding `0x…` (lowercase x) queried with `0X…` returns
  nothing on an exact match, which silently reads as **0 uses** — dangerous, because it looks
  like an unused key. Normalize the prefix before comparing.
- **Filter coins by the target tokenid.** A coins query returns token coins too; feeding an
  unrelated token coin into a set intended for a specific tokenid contaminates the signed set
  ("Inputs LESS than Outputs"). Filter to the target tokenid before assembling inputs.

## The core-code lesson — atomic counter increments

The definitive engineering lesson, publicly documented in the fork's maintainer writeup, is
about the counter itself. A `signData`-style path that does an **unsynchronized
read-modify-write** of the WOTS key-use counter can interleave under concurrency:

```
read  uses = N
sign  leaf N over data           // thousands of SHA3 hashes — a millisecond-wide window
write uses = N + 1
```

Two callers can both read `uses = N`, both sign leaf `N` over **different** data, and both write
`N + 1`. One leaf, two messages → the leaf's private key becomes recoverable, and one increment
is silently lost on top. The window is not tight: the signing between read and write is thousands
of hashes wide, so ordinary concurrent request handling is enough to trigger it.

Two aggravating factors make this reachable in normal operation:

- The path is hit by independent threads — per-request RPC handlers, per-socket app handlers, and
  periodic send timers can all call it concurrently on one node.
- Default-address selection that draws **uniformly at random from a small key set** with no
  least-used preference concentrates collisions rather than spreading them; by the birthday
  bound you expect a repeat within a handful of calls.

And verification does **not** save you: the tree verifier only walks the Merkle path to the root
public key — it has **no notion of leaf-index monotonicity**. A reused leaf still verifies
correctly and the network accepts it. Nothing in validation detects the reuse.

**The lesson:** any code that increments a key-use counter must be atomic/serialized (a single
`synchronized` mutator, or an equivalent lock/transaction), so two signers can never observe the
same `uses` value. The same applies to any shared prepared statement or cached state the counter
path touches. This is the one invariant that, if violated, quietly manufactures reuse no audit
downstream can prevent — only detect after the fact.


---

## reference: kissvm-language
<!-- (was references/kissvm-language.md) -->

# KISS VM Language Reference

The complete reference for KISS VM — Minima's on-chain smart-contract scripting language. Every function, global, and limit below is verified against the Minima node source (`org.minima.kissvm`) and the official in-node tutorial (`tutorial` command). Where published docs and the actual VM disagree, this file follows the source and says so.

---

## KISS gotchas

Read these before writing a single line. Each one has burned real developers on real chains.

- **NO underscores in variable names.** Variables are `[a-z]+` — lowercase letters ONLY. `player_pick` does not throw a helpful error; the script silently fails to parse/execute. Use `playerpick`, `pp`, `hpk`.
- **~1200-character script size limit (undocumented).** Scripts larger than roughly 1200 characters are silently rejected on-chain — no error, the transaction just never validates. Minify variable names to 2–3 chars and move rarely-used branches into MAST.
- **Use `@COINAGE`, not `@BLOCK`, for timing.** Relative timelocks via `@COINAGE GT n` are reliable everywhere. Absolute `@BLOCK` deadlines have proven unreliable on some synced nodes, and `txncheck` cannot evaluate `@BLOCK`/`@COINAGE` at all (they depend on the block the tx lands in). Store a timeout duration in state and compare against `@COINAGE`.
- **Word operators are boolean; symbol operators are bitwise-on-HEX.** `AND OR XOR NAND NOR NXOR` operate on TRUE/FALSE only. The symbols `& | ^ ~ << >>` are bitwise, but **both operands must be HEX values** — they do not work on numbers. `5 XOR 3` is not bitwise math; `0x05 ^ 0x03` is.
- **`NUMBER()` on a 32-byte hash overflows.** Never `NUMBER(SHA3(x))`. Truncate first: `NUMBER(SUBSET(0 4 h))` takes the first 4 bytes.
- **`SUBSET` is `(start end hexdata)` — numbers first, and the second number is an END index, not a length.** Some older docs describe `SUBSET(HEX start length)`; the actual VM reads param 0 = start, param 1 = end (exclusive), param 2 = the hex data. `SUBSET(0 4 h)` = first 4 bytes of `h`. Same shape for `SUBSTR(start end string)`.
- **Reading an unset state port crashes the script.** `STATE(n)` / `PREVSTATE(n)` on a port that was never set throws an execution error → script returns FALSE. Set every port your script might read (use `0` for unused), or guard the read behind a phase check that guarantees the port exists.
- **Scripts default to FALSE.** If execution falls off the end without `RETURN TRUE`, the coin cannot be spent. Every valid path must explicitly `RETURN TRUE`.
- **Scripts are case sensitive.** Keywords and function names are UPPERCASE, variables lowercase.
- **`VERIFYOUT` keepstate must match the output's `storestate`.** Phase transition (coin recreated at the script address, carrying state): `VERIFYOUT(... TRUE)` + `txnoutput ... storestate:true`. Final payout to a wallet: `VERIFYOUT(... FALSE)` + `storestate:false`. A mismatch silently invalidates the transaction.

---

## 1. Overview & philosophy

KISS = **K**eep **I**t **S**imple **S**tupid. The VM is deliberately minimal:

- **UTxO model.** Like Bitcoin, not Ethereum. Every coin (Minima, custom token, or NFT) is locked by a script. A script is a pure predicate: it runs when the coin is used as a transaction **input** and must return **TRUE** for the spend to be valid. The default is FALSE.
- **Deterministic and bounded.** Hard instruction cap (1024), hard stack depth (64), no unbounded loops in practice (`WHILE` exists but is subject to the instruction cap — every iteration costs instructions). Every full node re-executes every script; scripts must be cheap.
- **No global chain state.** A script sees only its own transaction: its input coin, the other inputs/outputs, the transaction's state variables, and the coin's stored (previous) state. Minima uses per-coin state in the MMR proof database rather than a global state trie — this is what makes contracts inherently Layer-2 compatible.
- **Human-readable.** Scripts are plain UTF-8 text, not bytecode. The address of a script is the hash of its cleaned text — `ADDRESS(script)` / the `newscript` command give you the `0x...` address coins are sent to.
- **Two scripts must pass.** When spending a token/NFT coin, both the coin's address script AND the token's own token script must return TRUE. A plain token has `RETURN TRUE` as its token script.

The default lock on an ordinary wallet coin:

```
RETURN SIGNEDBY(0xFFEEDDCCBBAA99887766554433221100FFEEDDCCBBAA99887766554433221100)
```

— TRUE only if the transaction carries a valid signature from that public key.

---

## 2. Value types

KISS has four value types. There are no type declarations; values are typed by literal form and functions are strict about what they accept.

| Type | Literal form | Notes |
|---|---|---|
| **NUMBER** | `0`, `42`, `3.14`, `-5` | Arbitrary-precision decimal (MiniNumber). Full decimal math, no wrap-around. |
| **HEX** | `0xFFEE01` | Raw bytes. Addresses, public keys, coinids, tokenids, hashes. Max 64 KB. |
| **SCRIPT** (string) | `[hello world]` | UTF-8 string, written in square brackets inside scripts. Also how you embed a script-as-data (for `EXEC`, `ADDRESS`, `FUNCTION`). Max 64 KB. |
| **BOOLEAN** | `TRUE`, `FALSE` | The only values a script may ultimately RETURN. |

Truthiness: `FALSE` is 0; `TRUE` is anything not FALSE (`BOOL()` converts explicitly). Don't rely on implicit coercion — convert with `BOOL`, `NUMBER`, `HEX`, `STRING`.

---

## 3. Official grammar

From the node's built-in tutorial (authoritative):

```
ADDRESS     ::= ADDRESS ( BLOCK )
BLOCK       ::= STATEMENT_1 STATEMENT_2 ... STATEMENT_n
STATEMENT   ::= LET VARIABLE = EXPRESSION |
                LET ( EXPRESSION_1 EXPRESSION_2 ... EXPRESSION_n ) = EXPRESSION |
                IF EXPRESSION THEN BLOCK
                  [ELSEIF EXPRESSION THEN BLOCK]* [ELSE BLOCK] ENDIF |
                WHILE EXPRESSION DO BLOCK ENDWHILE |
                EXEC EXPRESSION |
                MAST EXPRESSION |
                ASSERT EXPRESSION |
                RETURN EXPRESSION
EXPRESSION  ::= RELATION
RELATION    ::= LOGIC AND LOGIC | LOGIC OR LOGIC | LOGIC XOR LOGIC |
                LOGIC NAND LOGIC | LOGIC NOR LOGIC | LOGIC NXOR LOGIC | LOGIC
LOGIC       ::= OPERATION EQ OPERATION | OPERATION NEQ OPERATION |
                OPERATION GT OPERATION | OPERATION GTE OPERATION |
                OPERATION LT OPERATION | OPERATION LTE OPERATION | OPERATION
OPERATION   ::= ADDSUB & ADDSUB | ADDSUB | ADDSUB | ADDSUB ^ ADDSUB | ADDSUB
ADDSUB      ::= MULDIV + MULDIV | MULDIV - MULDIV | MULDIV % MULDIV |
                MULDIV << MULDIV | MULDIV >> MULDIV | MULDIV
MULDIV      ::= PRIME * PRIME | PRIME / PRIME | PRIME
PRIME       ::= NOT PRIME | NEG PRIME | ~PRIME | BASEUNIT
BASEUNIT    ::= VARIABLE | VALUE | -NUMBER | GLOBAL | FUNCTION | ( EXPRESSION )
VARIABLE    ::= [a-z]+
VALUE       ::= NUMBER | HEX | STRING | BOOLEAN
NUMBER      ::= ^[0-9]+(\.[0-9]+)?
HEX         ::= 0x[0-9a-fA-F]+
STRING      ::= [UTF8_String]
BOOLEAN     ::= TRUE | FALSE
```

### Statements

**`LET variable = expression`** — assignment. Variables are lowercase letters only, created on first assignment, reassignable.

```
LET amt = 10
LET total = amt * 2 + 1
LET h = SHA3(0xFFAA)
```

**`LET ( expr1 expr2 ... ) = expression`** — array assignment. KISS has a simple keyed array: the parenthesised expressions form a multi-dimensional key. Read back with `GET`, test with `EXISTS`:

```
LET ( 1 2 ) = [movevalid]
LET v = GET(1 2)
IF EXISTS(1 2) THEN RETURN TRUE ENDIF
```

**`IF ... THEN ... [ELSEIF ... THEN ...]* [ELSE ...] ENDIF`** — conditional. No braces, terminated by `ENDIF`.

**`WHILE expression DO ... ENDWHILE`** — loop. Exists, works, but every iteration consumes instructions from the 1024 budget; deep loops fail. Prefer straight-line code.

**`ASSERT expression`** — if the expression is FALSE, the script immediately fails (returns FALSE). The workhorse for enforcing spend conditions mid-script.

**`RETURN expression`** — ends execution with a boolean result. `RETURN TRUE` = coin may be spent on this path. Falling off the end of the script = FALSE.

**`EXEC expression`** — execute a SCRIPT value as code, inline, sharing the current variable scope. Pairs with `PROOF` for MAST-style patterns (see MAST section).

**`MAST expression`** — declare a Merkelized script block by its MMR root hash; the actual code is supplied with the spending transaction (see MAST section).

Comments: `/* ... */` blocks are accepted by the cleaner and stripped from the canonical script.

---

## 4. Operators

### Comparison (all types where sensible)
```
EQ  NEQ  GT  GTE  LT  LTE
```

### Boolean (word operators — TRUE/FALSE only, NOT bitwise)
```
AND  OR  XOR  NAND  NOR  NXOR  NOT
```
```
IF a GT 5 AND SIGNEDBY(pk) THEN RETURN TRUE ENDIF
```

### Bitwise (symbol operators — HEX operands ONLY)
```
&  |  ^  ~        /* AND, OR, XOR, NOT — both sides must be HEX */
<<  >>            /* bit shift on HEX, max shift 256 bits */
```
The VM enforces `Value.checkSameType(..., VALUE_HEX)` — `5 & 3` fails, `0x05 & 0x03` works. If you need bitwise mixing of numbers (e.g. for randomness), don't: hash instead — `SHA3(CONCAT(a b))`.

### Arithmetic (NUMBER)
```
+  -  *  /  %     /* arbitrary-precision decimal; % is modulo */
NEG               /* unary negation */
```
There is no `**`; use the `POW(n p)` function. Division is exact decimal, use `FLOOR`/`CEIL`/`SIGDIG` to control precision.

Precedence (low → high): word-boolean ops → comparisons → `& | ^` → `+ - % << >>` → `* /` → unary `NOT NEG ~`. When in doubt, parenthesise — parentheses are free, debugging on-chain is not.

---

## 5. Global variables

Source-verified — these are exactly the globals the VM sets (`Contract.java`):

| Global | Type | Meaning |
|---|---|---|
| `@BLOCK` | NUMBER | Block number this transaction is included in |
| `@BLOCKMILLI` | NUMBER | Block time in milliseconds since 1 Jan 1970 |
| `@CREATED` | NUMBER | Block number when this input coin was created |
| `@COINAGE` | NUMBER | `@BLOCK - @CREATED` — blocks since the coin was created |
| `@INPUT` | NUMBER | Index of this input in the transaction (first input = 0) |
| `@COINID` | HEX | CoinID of this input |
| `@AMOUNT` | NUMBER | Amount of this input coin |
| `@ADDRESS` | HEX | Address (script hash) of this input coin |
| `@TOKENID` | HEX | TokenID of this input (`0x00` = Minima) |
| `@SCRIPT` | SCRIPT | This coin's script as text |
| `@TOTIN` | NUMBER | Total number of inputs in the transaction |
| `@TOTOUT` | NUMBER | Total number of outputs in the transaction |

Notes:
- `@BLOCK`, `@BLOCKMILLI`, and `@COINAGE` are the "unknowable until mined" globals — `txncheck` cannot evaluate them, and older doc/tool material sometimes calls `@BLOCK` "@BLKNUM". Prefer `@COINAGE` relative timelocks (see gotchas).
- `@INPUT`/`@ADDRESS`/`@AMOUNT`/`@TOKENID` are what make the self-referencing recreate pattern work: `VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)` means "output at my own index recreates me, state preserved".

---

## 6. Function reference

Exact signatures from the current VM. All calls are `NAME ( arg1 arg2 ... )` — arguments separated by spaces, not commas.

### State

| Function | Description |
|---|---|
| `STATE ( port )` | Value of state port `port` (0–255) in the **spending transaction** (set via `txnstate` / `send state:{...}`) |
| `PREVSTATE ( port )` | State value **stored with the input coin** when it was created (its MMR data) |
| `SAMESTATE ( start end )` | TRUE iff `STATE(n) == PREVSTATE(n)` for every port `n` in the contiguous range `[start..end]` |

**Caveat — `SAMESTATE` ranges are contiguous only.** If port 6 changes but 0–5 and 7–11 must carry over, you need two calls:

```
ASSERT SAMESTATE(0 5)
ASSERT STATE(6) EQ 1
ASSERT SAMESTATE(7 11)
```

`STATE`/`PREVSTATE` on a never-set port throws → script fails. Initialize every port you will ever read (put `0` in unused ones at coin creation).

### Signatures

| Function | Description |
|---|---|
| `SIGNEDBY ( pubkey )` | TRUE if the transaction is signed by this public key |
| `MULTISIG ( n pk1 pk2 ... pkm )` | TRUE if signed by at least `n` of the listed public keys |
| `CHECKSIG ( pubkey data signature )` | Verify an arbitrary signature over arbitrary data (not tied to the tx) |

```
/* 2-of-3 */
RETURN MULTISIG(2 0xFF..01 0xFF..02 0xFF..03)
```

### Input / output verification

| Function | Description |
|---|---|
| `VERIFYOUT ( index address amount tokenid keepstate )` | ASSERT-style check that output `index` has exactly this address, amount, tokenid, and keepstate flag |
| `VERIFYIN ( index address amount tokenid )` | Same for input `index` |
| `GETOUTADDR ( index )` | Address of output `index` |
| `GETOUTAMT ( index )` | Amount of output `index` |
| `GETOUTTOK ( index )` | TokenID of output `index` |
| `GETOUTKEEPSTATE ( index )` | Is output `index` keeping the state (boolean) |
| `GETINADDR ( index )` | Address of input `index` |
| `GETINAMT ( index )` | Amount of input `index` |
| `GETINTOK ( index )` | TokenID of input `index` |
| `GETINID ( index )` | CoinID of input `index` |
| `SUMINPUTS ( tokenid )` | Sum of all input amounts of that token |
| `SUMOUTPUTS ( tokenid )` | Sum of all output amounts of that token |

The `keepstate` boolean in `VERIFYOUT` is what binds the script to the transaction builder: TRUE means the output must carry the transaction state forward (`txnoutput ... storestate:true`), FALSE means it must not. Get this wrong and the tx is silently invalid.

### Hashing & HEX manipulation

| Function | Description |
|---|---|
| `SHA3 ( hex\|string )` | Keccak/SHA3-256 hash → 32-byte HEX |
| `SHA2 ( hex\|string )` | SHA2-256 — kept for cross-chain hashlocks with legacy chains (Bitcoin HTLCs) |
| `CONCAT ( h1 h2 ... hn )` | Concatenate HEX values: `CONCAT(0xAA 0xBB)` → `0xAABB` |
| `LEN ( hex\|string )` | Length in bytes (HEX) / characters (SCRIPT) |
| `REV ( hex )` | Reverse the bytes |
| `SUBSET ( start end hex )` | Bytes `[start, end)` of the data. **Verified against source: numbers first, END index not length.** `SUBSET(0 4 h)` = first 4 bytes |
| `SETLEN ( len hex )` | Force HEX to `len` bytes — trims (keeps the low/rightmost bytes) or left-pads with zeros |
| `OVERWRITE ( srchex srcpos desthex destpos len )` | Copy `len` bytes from source at `srcpos` into dest at `destpos` |

### Casts / conversion

| Function | Description |
|---|---|
| `NUMBER ( value )` | HEX→NUMBER (also passes numbers/booleans through). **Fails/overflows on large hex — truncate 32-byte hashes with `SUBSET(0 4 h)` first** |
| `HEX ( script )` | SCRIPT → HEX (UTF-8 bytes) |
| `STRING ( hex )` | HEX → SCRIPT |
| `UTF8 ( hex )` | HEX → UTF-8 string |
| `ASCII ( hex )` | HEX → ASCII string |
| `BOOL ( value )` | Anything → TRUE/FALSE |
| `ADDRESS ( string )` | The `0x..` address of the given script text — lets a script compute/verify another script's address on the fly |

### Math

| Function | Description |
|---|---|
| `ABS ( n )` | Absolute value |
| `FLOOR ( n )` / `CEIL ( n )` | Round down / up |
| `MIN ( a b )` / `MAX ( a b )` | Smaller / larger of two numbers |
| `INC ( n )` / `DEC ( n )` | n+1 / n-1 |
| `POW ( n p )` | n to the power p (p must be whole) |
| `SQRT ( n )` | Square root |
| `SIGDIG ( digits n )` | Round n to the given significant digits |

### Bit operations

| Function | Description |
|---|---|
| `BITSET ( hex pos bool )` | Return hex with the bit at `pos` set to 1/0 |
| `BITGET ( hex pos )` | Boolean value of the bit at `pos` |
| `BITCOUNT ( hex )` | Number of set bits |

### Strings & arrays

| Function | Description |
|---|---|
| `REPLACE ( str find repl )` | Replace all occurrences |
| `SUBSTR ( start end str )` | Substring `[start, end)` — same numbers-first shape as SUBSET (source-verified) |
| `GET ( k1 k2 ... )` | Read array value set with `LET ( k1 k2 ... ) = v` |
| `EXISTS ( k1 k2 ... )` | Does that array key exist (use before GET to avoid failures) |

### MAST / meta

| Function | Description |
|---|---|
| `PROOF ( data sumval proof sumval root )` | Verify `data` is a leaf of the MMR tree with `root` via `proof` — same math as the `mmrproof` command. Use `0` for the two sum values on plain (non-sum) trees |
| `FUNCTION ( script v1 v2 ... )` | Generic function: runs `script` with `$1..$n` replaced by the values; the variable `returnvalue` inside it becomes the result |

```
LET double = [LET returnvalue = $1 * 2]
LET x = FUNCTION(double 21)    /* x = 42 */
```

---

## 7. Limits

Verified against `Contract.java` constants:

| Limit | Value | Behavior on breach |
|---|---|---|
| Instructions per execution | **1024** (each operation/function = 1) | Execution error → FALSE. (Some older docs say 512 — budget conservatively; only the executed path counts, which is why MAST works) |
| Script size | **~1200 characters** (undocumented) | **Silently rejected on-chain** — the worst failure mode in KISS. Minify + MAST |
| Stack depth | **64** | Execution error |
| Function parameters | **32** max | Parse/execution error |
| HEX / STRING value size | **64 KB** | Execution error |
| HEX bit shift | **256 bits** max | Execution error |
| State ports | **256** (0–255) | `txnstate` rejects out-of-range ports |

---

## 8. MAST — Merkelized Abstract Syntax Trees

MAST is how KISS scripts escape the size limit — and gain privacy. Instead of putting every branch on-chain, you commit to a **hash tree of script blocks** and reveal only the block you actually execute, plus a merkle proof.

### Why

```
IF SIGNEDBY(0xFF..AA) THEN
  /* CODEBLOCK 1 - the everyday path */
ELSEIF @COINAGE GT 100 THEN
  /* CODEBLOCK 2 - the rare recovery path */
ENDIF
```

If block 1 runs, nobody ever needs to see block 2. The blocks behind the roots can be megabytes in total; all that matters is that the *executed path* fits the VM limits. This enables things a flat script never could — e.g. a 1-of-10,000 multisig where each leaf is a different `SIGNEDBY` block, or a game contract with thousands of pre-committed valid end states.

### The `MAST` statement

```
IF SIGNEDBY(0xFF..AA) THEN
  MAST 0x72BE56DFD48B785139A72512FEAAC7E339B8F48132E9B9340A248EFC00F4A5DA
ELSEIF @COINAGE GT 100 THEN
  MAST 0xFA1B16685F09FA56581614AC55E731697C46926392129F3A6BF8FA5EE202A251
ENDIF
```

Each `MAST 0x...` is the root of an MMR tree of script blocks. MAST blocks can nest inside other MAST blocks.

### Worked sketch

**1. Build the tree** of alternative blocks:

```
mmrcreate nodes:["RETURN TRUE","RETURN FALSE"]
```

Returns a `root` plus a per-leaf `proof`. Verify any leaf:

```
mmrproof data:"RETURN TRUE" proof:0x0000010100...C7 root:0x0E3216...15
```

Both leaves verify against the **same root** with **different proofs**.

**2. Lock the coin** with a script containing `MAST <root>` (or the explicit PROOF+EXEC form below). Get its address with `scripts action:clean script:"..."` or `newscript script:"..." trackall:true`, and `send` funds to it.

**3. Spend it** — build the txn and attach the revealed block + proof as witness data via `txnscript`:

```
txncreate id:m
txninput  id:m coinid:0x...           /* the MAST-locked coin */
txnoutput id:m address:0xFF..AA amount:1
txnscript id:m scripts:{"RETURN TRUE":"0x0000010100...C7"}
txnpost   id:m auto:true
```

The JSON maps `script text → merkle proof`. For a single non-tree MAST script, leave the proof empty (`""`). If you skip `txnscript`, the post fails with `Script FAIL 0 MAST 0x72BE...` — the node has no way to know what code the root commits to. (`txnclear` wipes witness data if you need to retry.)

### The explicit PROOF + EXEC pattern

`MAST` is sugar over primitives you can also use directly — including with the revealed script delivered via **state variables**, which makes a generic "run any committed script" contract:

```
LET script = STATE(0)
LET proof  = STATE(1)
ASSERT PROOF(script 0 proof 0 0x0E321692DA8996A833F88EC8A73F3AA8A5E949AD12FF48207130C7AE6F9DC115)
EXEC script
```

Spend with `state:{"0":"[RETURN TRUE]","1":"0x0000010100...C7"}`. `PROOF` takes the same arguments as the `mmrproof` command (the two `0`s are the sum-values, unused for plain trees). In production, still require a signature — either around the EXEC or inside every leaf block — otherwise anyone holding a valid leaf+proof can spend.

### Using MAST to beat the 1200-char limit

Keep the hot path (phase logic, signature checks) in the main script; push cold paths (dispute resolution, emergency refund, upgrade) behind `MAST` roots. Main script stays small enough to post; cold-path code is only revealed and transmitted if it ever runs.

---

## 9. Testing workflow

### 1. `runscript` — offline unit tests (fast, first line of defense)

```
runscript script:"ASSERT SIGNEDBY(0xFF..AA) RETURN TRUE"
  state:{"0":"123"} prevstate:{"0":"100"}
  globals:{"@COINAGE":"50","@AMOUNT":"10"}
  signatures:["0xFF..AA"]
  extrascripts:{"RETURN TRUE":"0x00...proof"}
```

You control everything: state, prevstate, globals, which keys "signed", and MAST extra scripts. Returns parse-ok, run-ok, and the final TRUE/FALSE plus a variable trace.

**What it can't do:** there is no real transaction behind it, so `VERIFYOUT`/`VERIFYIN`/`GETOUT*`/`GETIN*`/`SUMINPUTS`/`SUMOUTPUTS` and the keepstate/`storestate` interplay are not meaningfully testable. `SAMESTATE` only reflects the state/prevstate maps you injected. A script can pass `runscript` all day and still be unspendable on-chain.

### 2. `txncheck` — structural validation of a built transaction

`txncheck id:<txnid>` validates the constructed transaction (inputs, outputs, amounts, scripts run against the real tx context). **It cannot evaluate `@BLOCK`, `@BLOCKMILLI`, or `@COINAGE`** — those don't exist until the tx is mined into a block. A tx that fails only on a timelock can look fine in `txncheck` and still be unspendable (or vice versa). Design so timelock paths can be exercised quickly on a test chain.

### 3. Private test chains — the real proof

Run a throwaway node with a private chain (`-test` / genesis mode, or an isolated "solo" node) so you get instant blocks and free funds. Post the real transactions for every path of your contract — creation, each phase transition, each ending. This is the only environment where `VERIFYOUT` + `storestate` + state carry-over + timelocks are all exercised for real. When tracking multi-phase contracts, follow **coinids** through transitions, not just phases — multiple concurrent instances share the same script address.

### 4. Offline bench: minima-vm

A native KISS VM test bench exists for scripted offline runs (fast iteration, CI-style testing of many cases). Same fundamental limitation as `runscript`: no real chain, so tx-context functions and mining-time globals aren't fully faithful. See `ecosystem-tooling.md` for the tool and its limits.

**Recommended pipeline:** `runscript` for logic → build with `txncreate`/`txninput`/`txnoutput`/`txnstate` → `txncheck` → post every path on a private chain → only then mainnet, with small amounts first.

---

## 10. Common mistakes

| Mistake | Result | Fix |
|---|---|---|
| Underscore in a variable name (`my_var`) | Silent parse failure — script just fails | Lowercase letters only: `myvar`, `mv` |
| Script > ~1200 chars | Silently rejected on-chain, no error anywhere | 2–3 char variable names; move cold paths to MAST |
| `@BLOCK` for deadlines | Unreliable on some synced nodes; untestable in `txncheck` | Store duration in state, check `@COINAGE GT dur` |
| `VERIFYOUT(... FALSE)` but output built with `storestate:true` (or vice versa) | Tx silently invalid | keepstate TRUE + `storestate:true` for phase transitions; FALSE + `storestate:false` for payouts |
| `XOR`/`AND` expecting bitwise math | Boolean result or type error | Word ops are boolean; `& \| ^ ~` need HEX operands; for entropy mixing use `SHA3(CONCAT(a b))` |
| `NUMBER(SHA3(x))` | Overflow / failure on 32-byte hex | `NUMBER(SUBSET(0 4 SHA3(x)))` |
| `SUBSET(data 0 4)` (docs order) | Wrong args — VM wants numbers first | `SUBSET(0 4 data)` — start, END index, data |
| Reading an unset state port | Execution error → FALSE | Set all readable ports at creation (`0` for unused), or guard reads by phase |
| `SAMESTATE(0 11)` when one port in range changes | FALSE — range must be identical end to end | Split around the changed port: `SAMESTATE(0 5)` + `SAMESTATE(7 11)` |
| No `RETURN TRUE` on a path | Coin unspendable via that path (default FALSE) | Every valid path ends in `RETURN TRUE` |
| Forgetting the token script | Token coin unspendable even though address script passes | Both scripts must return TRUE; default token script is `RETURN TRUE` |
| Posting a MAST spend without `txnscript` | `Script FAIL 0 MAST 0x...` | Attach revealed script + proof: `txnscript id:x scripts:{"<script>":"<proof>"}` |
| Deep `WHILE` loops | Instruction limit (1024) exceeded | Unroll, restructure, or redesign — KISS punishes loops |

---

## Appendix: the multi-phase contract idiom

The canonical KISS pattern for stateful contracts — a coin that recreates itself at the same address with updated state until a final payout. Skeleton (pubkeys are placeholders):

```
LET opk = PREVSTATE(0)          /* owner pubkey, set at creation */
LET ph  = PREVSTATE(6)          /* phase number */
LET dur = PREVSTATE(7)          /* timeout duration in blocks */

/* cancel: owner exits any time in phase 0 */
IF ph EQ 0 AND SIGNEDBY(opk) THEN RETURN TRUE ENDIF

/* advance: anyone moves phase 0 -> 1, must recreate the coin */
IF ph EQ 0 THEN
  ASSERT SAMESTATE(0 5)
  ASSERT STATE(6) EQ 1
  ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)
  RETURN TRUE
ENDIF

/* timeout refund: relative timelock via COINAGE */
IF @COINAGE GT dur AND SIGNEDBY(opk) THEN RETURN TRUE ENDIF

RETURN FALSE
```

Key moves: constants live in `PREVSTATE` (written once at creation), the phase port is the only one allowed to change (enforced by split `SAMESTATE` + explicit `STATE(n) EQ` checks), `VERIFYOUT(@INPUT @ADDRESS ... TRUE)` forces faithful recreation, and the escape hatch is a `@COINAGE` timelock. Commit–reveal randomness on top: parties store `SHA3(secret)` commitments in state, later reveal secrets, and the script derives the outcome with `NUMBER(SUBSET(0 4 SHA3(CONCAT(s1 s2)))) % range`.


---

## reference: maxima
<!-- (was references/maxima.md) -->

# Maxima — Off-Chain Encrypted Messaging

Maxima is Minima's information transport layer: every Minima node also runs Maxima, letting nodes exchange end-to-end encrypted data (text, JSON, files, transaction data) peer-to-peer over the same network — without touching the blockchain. It powers MiniDapp-to-MiniDapp communication: chat apps (MaxSolo, Chatter), DEX order relays, payment-channel negotiation, multiplayer games.

Full parameter dumps for `maxima`, `maxcontacts`, `maxextra`, `maxsign`, `maxverify`, and `maxcreate` live in `references/commands-node-network.md` — this file is the tutorial.

## Maxima gotchas

- **Delivery is NOT guaranteed.** Always check `response.delivered` on send, and design for messages that never arrive (timeouts), arrive twice (idempotency guards), or arrive out of order (check state before processing).
- **Large messages can corrupt in transit.** Transaction data relayed via Maxima between remote nodes can arrive truncated — `txnimport` then fails with `java.io.EOFException`. There is no fix except keeping payloads small and validating defensively on receipt.
- **For anything stateful, use a handshake.** A SYNACK/ACK exchange before proceeding, plus a phase/sequence guard against duplicates, is the difference between a demo and a production MiniDapp.
- **Contact addresses expire.** Share and add them immediately; for long-lived, publishable addresses you need a static MLS + permanent `MAX#` address.
- **Contacts cost work.** Each contact connection is maintained with small Tx-PoW; ~10–20 contacts is the practical per-node limit before performance suffers.

## Why Maxima exists

Where Minima is freedom of *value* exchange, Maxima is freedom of *information* exchange. Messages are:

- **End-to-end encrypted** with asymmetric RSA — relaying nodes cannot read them.
- **Signed** with the sender's Maxima private key, so the recipient can authenticate the origin.
- **Paid in work, not coins.** Every send requires a small Transaction Proof-of-Work (~1 second of hashing). This is the spam defense, and it also feeds hash power into securing the Minima chain. You need zero Minima coins to use Maxima.
- **Off-chain.** The message itself never appears on the blockchain; only the TxPoW unit (if it happens to be a block) propagates on-chain.

The send path: your node signs and encrypts the payload, mines a TxPoW unit, and sends both to the *recipient's* Maxima host — a randomly selected server node the recipient is connected through. The host verifies the work was done, then forwards the encrypted message to the recipient. Invalid or under-worked messages are discarded.

## Identity

Run `maxima` (or `maxima action:info`) to see who you are:

```bash
maxima
```

Key fields in the response:

- **`publickey`** — your Maxima public key (a long `0x3081...` RSA key). This is your stable Maxima identity; it never changes. Messages are addressed to it and signed by its private half.
- **`contact`** — your current **contact address**: a long `Mx...` string ending in `@ip:port` (the host you're currently routed through). **This changes at random intervals** for privacy — anyone holding an old one loses the ability to reach you.
- **`mls` / `staticmls`** — your current Maxima Location Service host and whether it's pinned.
- **`p2pidentity`** — relevant only for server nodes acting as hosts/MLS.
- **`name`** — your display name, set with `maxima action:setname name:alice`.

So: the *public key* is who you are, the *contact address* is where you currently live, and the *MLS* is the directory service that keeps the two linked for your contacts.

### Permanent MAX# addresses

Because contact addresses rotate, you can't print one on a website. If you run a second, always-on server node, you can build a permanent address of the form:

```
MAX#<yourMaximaPublicKey>#<staticMLShost>
```

Setup: pin your primary node to the server as static MLS (`maxextra action:staticmls host:<server p2pidentity>`), then on the *server* run `maxextra action:addpermanent publickey:0x3081...`. Anyone can now message you at the MAX# address forever — the static MLS answers `getaddress` requests with your latest rotating contact address. See "Advanced options" below.

## Contacts

Contacts are mutual: only one side needs to add the other's address, and both immediately see each other. Deleting a contact removes you from *their* list too, and since your contact address keeps rotating, they cannot silently re-add you later — this is the privacy model.

```bash
# Share your own address: copy the "contact" field from
maxima

# Add a friend (paste their FULL address including the @ip:port part,
# and do it immediately — addresses expire)
maxcontacts action:add contact:MxG18H....@<host>:9001

# List contacts — note each one's id, publickey, lastseen, samechain
maxcontacts

# Remove
maxcontacts action:remove id:1
```

In the `maxcontacts` listing, check **`samechain: true`** for each contact — `false` means you're on different forks and should re-add them. If a connection stays broken for more than 24 hours, delete and re-add with a fresh address.

## The MLS (Maxima Location Service)

Your MLS host is a randomly selected server node that tells your contacts your latest contact address whenever it rotates. As long as your node comes online at least once every 24 hours, the MLS keeps you and your contacts connected through address changes.

For privacy, your node moves to a *new* random MLS every 12 hours (staying registered with the previous one for a 12-hour overlap, so briefly-offline contacts can still find you). Net effect: a removed contact loses the ability to re-add you after at most 12 hours.

A **static MLS** replaces the random host with your own always-on server node — more stable connections, and the prerequisite for permanent MAX# addresses.

## Sending application messages

The workhorse is `maxima action:send`. Three ways to address the recipient, plus the two fields every MiniDapp cares about:

```bash
# by contact id (from maxcontacts)
maxima action:send id:1 application:myapp data:{"type":"HELLO","v":1}

# by contact address
maxima action:send to:MxG18H.... application:myapp data:0xFED5..

# by Maxima public key (with retry polling)
maxima action:send publickey:0x3081.. application:myapp data:0xFED5.. poll:true

# to a permanent MAX# address (recipient need not be a contact)
maxima action:send to:MAX#0x3081..#Mx...@<host>:9001 application:myapp data:{"hi":true}

# broadcast to every contact (optionally spaced out)
maxima action:sendall application:myapp data:{"type":"PING"} delay:500
```

- **`application`** is a free-form namespace string. Every listening MiniDapp on the recipient node receives the MAXIMA event; they filter on this field. Pick something unique to your dapp (e.g. `"dicegame_v1"`) and ignore everything else.
- **`data`** can be `0x` HEX or a JSON object. Either way it arrives at the far end as HEX — you decode it back.
- **`poll:true`** keeps retrying the send until it succeeds — useful for important messages to a contact who may be briefly offline. `delay:<ms>` postpones sending (used with poll/sendall).

The response includes a `delivered` boolean and a `msgid`. **Check `delivered`** — `false` means the message did not reach the recipient's host and you must retry or fail gracefully.

From a MiniDapp, wrap it in `MDS.cmd`:

```javascript
var payload = JSON.stringify({type:"MOVE", seq:seq, move:move});
MDS.cmd("maxima action:send id:1 application:myapp data:" + payload,
  function(resp){
    if (!resp.status || !resp.response.delivered) {
      MDS.log("Send failed — will retry");
      // schedule a retry / surface an error; do NOT assume it arrived
    }
});
```

## Receiving — the MAXIMA event

Incoming messages surface in MDS as a `MAXIMA` event, best handled in `service.js` so your dapp processes them even when no browser tab is open:

```javascript
// service.js
MDS.init(function(msg){
  if (msg.event == "MAXIMA") {
    // Only handle YOUR namespace — every dapp sees every message
    if (msg.data.application != "myapp") { return; }

    // msg.data.from  : sender's Maxima public key (authenticated)
    // msg.data.data  : the payload as 0x HEX
    // msg.data.msgid : unique message id
    var json = JSON.parse(hexToUtf8(msg.data.data));
    handleAppMessage(msg.data.from, json);
  }
});

function hexToUtf8(hex){
  hex = hex.startsWith("0x") ? hex.substring(2) : hex;
  var str = "";
  for (var i = 0; i < hex.length; i += 2) {
    str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
  }
  return decodeURIComponent(escape(str));
}
```

`msg.data.from` is trustworthy — the node has already verified the sender's signature and decrypted the payload before the event fires. Use it (not anything inside the payload) to identify who is talking to you.

## Delivery caveats — design for failure

Hard-won rules from production MiniDapps:

1. **Handle `delivered:false`.** Every send can fail. Retry with backoff or use `poll:true`, and tell the user when the counterparty is unreachable.
2. **Use timeouts for replies.** A message can be delivered but the response can be lost. For interactive flows (game rounds, order negotiation), 45+ seconds is a realistic timeout before declaring the exchange dead.
3. **Expect duplicates.** Retries and polling mean the same logical message can arrive twice. Guard with an idempotency check — a phase/state variable ("gamephase"), a sequence number, or a seen-`msgid` set — and drop anything that doesn't fit the current state.
4. **Expect out-of-order arrival.** Check your state machine before processing; never assume message N arrives before N+1.
5. **Keep payloads small.** Large blobs (notably serialized transactions passed between remote nodes) can arrive truncated. Symptom: `txnimport` throwing `java.io.EOFException` and one side stalling silently forever. Always validate:

```javascript
MDS.cmd("txnimport id:" + txid + " data:" + txndata, function(resp){
  if (!resp.status) {
    MDS.log("txnimport failed (corrupt Maxima transfer?): " + resp.error);
    return abortAndNotify();   // never continue on a broken import
  }
  // safe to continue
});
```

## Production messaging patterns

### SYNACK/ACK handshake

For any user-initiated action that commits both sides to something (channel open, game invite, trade), confirm the counterparty actually received it before proceeding:

```
A → B : {type:"SYN",    reqid:R, ...proposal}
B → A : {type:"SYNACK", reqid:R}        // "got it, proceeding"
A → B : {type:"ACK",    reqid:R}        // "confirmed, go"
```

Neither side changes durable state until the handshake completes; a timeout at any step rolls the flow back cleanly. Without this, a lost first message leaves one user staring at "Setting up..." forever.

### Phase guards against duplicates and crossed messages

Keep an explicit phase variable per peer/session (e.g. `0=idle, 1=request-sent, 2=active`) and enforce it on every incoming message:

- A duplicate `GAME_RESULT` while already back in phase 0 → drop it (prevents double-counting).
- An incoming `GAME_REQUEST` while *you* have already sent one (both users clicked simultaneously) → reject it; one side must win the race.

### One brain, one screen

Process Maxima messages in exactly one place — `service.js`. The UI (`index.html`) only renders notifications relayed from the service via `MDS.comms.solo()` and never handles `MAXIMA` events itself (the one conventional exception: responding to SYNACK in a UI-driven handshake). This guarantees each message is processed once, whether or not the dapp's window is open.

## Advanced options

- **Static MLS:** `maxextra action:staticmls host:<p2pidentity-of-your-server-node>` on your primary node; `host:clear` to revert to random hosts.
- **Permanent address:** on the static MLS server, `maxextra action:addpermanent publickey:<your primary node's Maxima pubkey>`. Others can then resolve you via `maxextra action:getaddress maxaddress:MAX#0x3081..#Mx..@<host>:9001` or just send to the MAX# address directly.
- **Lock down contact adds:** with a public MAX# address, anyone can add you as a contact. Disable that with `maxextra action:allowallcontacts enable:false` (messages still get through), then whitelist chosen peers with `maxextra action:addallowed publickey:0x...` / `listallowed` / `clearallowed`.
- **Signing arbitrary data:** `maxsign data:0xCD34..` signs with your Maxima private key (or a key made with `maxcreate` via `privatekey:`); the counterpart `maxverify data:.. publickey:.. signature:..` checks it. Useful for proving identity or authenticating off-band data — e.g. signing an order in a DEX relay so any third party can verify who posted it.

## Quick checklist for a Maxima-based MiniDapp

1. Namespace your messages: one unique `application` string, filter on it in `service.js`.
2. JSON payloads with a `type` field; decode from hex on receipt.
3. Check `delivered` on every send; retry or `poll:true` for must-arrive messages.
4. Timeouts on every expected reply; SYNACK/ACK before committing state.
5. Phase/sequence guards — drop duplicates and out-of-order messages.
6. Keep payloads small; validate `txnimport` (and any deserialization) on receipt.
7. Identify peers by `msg.data.from` (the verified public key), never by payload contents.


---

## reference: mds-development
<!-- (was references/mds-development.md) -->

# MDS MiniDapp Development

End-to-end guide to building a MiniDapp on **MDS** — the MiniDapp System, the app runtime
built into every Minima node. A MiniDapp is a self-contained web app (HTML/JS/CSS) that the
node serves over HTTPS and drives through a single JavaScript bridge, `MDS`. There is no
server to run: your code executes in the node's browser context (the page) and, optionally,
in a persistent background worker (`service.js`).

---

## MDS gotchas (read first)

These are the failures that cost the most time. Every one is silent — nothing throws.

- **`service.js` is ES5-only (Nashorn/Rhino).** No `async`/`await`, `Promise`, `Set`, arrow
  functions, `let`/`const`, or template literals. A single one makes the file fail to parse
  and **the daemon silently never starts**. Plain `var` + `function` + callbacks only.
- **256 KB reply cap → empty results, not an error.** A command reply over 256 KB trips the
  node's "too long" stub and you get an **empty** payload back. Bound every scan:
  `history max:4`, `depth:400`, coin queries limited.
- **Validate every `MDS.cmd` hop, `txndelete` on every error path.** Semicolon-chained
  commands return an **array** where each step fails independently. Null-check each hop; on
  any failure delete the transaction or it becomes a zombie that locks your coins.
- **Page and service share ONLY SQL.** `index.html` and `service.js` are two separate JS
  contexts. They do not share variables, closures, or memory — only the MiniDapp's SQL
  database and one-way `MDS.comms.solo()` messages.
- **SQL columns come back UPPERCASE.** `row.BLOCKNUMBER`, never `row.blockNumber`, regardless
  of how you cased the `CREATE TABLE`.
- **Every MDS function is async.** Treating `MDS.cmd`/`MDS.sql`/`MDS.file.*` as synchronous
  returns `undefined` and fails silently. Continue work **inside** the callback.

---

## Setup

### Option A — mds.js via `<script>` (plain JS)

Copy `mds.js` from the Minima repo into your MiniDapp folder and include it:

```html
<script type="text/javascript" src="mds.js"></script>
```

`MDS` is then a global. This is the only option that works for `service.js` (which is
loaded by the node, not bundled).

### Option B — `@minima-global/mds` (TypeScript SDK)

```bash
npm install @minima-global/mds
```

```typescript
import { MDS, MinimaEvents } from "@minima-global/mds";
```

The SDK ships types for the event payloads and command responses. Use it for the page/UI in
a bundled app; `service.js` still has to be hand-written ES5.

### The init pattern

Nothing works before `inited`. Register one handler and branch on `msg.event`:

```javascript
MDS.init(function (msg) {
  if (msg.event === "inited") {
    // MDS is ready — safe to run commands
    MDS.cmd("balance", function (res) {
      console.log(res.response);
    });
  }
});
```

### React / TypeScript note

In React, call `MDS.init()` **once** — inside a top-level `useEffect(() => { ... }, [])`
(or an app-bootstrap module), never inside a component that re-renders, or you register the
handler multiple times and every event fires N times. Push event data into state
(`useState`/store) from the init callback; render from state. Keep all `MDS.*` calls out of
render. The npm SDK's `MinimaEvents` enum gives you typed event names.

---

## The events to handle

`MDS.init`'s callback receives every event as `msg` with `msg.event` and (usually)
`msg.data`. Handle these:

| Event | Meaning | Payload |
|---|---|---|
| `inited` | MDS is ready; run first commands | none |
| `NEWBLOCK` | Chain tip advanced | `msg.data.txpow` — full TxPoW of the new block (`msg.data.txpow.header.block` = height) |
| `NEWBALANCE` | An unconfirmed coin arrived / balance changed | no useful data — call `MDS.cmd("balance")` to read it |
| `MINING` | Mining started/ended | `msg.data` = TxPoW object; started `true` / ended `false` |
| `MINIMALOG` | A new log line is available | the log message |
| `MDSAPI` | An API message from another MiniDapp (see `MDS.api.call`) | `msg.data` with the request + a `requestid` to reply to |
| `MDSFAIL` | A command failed at the bridge level | error info |
| `MAXIMA` | A Maxima message was received | `msg.data` — application, from, and data |

Also emitted: `MDS_TIMER_10SECONDS`, `MDS_TIMER_1HOUR` (periodic ticks — handy for polling
in `service.js`), `MDS_PENDING` (fires when the user accepts a pending transaction from the
Pending MiniDapp), `MDS_SHUTDOWN` (2 s warning before the MiniDapp system stops), and
`MDSCOMMS` (a `MDS.comms.solo`/`broadcast` message arrived — `msg.data.public` is `false`
for solo, and `msg.data.message` is the string you sent).

---

## MDS.cmd — running node commands

`MDS.cmd("<command with key:value args>", callback)`. The callback gets a response object;
its shape depends on the command:

```javascript
MDS.cmd("balance", function (res) {
  // res.response is an ARRAY of token balances:
  // { confirmed, unconfirmed, sendable, tokenid, token, coins, total }
  var confirmed = res.response[0].confirmed;   // tokenid "0x00" = Minima
});

MDS.cmd("getaddress", function (res) {
  // res.response.address (0x hex), .miniaddress (Mx…), .publickey, .script
  var addr = res.response.miniaddress;
});

MDS.cmd("send address:0xFF..DE amount:1.5", function (res) {
  if (res.error) { /* show res.error */ } else { /* sent */ }
});
```

### Chain-validation discipline

Semicolon-chained commands run in sequence and return an **array**, one entry per step. Any
step can fail while later steps come back `undefined`. **Every caller must null-check each
hop, and every error path must call `txndelete`** to avoid zombie transactions that hold
coins locked.

```javascript
// WRONG — crashes if any step failed
var chain = "txnimport id:" + txid + " data:" + txndata + ";"
          + "txnsign id:" + txid + " publickey:0xFF..AA;"
          + "txnexport id:" + txid + ";"
          + "txndelete id:" + txid + ";";
MDS.cmd(chain, function (resp) {
  callback(resp[2].response.data);   // resp[2] may be undefined
});

// RIGHT — validate every hop, clean up on failure
MDS.cmd(chain, function (resp) {
  if (!resp || !resp[0] || !resp[0].status) {
    MDS.log("txnimport failed: " + JSON.stringify(resp && resp[0] && resp[0].error));
    MDS.cmd("txndelete id:" + txid);            // never leave a partial txn
    callback(null);
    return;
  }
  if (!resp[2] || !resp[2].response) {
    MDS.log("txnexport failed");
    MDS.cmd("txndelete id:" + txid);
    callback(null);
    return;
  }
  callback(resp[2].response.data);
});
```

Route all transaction builders through **one helper that guarantees `txndelete`** on every
exit — success and failure. `txnimport` in particular can throw `EOFException` on truncated
data (common with large Maxima messages); it fails inside `service.js` with no browser
console trace, so always check `resp.status` before continuing.

### Async discipline

Every `MDS.*` function is async/callback. There is no synchronous return value — reading one
gives `undefined`.

```javascript
// WRONG
var outcome = computeOutcome(a, b);   // undefined
var r = outcome.result;               // throws / NaN

// RIGHT
computeOutcome(a, b, function (outcome) {
  var r = outcome.result;             // continue here, inside the callback
});
```

A whole transaction lifecycle is nested callbacks; if one link forgets its callback, the
chain stalls silently and no notification is ever sent.

---

## SQL (MDS.sql / H2)

Each MiniDapp gets a private H2 SQL database. Use it for persistent, multi-field, queryable
data. `MDS.keypair` is for simple settings.

```javascript
MDS.sql(
  "CREATE TABLE IF NOT EXISTS `metrics` (" +
  " `id` bigint auto_increment," +
  " `blockNumber` int NOT NULL," +
  " `blockHash` varchar(160) NOT NULL," +
  " `recipe` text," +               // NOT varchar — see quirk below
  " `blockSpeed` float NOT NULL)", function (res) { /* res.status */ });

MDS.sql("INSERT INTO metrics (blockNumber, blockHash) VALUES (100, '0xabc')", cb);

MDS.sql("SELECT * FROM metrics ORDER BY blockNumber DESC LIMIT 10", function (res) {
  if (res.status) {
    res.rows.forEach(function (row) {
      console.log(row.BLOCKNUMBER, row.BLOCKSPEED);   // UPPERCASE columns
    });
  }
});
```

### SQL quirks (all silent)

- **Columns return UPPERCASE** in `res.rows`, regardless of `CREATE TABLE` casing. Use
  `row.BLOCKNUMBER`.
- **`varchar(1024)` silently TRUNCATES.** A covenant script recipe can be ~1300 chars, so the
  row is silently dropped/cut. Use **`text`** for anything that can exceed the width, and
  verify column widths against the node's actual H2 engine.
- **An `UPDATE` can affect 0 rows while the callback still fires with `status:true`** (row
  didn't match, table locked, etc.). For any critical update, **verify with a follow-up
  `SELECT`** — don't trust the callback firing as proof the write landed.
- **Single-writer coordination:** page and service can both fire. Coordinate a single writer
  through a shared table with a `PRIMARY KEY` (and `INSERT … ` that fails on duplicate) so
  concurrent inserts from both contexts fail harmlessly instead of double-counting. Dedup by
  primary key, never check-then-insert (that races).
- **Cap unbounded tables** with a clip after each insert:

```javascript
MDS.sql("DELETE FROM metrics WHERE id NOT IN " +
        "(SELECT id FROM metrics ORDER BY blockNumber DESC LIMIT 4320)", function () {});
```

---

## File operations (MDS.file.*)

Files live in the MiniDapp's private data folder; a separate **web folder** serves assets
from the app's web root.

```javascript
MDS.file.save("data.json", JSON.stringify(obj), cb);        // write text/JSON
MDS.file.load("data.json", function (res) { JSON.parse(res.response); });

MDS.file.savebinary("f.bin", hexString, cb);                // binary is HEX-encoded
MDS.file.loadbinary("f.bin", function (res) { var hex = res.response; });

MDS.file.list("/", cb);                                     // list data folder
MDS.file.getpath("data.json", cb);                          // absolute path on node
MDS.file.copy("a.json", "b.json", cb);
MDS.file.move("old.json", "new.json", cb);
MDS.file.delete("f.json", cb);
MDS.file.makedir("myfolder", cb);

MDS.file.listweb("/", cb);                                  // web folder
MDS.file.copytoweb("local.js", "web/file.js", cb);
MDS.file.deletefromweb("web/file.js", cb);

MDS.file.upload(fileObject, cb);                            // chunks at 1 MB
MDS.file.download(url, cb);
```

---

## Key-value storage (MDS.keypair)

Simple per-MiniDapp settings. Async like everything else.

```javascript
MDS.keypair.set("username", "alice", cb);
MDS.keypair.get("username", function (res) { var v = res.value; });
```

---

## Inter-MiniDapp communication

All of these are **one-way** message primitives — there is no request/response socket.

```javascript
// Fire-and-forget to ALL MiniDapps
MDS.comms.broadcast("Hello all", cb);

// Fire-and-forget to your OWN service.js / page (same MiniDapp)
MDS.comms.solo(JSON.stringify({ type: "GAME_RESULT", winner: "player" }), cb);
// received as event MDSCOMMS: msg.data.public === false, msg.data.message === the string

// Named API to another MiniDapp — arrives there as an MDSAPI event
MDS.api.call("OtherDappName", data, function (reply) {
  console.log(reply.status, reply.data);
});
// the receiver answers with the requestid from its MDSAPI event:
MDS.api.reply("OtherDappName", requestId, responseData, cb);

// Get a link/URL to another installed MiniDapp
MDS.dapplink("OtherDappName", function (res) {
  if (res.status) window.location = res.base;
});
```

---

## Notifications, utilities, params, logging

```javascript
MDS.notify("Transaction sent!");     // system notification
MDS.notifycancel();

MDS.util.hexToBase64(hex);           // strip 0x first
MDS.util.base64ToHex(b64);
MDS.util.base64ToArrayBuffer(b64);
MDS.util.getStateVariable(coin, port);   // read a coin's state var

var uid = MDS.form.getParams("uid");     // read a GET param from the URL, or null

MDS.log("message");                  // → "Minima @ [timestamp] : message"

MDS.net.GET("https://api.example.com/data", cb);      // external HTTP (may be CORS-limited)
MDS.net.POST("https://api.example.com/post", "k=v", cb);
```

---

## service.js — the background worker

`service.js` runs persistently on the node even when the UI is closed. It is where all the
serious work goes: message handling, DB writes, signing.

### ES5 ONLY

The service worker is Nashorn/Rhino. **Forbidden:** `async`/`await`, `Promise`, `Set`,
arrow functions, `let`/`const`, template literals, and any other ES6+ syntax. Any single one
→ parse failure → **the daemon silently never starts**, no error anywhere. Write plain ES5:
`var`, `function`, string concatenation, callbacks.

```javascript
// service.js — ES5, callbacks only
MDS.load("dapplib.js");          // pull in shared functions

MDS.init(function (msg) {
  if (msg.event == "inited") {
    createDB(function () { MDS.log("service ready"); });
  } else if (msg.event == "NEWBLOCK") {
    saveNewBlock(msg);
  } else if (msg.event == "MAXIMA") {
    handleMaxima(msg.data);      // the brain handles ALL messages here
  }
});
```

### One Brain, One Screen

For any MiniDapp with background processing (games, channels, messaging, metrics):

```
service.js = THE BRAIN  — processes all messages, updates DB, signs transactions
index.html = THE SCREEN — displays notifications, handles user clicks
```

Rules:

- `service.js` **NEVER touches the DOM** (it has none).
- `index.html` **NEVER processes Maxima messages** (the one exception is a SYNACK/ACK
  handshake for user-initiated actions).
- Communication is **one-way**: `service.js → index.html` via `MDS.comms.solo()`.
- **One notification per event, one display update per notification.** Update stats,
  balances, logs in exactly one place — this is what prevents double-counting.

```javascript
// service.js — the ONLY place a result is computed, then announced once
function notify(data) { MDS.comms.solo(JSON.stringify(data)); }
notify({ type: "GAME_RESULT", result: 3, winner: "player", betamt: "10" });

// index.html — the ONLY place a result is rendered
MDS.init(function (msg) {
  if (msg.event === "MDSCOMMS" && !msg.data.public) {
    var comms = JSON.parse(msg.data.message);
    if (comms.type === "GAME_RESULT") onGameResult(comms);   // display only
  }
});
```

Persist-before-broadcast: write to SQL first, then `solo()`, so the screen reading the DB
always sees a consistent state.

---

## READ vs WRITE permission modes

A MiniDapp installs as **READ** by default; the user must explicitly grant **WRITE**
(`mds action:permission uid:0x.. trust:write`). This is a real runtime gate, not advice:

- On a **read-only** node, the `txncheck` post-gate simply **never posts** — privileged
  actions produce a pending-approval prompt or silently do nothing.
- An app that wants to **run clean in READ mode** (no approval prompts) must **drop
  privileged calls** — e.g. don't call `mds action:list`; read your version from a constant
  instead.
- A daemon that **needs WRITE** to function must **tell the user**, or it "looks fine but
  isn't" — the UI renders while the background actions silently no-op.

Never give WRITE to a MiniDapp you don't trust: WRITE is effectively full node access.

---

## MiniDapp structure + dapp.conf

```
myminidapp/
├── dapp.conf        # name, version, description, icon (required)
├── favicon.ico
├── index.html       # the screen
├── service.js       # the brain (optional, runs persistently — ES5 only)
├── styles.css
└── mds.js           # copy from the Minima repo
```

```json
{
  "name": "MyMiniDapp",
  "version": "1.0.0",
  "description": "What this MiniDapp does",
  "icon": "favicon.ico"
}
```

Package with `zip -r mydapp.mds.zip *` and install via the MiniHub UI
(`https://127.0.0.1:<MDS_PORT>`) or `mds action:install file:mydapp.mds.zip`. Full packaging
and distribution (stores, manifests, versioning) is covered in
`references/distribution-packaging.md`.

---

## Reference pattern 1 — minimal Wallet MiniDapp

`index.html` only. Shows balance + address, sends Minima. Re-reads balance on `NEWBALANCE`.

```html
<html>
  <head><script type="text/javascript" src="mds.js"></script></head>
  <body>
    <p>Balance: <span id="balance">0</span> (unconfirmed <span id="unconfirmed">0</span>)</p>
    <p>Address: <span id="address"></span></p>
    <input id="sendAddress" placeholder="Mx… or 0x…">
    <input id="amount" placeholder="amount">
    <button onclick="Send()">Send</button>
    <p id="status"></p>

    <script>
      function GetBalance() {
        MDS.cmd("balance", function (res) {
          if (!res.response || !res.response[0]) return;
          document.getElementById("balance").innerText =
            JSON.stringify(res.response[0].confirmed).slice(1, -1);
          document.getElementById("unconfirmed").innerText =
            JSON.stringify(res.response[0].unconfirmed).slice(1, -1);
        });
      }
      function GetAddress() {
        MDS.cmd("getaddress", function (res) {
          document.getElementById("address").innerText =
            JSON.stringify(res.response.miniaddress).slice(1, -1);
        });
      }
      function Send() {
        var addr = document.getElementById("sendAddress").value;
        var amt  = document.getElementById("amount").value;
        MDS.cmd("send address:" + addr + " amount:" + amt, function (res) {
          document.getElementById("status").innerText = res.error ? res.error : "Sent!";
        });
      }
      MDS.init(function (msg) {
        if (msg.event === "inited")     { GetBalance(); GetAddress(); }
        if (msg.event === "NEWBALANCE") { GetBalance(); }
      });
    </script>
  </body>
</html>
```

> Note: build commands from validated input only. Never concatenate an unvalidated value
> into a command string — `;` and quote-escapes let a crafted value append arbitrary commands
> at your privilege. Validate against a strict charset (hex/decimal) before interpolating.

---

## Reference pattern 2 — MiniStats metrics dashboard

Split across `service.js` (collect into SQL) and `index.html` (render). Demonstrates the
One-Brain/One-Screen split with SQL as the only shared state.

### service.js — collect on every block

```javascript
MDS.load("ministats.js");

MDS.init(function (msg) {
  if (msg.event == "inited") {
    createMetricsTable(function () { MDS.log("ready"); });
  } else if (msg.event == "NEWBLOCK") {
    saveNewBlock(msg);
  }
});
```

### ministats.js — shared ES5 logic

```javascript
function createMetricsTable(cb) {
  MDS.sql(
    "CREATE TABLE IF NOT EXISTS `metrics` (" +
    " `id` bigint auto_increment," +
    " `blockTime` varchar(160) NOT NULL," +
    " `blockHash` varchar(160) NOT NULL," +
    " `blockDifficulty` varchar(160) NOT NULL," +
    " `blockNumber` int NOT NULL," +
    " `blockSpeed` float NOT NULL," +
    " `chainWeight` varchar(160) NOT NULL," +
    " `cascadeWeight` varchar(160) NOT NULL)", function (res) { if (cb) cb(res); });
}

function saveNewBlock(msg) {
  var blockNumber = parseInt(msg.data.txpow.header.block);
  isBlockSaved(blockNumber, function (saved) { if (!saved) buildMetricRow(blockNumber); });
}

function isBlockSaved(blockNumber, cb) {
  MDS.sql("SELECT * FROM metrics WHERE blockNumber = " + blockNumber, function (res) {
    cb(res.status && res.rows.length > 0);      // dedup by primary key in production
  });
}

function buildMetricRow(blockNumber) {
  MDS.cmd("status", function (msg) {
    var c = msg.response.chain;
    storeMetric({ time: c.time, blockHash: c.hash, blockSpeed: c.speed,
      chainweight: c.weight, difficulty: c.difficulty,
      cascadeWeight: c.cascade.weight, blockNumber: blockNumber });
  });
}

function storeMetric(m) {
  var MAX = 4320;   // ~1 day at 20s/block
  MDS.sql(
    "INSERT INTO metrics (blockTime,blockHash,cascadeWeight,blockSpeed,chainWeight,blockDifficulty,blockNumber) " +
    "VALUES ('" + m.time + "','" + m.blockHash + "'," + m.cascadeWeight + "," + m.blockSpeed +
    ",'" + m.chainweight + "','" + m.difficulty + "'," + m.blockNumber + ")",
    function (res) { MDS.log(res.status ? "saved" : "failed"); });
  MDS.sql("DELETE FROM METRICS WHERE id NOT IN " +
          "(SELECT id FROM METRICS ORDER BY blockNumber DESC LIMIT " + MAX + ")", function () {});
}
```

### index.html — render (Chart.js)

```javascript
MDS.init(function (msg) {
  if (msg.event === "inited")   { displayMetrics(); displayCharts(); }
  else if (msg.event === "NEWBLOCK") { displayMetrics(); }   // avoid full reload if possible
});

function displayMetrics() {
  MDS.cmd("status", function (msg) {
    var c = msg.response.chain;
    document.getElementById("current-block").innerText = c.block;
    document.getElementById("block-speed").innerText   = (1 / c.speed).toFixed(1) + " s/block";
    document.getElementById("chain-weight").innerText  = c.weight;
    document.getElementById("total-weight").innerText  = msg.response.weight;
  });
}

function showChart(limit, canvasId, labelId) {
  MDS.sql("SELECT * FROM metrics ORDER BY blockNumber DESC LIMIT " + limit, function (res) {
    if (!res.status) return;
    var rows = res.rows;                           // columns UPPERCASE
    if (rows.length < limit) {
      document.getElementById(labelId).innerText = "N/A (< " + limit + " blocks)";
      return;
    }
    var total = 0, xdata = [], ydata = [];
    rows.forEach(function (row) {
      total += parseFloat(row.BLOCKSPEED);
      xdata.push(row.BLOCKNUMBER);
      ydata.push(parseFloat(1 / row.BLOCKSPEED));
    });
    document.getElementById(labelId).innerText = (1 / (total / rows.length)).toFixed(1) + " s/block";
    new Chart(canvasId, { type: "line",
      data: { labels: xdata.reverse(), datasets: [{ fill: false, data: ydata.reverse() }] },
      options: { legend: { display: false } } });
  });
}

function displayCharts() { showChart(10, "c1", "l1"); showChart(100, "c2", "l2"); }
```

---

## 2026 production gotchas

Hard-won lessons from real MiniDapps (wallets, history scanners, AMM pools, channels).

### The 256 KB reply cap — silent empty results

A command reply over **256 KB** trips the node's "too long" stub and returns **empty
results, not an error**. `history max:50` came back empty; `history max:4` (~160 KB) works.

- **Bound every scan.** `history max:4`, `depth:400`, cap coin/output counts.
- Don't `coinnotify action:add` a shared address without later removing it — the node retains
  every dust beacon posted there, growing the `scripts` reply without bound until it overflows
  the cap.

### The txncheck post-gate — read the verdict, not the count

Post a transaction only if **all** of these are truthy, else `txndelete`:

```javascript
function truthy(v) { return v === true || v === 1 || v === "1" || v === "true"; }

MDS.cmd("txncheck id:" + txid, function (res) {
  var r = res.response;
  if (truthy(r.valid.scripts) && truthy(r.validamounts) && truthy(r.valid.mmrproofs)) {
    MDS.cmd("txnpost id:" + txid);
  } else {
    MDS.cmd("txndelete id:" + txid);   // fail closed
  }
});
```

- It is **`valid.scripts`** (the covenant pass/fail verdict) — **NOT** the top-level
  `scripts` (a count). Reading the count instead of the verdict posts invalid transactions.
- The flags are **inconsistently typed** — sometimes bool, int, or string — so always route
  them through a `truthy()` helper accepting `true / 1 / "1" / "true"`.

### `history` response shape

`details[]` carry **no `txpowid`** and are **index-parallel** to `txpows[]` — associate them
by array index. Their inputs/outputs are token-sum maps, not coin arrays. Index-pair or
personal history renders nothing.

### Owner-coin funding exclusion

When funding a transaction against a contract, **exclude the pool/owner addresses** (each
`$OADR`) from coin selection, or `txnsign publickey:auto` may sign with the owner key
(`$OPK`) and trip the covenant's owner branch. Prefer signing with an **explicit public
key**, never `auto` — auto selection can conscript unrelated wallet keys into inputs you
never meant to spend.

### `newaddress` keys regenerate ASYNC after a seed restore

`newaddress` keys (index ≥ 64) are regenerated **asynchronously** after a seed-only restore.
Acting too soon fails with **"signing failed: Public Key not found."** Wrap sign call-sites
in an `ensureKeys([pubkey])` check (a no-op when the key is already held) before signing. Do
**NOT** bulk-regenerate keys on launch — it triggers a 256-key / pending-approval storm on a
re-paired node.

```javascript
function ensureKeys(pubkeys, done) {
  MDS.cmd("keys action:list", function (res) {
    var have = {};
    (res.response || []).forEach(function (k) { have[k.publickey] = true; });
    var missing = pubkeys.filter(function (p) { return !have[p]; });
    if (missing.length === 0) { done(true); return; }
    // regenerate only what's missing, one at a time; never bulk-regen on launch
    done(false);
  });
}
```

### Re-entrancy guard with a stuck-timeout on scan()

Page and service can both fire a scan concurrently. Guard with a flag **and** a stuck-timeout
so a crashed scan can't wedge the app forever:

```javascript
var scanning = false, scanStart = 0;
function scan() {
  var now = Date.now();
  if (scanning && (now - scanStart) < 120000) return;   // 2-min stuck-timeout releases it
  scanning = true; scanStart = now;
  doScan(function () { scanning = false; });
}
```

### Best practices

| Do | Why |
|---|---|
| Bound every `history`/scan (`max:4`, `depth:400`) | 256 KB cap returns empty, not error |
| Validate every `MDS.cmd` hop + `txndelete` on error | zombie txns lock coins |
| Sign with an explicit public key, never `auto` | avoid conscripting unrelated keys |
| Use `text` not `varchar(1024)` for scripts/recipes | `varchar` silently truncates ~1300-char recipes |
| Verify critical `UPDATE`s with a `SELECT` | UPDATE can affect 0 rows, callback still fires |
| Coordinate a single writer via a `PRIMARY KEY` table | page + service share only SQL |
| Persist to SQL before `comms.solo()` broadcast | screen always sees consistent state |
| Store contract addresses in coin state, use `$OADR`/PREVSTATE | `getaddress` returns different addresses over time |
| Set unused state ports to `0` | unset STATE ports crash the KISS VM |
| Keep ES6+ out of `service.js` | any ES6 → silent parse failure, daemon never starts |

### Common issues

| Symptom | Cause / fix |
|---|---|
| `service.js` never runs, no error | ES6 syntax in it → parse failure. Rewrite as ES5. |
| Query returns empty for no reason | reply exceeded 256 KB. Lower `max`/`depth`. |
| `resp[2]` is undefined in a chain | an earlier hop failed. Null-check each; `txndelete`. |
| Coin stays locked / can't spend | UTXO-locked until the previous txn confirms; also a leaked partial txn — `txndelete`. |
| `row.blockNumber` is undefined | columns are UPPERCASE — use `row.BLOCKNUMBER`. |
| Recipe rows silently missing | `varchar` truncation — switch the column to `text`. |
| "Public Key not found" after restore | `newaddress` keys still regenerating — `ensureKeys` before signing. |
| Privileged call silently no-ops | node is in READ mode — tell the user or drop the call. |
| Stale session / `serviceWorker Frame removed` noise | reopen the MiniDapp from MiniHub for a fresh session; the console noise is Chrome-extension chatter, not your bug. |

---

## Security lessons (fold into every MiniDapp)

These are pattern-level hazards, each stated as *hazard → what to do instead*.

- **Never build node commands by string concatenation with untrusted values.** `;` separates
  commands, so an unvalidated parameter appends arbitrary commands at the builder's privilege
  — and a value inside a quoted argument can close the quote and append more `key:value`
  tokens. Rejecting `;` alone is insufficient. Validate against a strict charset, or use a
  structured call interface.
- **Blocklist SQL "sanitisers" do not work.** Stripping `;`/`update`/`delete` from a free-form
  `where:` still allows unions, subqueries and comments. Drop free-form SQL fragments from any
  API; expose only typed, bound parameters.
- **Treat wallet *read* access as privileged.** Address, coin, balance and key-use enumeration
  deanonymises the whole wallet and feeds signature-reuse targeting. Require per-app,
  user-approved scopes — don't grant it to anything that can't spend. Don't ship a permission
  model that's commented out.
- **Escape every chain-sourced string before it reaches HTML.** Token names, state variables,
  app names and log details are attacker-writable and render in dashboards — unescaped output
  is stored XSS in your own session. Auto-escape on output and add a CSP.
- **Never treat an inbound notification/message as on-chain truth.** Anyone can forge a
  "payment received" event. A coinid, amount or address in an inbound message is
  attacker-controlled — resolve it yourself from your own chain scan by an immutable on-chain
  key and compare, then confirm against your node's chain view.
- **Never co-sign a transaction you didn't reconstruct yourself.** Importing counterparty hex
  and signing authorises every input it contains, including yours the attacker added. Require
  an exact match against outputs you recompute locally (address, amount, token, burn, state)
  and abort otherwise. Sign with an explicit key, never `auto`.
- **Winternitz keys are one-time.** Two signatures under one leaf leak the private key.
  Serialise signing through one gate, claim the leaf index atomically before signing, and on
  key-tree exhaustion **refuse to sign — never wrap around** to leaf zero.
- **Dedup by primary key, not check-then-insert.** A `SELECT`-then-`INSERT` races under
  concurrent message delivery; use a `PRIMARY KEY` and an insert that no-ops on duplicate.
  Avoid a single global "pending transaction" slot — key pending state by nonce/id in a table.
- **Validate input; return generic errors.** Unchecked `parseInt`, blind JSON casts and
  indexing possibly-empty arrays turn malformed input into unhandled exceptions; raw exception
  text discloses hosts, schema and versions. Validate types and lengths, log detail internally
  only.
- **Enforce security invariants at one greppable chokepoint.** "Transaction import happens in
  exactly one place, and nothing reaches signing without inspection returning clean" is
  mechanically verifiable after every change; the same rule spread across call sites is not.
- **Fail closed when a response doesn't match expectations.** If a validation gate can't find
  the field it expects in a node response, it must reject. Unknown shape = unknown state.
- **Client checks and contract enforcement are two layers — build both.** Inspection stops a
  bad transaction being signed; the covenant stops a co-signed one settling. Design so either
  alone rejects the theft.
</content>
</invoke>


---

## reference: native-android-ipc
<!-- (was references/native-android-ipc.md) -->

# Native Android IPC — companion apps and an on-device Minima node

How a native (non-MDS) Android app talks to a full Minima light client running on the
same phone, over Android broadcast-Intent IPC. This is the integration surface for
companion apps (wallets, faucets, mail, DEX/AMM front-ends) that live in their own APK and
drive a separately-installed **node app**.

Throughout, `<node-package>` is the Android package id of the installed node app (e.g. the
one that embeds `minima.jar`). Substitute your node's real package everywhere.

---

## Native IPC gotchas (read first)

- **A large response is an UNCATCHABLE node/app KILL.** Intent extras cross the Android
  Binder (~1 MB per transaction). An over-limit `sendBroadcast` throws
  `TransactionTooLargeException` with no try/catch in the path, so the OS **force-kills the
  process before any callback runs**. You cannot catch it and you cannot truncate app-side —
  the only lever is making the node return **less up front**. Bound `depth:`, use
  `simplestate:true`, request one token's `balance tokenid:` not all, and take a tiny first
  `history` page.
- **There are THREE distinct size caps — don't conflate them** (details below): a node-RPC
  256 KB "results too long" stub (catchable), an app-side `MinimaReceiver.MAX_MESSAGE_LEN`
  ~256 KB (an application constant, not in the core jar), and the native Binder ~1 MB (the
  uncatchable one).
- **Adaptive paging** is the portable mitigation: start `max:8`, halve on any
  over-limit/empty/errored page (8→4→2→1), and keep the smaller size.
- **Socket-opening commands fail over IPC with misleading errors.** `megammrsync`,
  `archive`, `restoresync` open a raw socket inline on the main thread →
  `NetworkOnMainThreadException` is swallowed → the user sees "Could not connect to Archive
  host!" though the host was never contacted.
- **`getaddress` rotates.** It returns a different one of the node's 64 permanent keys on
  every call. Never treat it as stable identity.
- **Pin `com.h2database:h2:2.1.214`.** 2.4.240 fails Android's ART verifier and hangs the
  node on "Syncing…".
- **Authenticate the caller — never trust self-asserted identity.** Package name, app id and
  "secret" in Intent extras are all chosen by the sender.

---

## What it is, and how it differs from MDS

A native Android node app embeds the **real Java `minima.jar`** in an Android service,
started in-process with `minima.mainStarter(...)` and driven with
`minima.runMinimaCMD(cmd, false, userid)`. It runs a full light client. A representative
startup profile:

```
-daemon -data <filesDir> -basefolder <filesDir> -port 11001
-isclient -mobile -limitbandwidth -allowallip
-nosyncibd -noshutdownhook -anyseed -seed <seed>
```

Divergences from a desktop/server node — internalize these:

- **No MDS subsystem** — no `-mdsenable`, no MiniHUB, no MDS web server.
- **No `.mds.zip` support** — MiniDapps do not install here at all. "Apps" are separate
  native Android APKs, each its own installed package.
- **No RPC HTTP port** — the only control surface is the in-process `runMinimaCMD`, reached
  through broadcast IPC.
- **Instead:** companions send terminal-style command strings (`balance`, `send ...`,
  `history max:50`, `getaddress`) and get back the **same JSON the terminal would**. Node
  events arrive via a `.NOTIFY` broadcast.

Because there is no HTTP surface, everything below is the *entire* integration surface.

---

## The `minimaapi` broadcast protocol

Actions (all prefixed with the node package):

| Action | Direction | Purpose |
|---|---|---|
| `<node-package>.REGISTER` | app → node | announce the app; create its `receive.db` row |
| `<node-package>.CMD`      | app → node | run one terminal command |
| `<node-package>.RESPONSE` | node → app | the command's JSON reply |
| `<node-package>.NOTIFY`   | node → app | pushed node event |

Extras: `PACKAGE_CLASS`, `APP_UID`, `REGISTER_MINIMAID`, `CMD_ACTION`, `RESPONSE_ID`,
`RESPONSE_RESULT`, `NOTIFY_DATA`.

Companion lifecycle:

```java
// 1. On first construction, mint two random 32-hex ids ONCE and persist them.
//    Use SecureRandom — NOT java.util.Random (clock-seeded, predictable). If this id
//    is the only thing distinguishing your traffic, it must be a real secret.
SharedPreferences p = ctx.getSharedPreferences("minima_api_prefs", MODE_PRIVATE);
String appUid   = p.getString("myapp_uid",  null);
String minimaId = p.getString("minima_uid", null);
if (appUid == null) {
    appUid   = randomHex32(new SecureRandom());
    minimaId = randomHex32(new SecureRandom());
    p.edit().putString("myapp_uid", appUid)
            .putString("minima_uid", minimaId).apply();
}

// 2. Register a RESPONSE receiver (and a NOTIFY receiver) for this app's actions.
ctx.registerReceiver(responseReceiver, new IntentFilter("<node-package>.RESPONSE"));

// 3. Fire REGISTER.
Intent reg = new Intent("<node-package>.REGISTER");
reg.setPackage("<node-package>");                 // unicast to the node ONLY
reg.putExtra("PACKAGE_CLASS", ctx.getPackageName());
reg.putExtra("APP_UID", appUid);
reg.putExtra("REGISTER_MINIMAID", minimaId);
ctx.sendBroadcast(reg);

// 4. Every command: setPackage + a fresh RESPONSE_ID to correlate the reply.
Intent cmd = new Intent("<node-package>.CMD");
cmd.setPackage("<node-package>");                 // MANDATORY on every command
String responseId = randomHex32(new SecureRandom());
cmd.putExtra("CMD_ACTION", "balance");
cmd.putExtra("RESPONSE_ID", responseId);
cmd.putExtra("APP_UID", appUid);
cmd.putExtra("REGISTER_MINIMAID", minimaId);
ctx.sendBroadcast(cmd);
// RESPONSE arrives with the matching RESPONSE_ID in RESPONSE_RESULT (a JSON string).
```

`setPackage(<node-package>)` on **every** outbound intent keeps the broadcast unicast to the
node, so no other app can intercept it.

**Node side** (`MinimaReceiver`): looks the app up by the triple
`(package, packageid, minimaid)` in a receive DB, and **refuses commands unless the app is
Enabled** (`penabled=1`). It picks the RPC user, runs `runMinimaCMD`, and broadcasts the raw
JSON reply back to the caller only.

---

## The Enable / Admin permission model (native equivalent of MDS READ / WRITE)

- An app **auto-registers on first launch but starts Disabled and non-admin**. Commands are
  refused until the **user flips Enable** in the node's Apps screen.
- **Admin** is a separate toggle. It maps the app's node user: a normal (enabled) app runs
  as RPC user **`0xFF`**; an **admin** app runs as **`0x00`** (full RPC).
- Drawing the installed-app list (icons/labels for the consent UI) needs the
  `QUERY_ALL_PACKAGES` manifest permission — see the security note on why that permission is
  a double-edged sword.

---

## NOTIFY events (node → app push)

On node events the service pushes `{event, data}` via `.NOTIFY` in `NOTIFY_DATA`, **only to
enabled apps** (unicast per app via `setPackage`). Events:

- `NEWBLOCK` — a new tip block.
- `NEWBALANCE` — wallet balance changed.
- `SHUTDOWN` — node is stopping.
- `LOAD_ALL_KEYS_START` / `LOAD_ALL_KEYS_FINISH` / `LOAD_ALL_KEYS_VALUE` — key-load progress.
- `MINIMALOG` — a line of node log output (drives a live Logs tail).

Gate handling on a minima-id check before trusting the payload — a companion's NOTIFY
receiver should verify the incoming minima-id matches its own persisted `minima_uid` (the
equivalent of `MinimaAPI.checkMinimaID`) and reject otherwise.

---

## The four hard truths about response size (fund / stability critical)

### 1. A large response CRASHES the node/app — uncatchable, not app-fixable

The node's IPC receiver runs the command and returns the result as an Intent String extra
via `sendBroadcast`, with **no try/catch**. Intent extras cross the Binder (~1 MB per
transaction), so a response over the limit throws `android.os.TransactionTooLargeException`
at `sendBroadcast`, and the OS **force-kills the app before any callback runs**. There is no
error object to catch and no chance to truncate — the process is already dead.

⇒ **The only lever is making the node return less up front.** Bound `depth:`, use
`simplestate:true`, take a tiny first `history` page, request `balance tokenid:<one>` rather
than the all-token balance.

Danger commands:

- **`history`** ≈ 14 KB per txpow (`action:list` always returns full txpow bodies, no lite
  mode). Default `max` ~100 ⇒ well over 1 MB on an active wallet. **Never** call `history`
  on a per-reload or per-block loop — poll it only while the History tab is visible, at most
  once per new block, bounded (e.g. `history relevant:true max:25 offset:N`).
- **`coins`** has **no `max:`/count cap** — only `depth:`, which is a **time** bound, not a
  count bound. So a feeless-PoW **dust flood** of a public/shared address can still overflow
  regardless of `depth:`. That residual is **not app-fixable** — it needs a **node-side
  coins count-cap**.

The node also processes commands serially and synchronously on the broadcast thread, so a
slow/heavy command blocks everything and a looping client can wedge or OOM the node.

### 2. Three DISTINCT size caps — do not conflate them

| Cap | Where it lives | Value | Behaviour |
|---|---|---|---|
| (a) node RPC "results too long" stub | inside `runMinimaCMD`'s result path over RPC | 256 KB | **CATCHABLE** — reply comes back empty/stubbed; you can distinguish TOO_LONG-vs-empty and adaptive-shrink-retry |
| (b) `MinimaReceiver.MAX_MESSAGE_LEN` | the **app-side** receiver — an APPLICATION constant, **NOT in the core jar** | ~256 KB | returns a "Result too long!" stub |
| (c) native Binder | Android OS, the `sendBroadcast` transaction | ~1 MB | **UNCATCHABLE** — `TransactionTooLargeException` → OS force-kill |

**Reconciliation of an earlier belief.** Earlier notes said "the node caps at 256 KB." That
is wrong. Exhaustive source search confirms `runMinimaCMD` → `CommandRunner.runMultiCommand`
applies **no** size limit; the only 256000 in the codebase is the **receiver's**
`MAX_MESSAGE_LEN` — an app-side constant. So (b) is an application concern, and the
physical ceiling is the Binder (c), not the jar.

### 3. The fix for unbounded responses — a `content://` FileProvider hand-off

Describe this as a **generic design pattern** (a forked node implements it; upstream may
not). When a result exceeds the receiver cap, instead of inlining it:

1. The client opts in with a new CMD extra `CMD_FILERESP=true` (set automatically inside the
   updated `minimaapi` `Command()`, so app code needs no change).
2. The node writes the large result to a per-package file in its cache
   (`cacheDir/ipcresponses/<responseid>.json`), exposed through a **non-exported
   FileProvider**, and grants **read to exactly the calling package**
   (`grantUriPermission`, plus a ClipData grant for newer Android).
3. The node broadcasts the normal RESPONSE intent carrying new extras **`RESPONSE_URI`** and
   **`RESPONSE_LEN`** instead of `RESPONSE_RESULT`.
4. The client, on RESPONSE, checks for `RESPONSE_URI` first; if present it opens the URI via
   `ContentResolver.openInputStream` (off the main thread), reads UTF-8, parses JSON, and
   delivers through the unchanged listener callback.

A **file descriptor** crosses the Binder, not the data, so response size becomes effectively
**unbounded** and the `TransactionTooLargeException` node-kill is gone for opted-in clients.

Compatibility is three-way: old client → new node ignores the unknown flag and gets the old
stub; new client → old node has its unknown extra ignored and gets the old stub; results
under the cap keep the existing inline fast path in every pairing.

Hygiene: response files pruned after ~5 minutes and wiped at receiver start; URI grants
revoked on prune; a failed file write falls back to the stub (never silence — a dropped
callback hangs the caller forever). The provider stays non-exported and the grant is
read-only to the one registered, enabled caller.

```
client CMD (CMD_FILERESP=true) ─▶ node runs command
                                    │  result ≤ cap → RESPONSE + RESPONSE_RESULT (inline)
                                    │  result > cap → write file, grant read to caller,
                                    ▼                  RESPONSE + RESPONSE_URI + RESPONSE_LEN
client reads URI off-main-thread ─▶ same listener.response(json)
```

### 4. Adaptive paging — the portable mitigation

For any app on the **old** `minimaapi`, or talking to an upstream/store node without the
file hand-off, page adaptively rather than with a fixed size:

```
max := 8
loop:
  page := node("history relevant:true max:{max} offset:{off}")
  if page is over-limit / empty / errored:
      if max > 1: max := max / 2   // 8 → 4 → 2 → 1
      retry same offset
  else:
      keep this (smaller) max for subsequent pages
      off := off + len(page)
```

A **fixed** page size is not safe: contract-heavy txpows are large, so even
`history relevant:true max:25` can exceed 256 KB on an active node.

---

## Socket-opening commands fail over IPC with misleading errors

Because the receiver runs `runMinimaCMD` synchronously on the process **main thread**,
commands that open a raw `Socket` inline — `megammrsync`, `archive`, `restoresync` — throw
`NetworkOnMainThreadException`. The internal catch-all swallows it (returns null), so the
user sees **"Could not connect to Archive host!"** even though the host is fine and was never
contacted. Desktop nodes are unaffected. Colon-parsing of `host:<ip>:9001` is fine
(first-colon split — the host was never the problem).

**Fix pattern:** run companion commands on a single background executor, not the broadcast
thread. This both removes the misleading socket failures and eliminates the ANR risk of slow
commands.

---

## Reading coins at a SHARED address you do not own

To read coins at an address the node does not already track (e.g. a shared registry/mailbox
sentinel address):

- `coins relevant:false address:X` returns **0**. `relevant:true` also returns 0 until the
  address is tracked.
- You must first **track** the address: `coinnotify action:add address:X`. This must be
  **redone every node start** (over native IPC you get no coin-notify callback, but the add
  still makes the address's coins indexed/retained).
- Then query with a **bare `coins address:X`** — **no `relevant:` flag**. That returns the
  coins.
- **Bound it.** `coins` has no `max:`/`offset:`, only `depth:` + `order:desc`. An
  **unbounded `coins address:X` on a busy shared address CRASHED a phone node** (it built a
  giant reply and died). Start small (`depth:4`), grow only while replies stay safe, shrink
  on over-limit, and throttle — do not scan on every `NEWBLOCK`/`NEWBALANCE`.

Coin `state` comes back as `[{"port":99,"type":1,"data":"0x..."}]`.

Posting a message coin includes **PoW** (seconds on a phone). **Disable the Send button**
while a post is in flight — repeated taps post duplicates, each with a fresh random id, so DB
dedup will not catch them.

---

## Other IPC truths

- **`getaddress` rotates across the 64 default keys.** Two back-to-back calls return
  different `publickey`/`miniaddress`. All 64 are permanent keys the node controls, so any
  returned key is yours to sign with — but never treat "whatever `getaddress` returned this
  session" as stable identity. Persist **one** chosen address+pubkey once (in prefs) for
  anything others address coins to. To test "is this coin mine", match the pubkey against the
  **full 64-key set** from `keys` (`response.keys[].publickey`), not a single key. To spend a
  script coin, sign with the key embedded in the coin's own state — one of the 64, not
  necessarily the persisted one.
- **`txnpost` mines ASYNChronously.** `response.istransaction` is **false** on return for a
  good tx — it only means "PoW not done yet"; the tx is accepted/queued and confirms a few
  blocks later. **Do not treat `istransaction:false` as failure.** Retrying on it causes a
  **re-post storm** every block that races itself and stalls confirmation for minutes.
  Success signal = `status:true`; confirm the real outcome from chain state.
- **`txnsign txnpostauto:true` returns a txpowid that is NOT the on-chain id.** Its
  `response.txpow.txpowid` (no leading zeros) is never found on the node or explorer. Resolve
  the **real** on-chain txpowid (leading-zero PoW hash) after mining by matching the **input
  coinids you spent** against `history`'s
  `response.txpows[].body.txn.inputs[].coinid` → that txpow's `txpowid` is the real one.

---

## H2 VerifyError on Android — pin the dependency

`com.h2database:h2:2.4.240`'s `org.h2.security.SHA256.getPBKDF2` is **rejected by Android's
ART bytecode verifier** (reproduced on Samsung S10+, Android 9–12). The node opens its H2 DB
with a password → `ConnectionInfo.hashPassword → SHA256.getPBKDF2` → `VerifyError` kills the
node's main thread at boot, and the wallet hangs forever on "Syncing…".

`minima.jar` bundles no `org/h2/*` classes, so the offending class comes solely from gradle.

**Fix — one dependency line:**

```diff
-implementation 'com.h2database:h2:2.4.240'
+implementation 'com.h2database:h2:2.1.214'
```

2.1.214 is the same H2 2.x API used by `org.minima.utils.SqlDB` and verifies cleanly on ART.
General rule: pin native-hostile deps to ART-verifiable versions.

---

## Keep IPC ids stable across a package rename

A namespace migration (renaming the app package) **must preserve** the persisted
`myapp_uid` / `minima_uid` in the app's prefs. The node identifies apps by
`(package, packageid, minimaid)`; if you regenerate the ids on rename, the node's existing
registration and enable-state for the app are orphaned and the user must re-enable. Carry the
old ids forward so existing registrations survive.

---

## JSON `\/` script-quoting trap (cross-reference)

The same `JSONObject.quote` hazard that bites covenant builders bites native builders too:
escaping `/` in a script/address string (via `JSONObject.quote`, which emits `\/`) can make
an address unspendable. See `references/contracts-covenants.md` for the full write-up — the
takeaway here is that any node command string you assemble from JSON-quoted values is exposed
to the same trap.

---

## versionCode convention

`versionCode = major*10000 + minor*100 + patch` (e.g. `0.4.2` → `402`, `1.3.0` → `10300`).

---

## Security checklist for a native companion

- **Authenticate the caller — never the self-asserted identity.** Package name, app id and
  "secret" in Intent extras are all chosen by the sender, so any app can claim to be a
  trusted companion — and if the UI renders that package's real icon/label, the impersonation
  wins the Enable/Admin toggle. On the node side, authenticate via `Binder.getCallingUid()`
  on a bound service, `PendingIntent.getCreatorPackage()`, or a signature-level permission
  before any command runs.
- **`SecureRandom`, not `java.util.Random`, for every id/secret.** Clock-seeded ids are
  predictable and are not secrets.
- **Fail closed on empty secrets.** A gate comparing an incoming id to a stored id that
  defaults to `""` authenticates everyone until the secret is provisioned. Reject blank
  stored and blank/null incoming values.
- **Export only components that need cross-app delivery** (`exported="false"` by default),
  null-guard and try/catch inside every exported receiver, and reject unknown actions early —
  never let an exception escape `onReceive`.
- **Run node commands off the broadcast thread** (`goAsync()` or a background executor);
  `onReceive` on the main thread turns any registered app's heavy command into an ANR.
- **Key permission mutations on the same identifier used for lookup.** If registration keys
  on `(package, package-id, minimaid)` but Enable/Admin updates match only
  `(package, package-id)`, one grant flips sibling rows and is not bound to the identity that
  authenticates traffic.
- **Store the seed encrypted and out of backups.** A mnemonic in plain `SharedPreferences`
  with `allowBackup="true"` is extractable via device transfer or cloud backup. Use
  `allowBackup="false"` plus Keystore-wrapped encrypted prefs — same for any store holding an
  IPC secret. Backup-rule XML is inert unless a `fullBackupContent`/`dataExtractionRules`
  attribute actually points at it — verify the attribute, not just the file.
- **`QUERY_ALL_PACKAGES` is both a privacy and a spoofing problem.** It exposes the full
  installed-app inventory and is exactly what lets an impersonating package resolve to a
  convincing icon and label. Prefer a scoped `<queries>` element, or do not render identity
  assets for unverified callers.
- **Do not broadcast the full event firehose to every enabled app.** Per-app subscription
  and filtering leaks far less than sending every chain/wallet event with full data to each
  app.
- **Prefer internal storage and client mode for the embedded node** — app-private storage, no
  inbound listener, RPC/MDS disabled. Relaxing peer-IP filtering re-adds LAN exposure; only do
  it if the topology needs it.


---

## reference: node-operations
<!-- (was references/node-operations.md) -->

# Node Operations — install, run, restore, recover

How to install, run, back up, restore, and troubleshoot a Minima node. Commands shown are typed
into the Minima terminal (CLI stdin, Terminal MiniDapp, RPC client, or `curl` to the RPC port)
unless prefixed with `java -jar` / `docker` / `sudo` (shell commands).

## Node-ops gotchas

Read these before touching a node that holds funds:

1. **`keyuses` on a seed restore is FUND-CRITICAL.** Minima signatures are stateful one-time
   WOTS leaves. When restoring from a seed phrase you MUST set `keyuses:` HIGHER than the true
   number of signatures you have ever made with those keys. Too low = the node re-issues
   already-spent leaves = self-inflicted key reuse = the exposure that loses funds.
   `keyuses:0` is only for a brand-new wallet that has never signed anything.
   (See `references/key-security-wots.md`.)
2. **Restore/resync with `megammrsync`, never `archive action:resync`.** `megammrsync` against a
   `-megammr` host lands you on the chain tip in seconds. `archive action:resync` replays the
   whole archive and takes hours. Same end state, 1000x the time.
3. **On a megammr/fresh node you cannot spend a coin at an untracked address**, even though
   `coins` shows it. You must `newscript trackall:true` + `coinexport` + `coinimport track:true`
   first. Symptom in the logs: `Script Missing from TxPoW for address 0x... TxPoW FAILS Basic
   checks`. The coin is never lost — just unspendable until tracked.
4. **The archive cannot recover deep-history spend destinations.** Archive data keeps coins
   (MMR), not witnesses: inputs, outputs, and signatures of transactions older than the
   unpruned window (~last 13k blocks) are gone from the archive format itself. No archive node,
   hosted or local, can tell you where an old spend went.
5. **Never poll long-running archive commands.** `archive action:integrity` takes minutes —
   issue it once and wait. Concurrent heavy archive queries can OOM a JVM with a bounded heap.
6. **Never ship a default backup password.** A backup of an unlocked node contains the
   unencrypted seed. A weak or hardcoded default password (in a script, an app, a doc example)
   means every backup is effectively plaintext. Always require a strong, user-chosen password.
7. **`-clean` deletes everything.** It wipes the node's data including keys. Only use it when
   you have the seed phrase written down or genuinely want a fresh node.

---

## Install and run

Minima is a single Java jar. Any platform with Java (11+) runs it:

```bash
java -jar minima.jar
```

With no parameters it connects to the main Minima network and works out of the box.
Full parameter list:

```bash
java -jar minima.jar -help
```

### Ports

A node uses a range of 5 ports starting at the base port (default **9001**, so 9001–9005):

| Port | Use |
|------|-----|
| 9001 | Main Minima P2P port. Open this externally on a server so your node acts as a relay. |
| 9002 | Not in use. |
| 9003 | MDS (MiniDapp System), **base port + 2**, served over **https** with a self-signed cert: `https://127.0.0.1:9003/` |
| 9004 | Not in use. |
| 9005 | RPC port (when `-rpcenable` is set). |

`-port [port]` moves the whole range (e.g. `-port 9010` → 9010–9014, MDS on 9012).

### MDS (MiniDapp System)

```bash
java -jar minima.jar -mdsenable -mdspassword YOUR_STRONG_PASSWORD
```

- MDS runs over SSL on base+2 (default `https://127.0.0.1:9003/`). Your browser will warn
  about the self-signed certificate — proceed past the warning.
- If you don't specify `-mdspassword`, a good one is generated for you; see it by running
  `mds` in the CLI.
- If the MDS login page is reachable from the internet, the password must be long and strong.
  Prefer firewalling 9003 to your own IP.

### RPC

RPC is off by default. Enable it at startup:

```bash
java -jar minima.jar -rpcenable -rpcpassword YOUR_RPC_PASSWORD -rpcssl
```

Then call commands over HTTP (URL-encode spaces as `%20`):

```bash
curl -k -u minima:YOUR_RPC_PASSWORD https://127.0.0.1:9005/status
curl -k -u minima:YOUR_RPC_PASSWORD "https://127.0.0.1:9005/balance"
```

Or use the bundled RPC client for an interactive terminal:

```bash
java -cp minima.jar org.minima.utils.MinimaRPCClient -password YOUR_RPC_PASSWORD -host https://127.0.0.1:9005
```

**Never expose port 9005 to the internet.** An open RPC port exposes your node and seed phrase.
If you must reach it remotely, firewall it to your own IP and always use `-rpcpassword` with
`-rpcssl` (Basic Auth without SSL is not secure).

In the interactive terminal: `exit` leaves the terminal, **`quit` shuts down the node** — don't
confuse them.

### Linux VPS as a systemd service

Install Java, create a `minima` user, download the jar:

```bash
sudo apt update -y && sudo apt install default-jdk wget jq -y
sudo adduser minima
sudo su minima && cd /home/minima
wget https://github.com/minima-global/Minima/raw/master/jar/minima.jar
exit
```

Create `/etc/systemd/system/minima.service`:

```ini
[Unit]
Description=minima
[Service]
User=minima
Type=simple
ExecStart=/usr/bin/java -jar /home/minima/minima.jar -rpcenable -rpcpassword YOUR_RPC_PASSWORD -rpcssl -mdsenable -mdspassword YOUR_MDS_PASSWORD -daemon -basefolder /home/minima -data /home/minima/.minima
Restart=always
RestartSec=100
[Install]
WantedBy=multi-user.target
```

Note `-daemon` — required for background/service mode (no stdin).

```bash
sudo systemctl daemon-reload
sudo systemctl enable minima
sudo systemctl start minima
sudo journalctl -u minima -f        # watch the logs (Ctrl+C to exit; node keeps running)
```

Firewall rules for a server node:

- **Allow 9001 in from anywhere** — makes your node a relay (backbone of the P2P network).
- **Deny 9002, 9004** (unused), **deny 9005** (RPC) from everywhere; RPC stays localhost-only.
- **9003 (MDS)**: allow only from your home IP, or keep closed for CLI-only nodes.
- If you run Docker on a VPS, do NOT rely on UFW — Docker bypasses UFW rules. Use the VPS
  provider's firewall.

The node does not auto-update. To update: stop the service, replace `minima.jar` with the
latest release, restart the service.

### Docker

```bash
docker pull minimaglobal/minima:latest
docker run -d \
  -e minima_mdspassword=YOUR_STRONG_PASSWORD \
  -e minima_desktop=true \
  -v ~/minimadocker:/home/minima/data \
  -p 9001-9003:9001-9003 \
  --restart unless-stopped --name minima minimaglobal/minima:latest
```

Startup parameters become environment variables in Docker: `-clean` → `-e minima_clean=true`,
`-megammr` → `-e minima_megammr=true`, `-archive` → `-e minima_archive=true`, and so on.
Run additional containers on shifted host ports, e.g. `-p 7001-7003:9001-9003`.

### Desktop

Windows/Mac users can use the Minima launcher (JNLP); custom startup parameters go in
Settings → "Use custom startup params". Or just run the jar from a terminal as above.
Android runs a full node via the official app.

---

## Key startup parameters

From `java -jar minima.jar -help` (Docker: convert to `-e minima_<name>=<value>`):

### General

| Parameter | Description |
|-----------|-------------|
| `-data [path]` | Config/data folder. Default: hidden `.minima` in the home directory. Always reuse the same folder when restarting an existing node. |
| `-basefolder [path]` | Default folder for file creation, backup, restore. Default: home directory. |
| `-conf [path]` | Configuration file (same keys as parameters: `-port 9008` → `port=9008`). CLI args take precedence. |
| `-port [port]` | Base port; range used is base to base+4. Default 9001–9005. |
| `-host [ip]` | Specify the host IP. |
| `-clean` | CAREFUL: wipes existing data, starts fresh. All coins (keys) on the node are lost. |
| `-daemon` | Run with no stdin (background services). |
| `-server` / `-desktop` / `-isclient` | Connection profile: accepts incoming connections / doesn't / tells P2P it can't. |
| `-allowallip` | Allow all IPs for Maxima/networking. |
| `-dbpassword [pw]` | AES password for the wallet/SQL DBs. Must be set on FIRST launch; cannot be changed later. |
| `-seed "[24 words]"` | Use this seed phrase when starting a new node. |
| `-archive` | Run as an archive node (store all sync blocks from now on). |
| `-megammr` | Run as a Mega MMR node (store all unspent coins + proofs for the whole chain). |
| `-rescuenode [host]` | MegaMMR node to resync from if you land on a heavier chain. |

### MDS

| Parameter | Description |
|-----------|-------------|
| `-mdsenable` | Enable the MiniDapp System (default port 9003). |
| `-mdspassword [pw]` | MDS login password. |
| `-mdsinit [folder]` | Install a folder of MiniDapps at startup. |
| `-mdswrite [minidapp]` | Give an initial MiniDapp WRITE access. |
| `-nodefaultminidapps` | Skip installing the default MiniDapps. |
| `-nosslmds` | Disable the self-signed MDS cert (put your own SSL proxy/stunnel in front). |
| `-publicmdsuid [uid]` | Session ID for the public MiniDapp system (Mega MMR nodes only). |

### RPC

| Parameter | Description |
|-----------|-------------|
| `-rpcenable` | Enable RPC (port base+4, default 9005). |
| `-rpcpassword [pw]` | Basic Auth password. Only secure combined with SSL. |
| `-rpcssl` | Self-signed SSL for RPC. |
| `-rpcclrf` | CRLF line endings in RPC headers (NodeJS clients). |

### Test / private networks

| Parameter | Description |
|-----------|-------------|
| `-test` | Test parameters (faster block times). |
| `-genesis` | Start from the genesis block (implies `-clean`). |
| `-nop2p` | Disable the automatic P2P system. |
| `-connect [ip:port,...]` | Disable auto-P2P and connect manually to these hosts. |
| `-noconnect` | Don't dial out until connected to. |
| `-solo` | Solo/private network: `-test -nop2p`, and runs `-genesis` only the FIRST time. |
| `-nosyncibd` | Don't sync the initial blockchain download (testing). |
| `-testchainlength [n]` | Tree length kept in `-test` mode (default 32). |

### Other useful

| Parameter | Description |
|-----------|-------------|
| `-txpowdbstore [days]` | Days of TxPoW kept in the internal H2 DB (default 3). |
| `-notifyalltxpow` | Fire a NEWTXPOW notification for every transaction/block (needed by MEG). |
| `-mysqlalltxpow` | Store all TxPoW to MySQL when MySQL autobackup is on. |
| `-limitbandwidth` | Limit archive-sync bandwidth. |
| `-showparams` | Print effective startup params at launch. |

---

## Private dev/test network

For an isolated chain for development and contract testing:

```bash
java -jar minima.jar -nop2p -test -genesis
```

- `-genesis` mines the genesis block and credits the node with **1 billion Minima** — your
  faucet. It implies `-clean`, so only use it the first time.
- Restart later WITHOUT `-genesis` to keep the chain: `java -jar minima.jar -nop2p -test`
- `-solo` is the convenient shorthand: `-test -nop2p` plus automatic `-genesis` on first run
  only. A solo node auto-mines roughly **one block every ~15 seconds** — the chain advances by
  itself. Do NOT send yourself transactions to "advance the chain"; just poll `balance` (or
  `block`) and wait for confirmations to mature.

Fund a test address from the genesis node:

```
getaddress
send amount:1000 address:MxG08...
balance
```

Connect a second local node to your private chain (different base port, own data folder):

```bash
java -jar minima.jar -connect 127.0.0.1 9001 -port 9010 -clean -data minimadata2
```

---

## Backup and restore

### Take a backup

```
backup password:YOUR_BACKUP_PASSWORD file:mybackup.bak
```

- `auto:true` enables automatic backups every 24 hours (latest 14 kept on the node) — but
  CLI auto-backups **cannot be password protected**; lock your node first, or set a
  password-protected auto-backup via the Security MiniDapp.
- **If your node is not locked, backups contain your unencrypted seed phrase.** Treat an
  unlocked-node backup exactly as you would the seed itself.
- The backup password cannot be changed or recovered. Choose a strong one and record it.
- Never bake a default password into scripts or apps that create backups — a known default
  makes every backup effectively unencrypted.
- Periodically download backups off the node and store them on a different device.

### Restore a backup

```
restore file:mybackup.bak password:YOUR_BACKUP_PASSWORD
```

The node shuts down when done; restart it to complete the restore. Variants:

- Backup < ~1 month old: `restore` alone, the network catches you up.
- Older backup: `restoresync file:... password:... host:<archive-node-ip:9001>` (archive host
  syncs the missing blocks), or the fast path below with `megammrsync ... file:yourbackup.bak`.
- `reset archivefile:archiveexport.gzip action:restore file:backup.bak password:...` replays
  from an archive file (slow legacy path).

Check restore progress programmatically (useful for tooling around a restoring node):

```
checkrestore
```

Returns `restoring`, `shuttingdown`, and `complete` flags.

If the node was locked when backed up, keys are still encrypted after the restore.

---

## MegaMMR seed resync runbook (fund-critical)

**To restore a wallet from a seed phrase, restore a backup to the tip, or fast-sync any node
onto the correct chain — use `megammrsync`. It completes in seconds. Do NOT use
`archive action:resync`, which replays the entire archive and takes hours.**

The `host:` must be a node running with `-megammr`. The public example host is
`megammr.minima.global:9001`; substitute your own mega node as `<your-megammr-host:9001>`.

### Case 1 — wrong chain / behind on sync, seed is fine

Host only. Keeps your current wallet, lands you on the correct tip:

```
megammrsync action:resync host:<your-megammr-host:9001>
```

### Case 2 — restore a backup and sync to tip

```
megammrsync action:resync host:<your-megammr-host:9001> file:yourbackup.bak
```

### Case 3 — restore from 24-word seed phrase (last resort, no backup)

```
megammrsync action:resync host:<your-megammr-host:9001> phrase:"YOUR 24 WORD SEED PHRASE" keyuses:2000
```

- This **WIPES the node** and rebuilds the wallet from the phrase.
- **`keyuses` is THE fund-critical parameter.** WOTS keys are stateful — every signature
  consumes a one-time leaf. `keyuses` tells the node how many leaves to skip as already used.
  - Brand-NEW wallet that has never signed: `keyuses:0`.
  - RESTORE of a wallet that has transacted: set it **higher than the maximum number of times
    you could possibly have signed** (docs default guidance: 1000 first restore, 2000 next,
    always increasing on every resync). Max is 262144 for normal keys.
  - Setting it too low re-issues already-spent leaves → key reuse → attackers can forge
    signatures over your funds. When in doubt, go higher — unused leaves cost nothing.
- `keys:` (optional) — number of key pairs to recreate; default 64 (all nodes create 64
  addresses; specify more if you used `newaddress`).
- `anyphrase:true` — required if the phrase is not a standard BIP39 24-word phrase.
- The node shuts down when the sync completes; restart it, give it a few minutes to connect.
- Do not use MiniDapps while a resync is in progress. Transaction history is deleted by a
  resync — export it from the Wallet MiniDapp first if you care.
- MiniSwap users: after a seed restore you must open MiniSwap (WRITE mode, set API keys) to
  re-register its script, then run a second chain resync to recover MiniSwap coins.

`help command:megammrsync` shows all options. The GUI equivalent is the **Security MiniDapp**
(Restore node → QuickSync / Import a Backup / Import Seed Phrase).

The slow legacy alternatives (`archive action:resync host:...`,
`reset archivefile:... action:seedsync phrase:"..." keyuses:...`) exist and work, but take
from many minutes to hours. Only reach for them when no `-megammr` host is available.

---

## Untracked-coin spend runbook

A `-megammr` (or freshly restored) node **cannot spend a coin at an address it does not
track**, even though `coins megammr:true address:0x...` displays the coin. `coins` returns
coin DATA for display; a spend needs the input's **script** and **MMR proof** attached at the
`txnbasics` step, and those come only from the node's tracked script/MMR DBs.

**Symptom** (node logs): `Script Missing from TxPoW for address 0x... → TxPoW FAILS Basic
checks`. The coin is safe on-chain the whole time — unspendable from this node, not lost.

**Fix, in order:**

```
# 1. Register the address script and track everything sent to it
newscript trackall:true script:"RETURN SIGNEDBY(0xYOUR_PUBLIC_KEY)"

# 2. Export the coin's proof out of the mega MMR
#    MUST be the FULL 66-char coinid — a truncated coinid returns a FALSE "Coin not found"
coinexport coinid:0x<FULL_66_CHAR_COINID>

# 3. (optional but recommended) validate the exported proof
coincheck data:0x<EXPORT_BLOB>

# 4. Import it into the tracked set
coinimport data:0x<EXPORT_BLOB> track:true

# 5. Verify, then spend normally
coins relevant:true
```

Notes:

- Coins that arrive AFTER the address is tracked are spendable normally — only coins created
  before tracking need the `coinexport`/`coinimport` backfill.
- **Token spends:** if the input is a custom token, rebuild the `Token` object from the
  byte-exact binary `coinexport` CoinProof — never by re-serializing the token-name JSON.
  JSON key reordering changes the bytes, the recomputed tokenid no longer matches, and the
  chain rejects the spend (`TokenID in Coin input 0 doesn't match token → TxPoW FAILS Basic
  checks`). Native MINIMA has no token descriptor and is unaffected.
- **Balance display:** for an untracked address, `balance megammr:true address:0x...` returns
  `sendable: 0` (the node can't compute "spendable by me" without tracking) and `total` is the
  token's total SUPPLY — not your balance. The real amount is in **`confirmed`**. Wallet UIs
  must display `confirmed`.

---

## Node types: full, mega, archive

All Minima nodes are full nodes — they validate everything and contribute PoW.

| | Full node (default) | Mega (MMR) node | Archive node |
|---|---|---|---|
| Startup flag | (none) | `-megammr` | `-archive` |
| Stores | Own coins + cascade; prunes blocks after ~24h | **All unspent coins + proofs** for the whole chain | **All sync blocks** since the node started (tx details still pruned after ~2 months) |
| Specs | 2 CPU / 2 GB RAM | 4 CPU / 8–16 GB RAM, 50 GB | 2 CPU / 4 GB RAM, 50 GB (grows) |
| Recovers other users | No | Yes (`megammrsync` host) | Yes (archive resync/export) |
| Export | MySQL | `.mmr` coin-history file | `.raw.dat` block-history file, MySQL |
| Uptime | Any | Must stay online (< 24h downtime) | Should stay online |

### Mega node

```bash
java -jar minima.jar -megammr -mdsenable -mdspassword YOUR_STRONG_PASSWORD
# Docker: add -e minima_megammr=true
```

Join the P2P network (default public peers list):

```
peers action:addpeers peerslist:https://spartacusrex.com/minimapeers.txt
```

Backfill the complete coin history by importing a recent `.mmr` export (< 1 week old) from
another mega node:

```
megammr action:import file:/path/to/megammr.mmr
```

A mega node is what `megammrsync` clients point at; it can also host public-facing MiniDapps
and create/post transactions on behalf of external users (see MEG below).

### Archive node

```bash
java -jar minima.jar -archive -data .minima -mdsenable -mdspassword YOUR_STRONG_PASSWORD
# Docker: add -e minima_archive=true
```

An archive node stores blocks **from the moment it starts**. To serve resyncs for any user,
first resync it from an archive node that has the chain back to genesis. Export for sharing:

```
archive action:exportraw file:archiveexport-ddmmyy.raw.dat
```

Verify archive integrity:

```
archive action:integrity
```

### Archive hard limits (what an archive CANNOT do)

- **Archive keeps COINS, not WITNESSES.** For blocks older than the unpruned window (~last
  13k blocks from tip), `txpow txpowid:0x...` returns `body.txn.inputs = []` and
  `body.txn.outputs = []`. Only block-level txpow (with `txnlist`) survives.
- Preserved: coin creation via the MMR (`archive action:addresscheck address:0x...` works back
  to archive genesis), coin state and value at creation, the spent block/timestamp of each
  coin, and the txpowid of the block containing the spend.
- NOT preserved: which coinids a deep-history transaction consumed, its output addresses and
  amounts, signatures, witnesses, proofs. **You cannot determine an old spend's destination
  from the archive alone** — the data is absent from the archive format itself, on every
  archive node. The only partial workaround is cross-referencing: for an outgoing spend at
  block N of amount A, look for coins CREATED at block N with amount A at addresses you can
  enumerate.
- **Commands that hang or hurt:**
  - `archive action:integrity` can run 3+ minutes on a large archive. Issue once, wait for the
    reply. Never poll it, and never fire it from a UI on a timer.
  - Concurrent heavy archive queries will OOM a JVM running with a bounded heap. Serialize
    them.
  - `txpow address:0x...` only searches roughly the last 13k blocks — it is NOT a full-history
    address index. For full history you need the MySQL export.

---

## MySQL export

Any node can export chain data to an external MySQL database for storage and querying beyond
the node's own pruning limits. Tables: `cascadedata`, `syncblocks`, `coins`, `txpow` (full
transaction detail, v1.0.41+).

```
# check connectivity
mysql action:info host:127.0.0.1:3306 database:archivedb user:archiveuser password:...

# store login so you can omit credentials afterwards
mysql action:setlogin host:127.0.0.1:3306 database:archivedb user:archiveuser password:...

# push new blocks/coins to MySQL
mysql action:update
```

`mysqlcoins` provides coin-specific export/queries against the same database. Start the node
with `-mysqlalltxpow` to include every TxPoW when MySQL autobackup runs. Check the node is on
the correct tip (`status`) before running an update. See `help command:mysql` for the full
action list (update, info, integrity, resync, wipe, ...).

---

## Cold storage (offline vault)

For large holdings: private keys live on a permanently-offline device; an online watch node
receives funds and builds transactions; the offline device signs.

Setup sketch (full flow in the official cold-storage guide):

```
# on the OFFLINE device (never connects to the internet again)
vault                                  # view 24-word phrase + 0x seed — write the words down
vault action:wipekeys seed:0x...       # wipe private keys from the node
backup file:nokeysbackup.bak password:...   # keyless backup for the online device
vault action:restorekeys phrase:"YOUR 24 WORDS"   # put keys back on the offline node
vault action:passwordlock password:... confirm:...  # encrypt keys with a password

# on the ONLINE device
restore file:nokeysbackup.bak password:...   # same wallet, watch-only (no keys)
```

Transacting: `sendnosign` (online, creates unsigned `.txn`) → USB → `sendsign file:...
password:...` (offline) → USB → `sendpost file:...` (online). `sendview file:...` inspects a
`.txn` at any time.

---

## MEG (Minima Enterprise Gateway) — pointer

MEG is a separate Java middleware service that exposes a Minima node over an HTTP/S API with
webhooks — triggers on on-chain events, custom endpoints, and a custodial Wallet API — aimed
at exchanges and enterprise integrations. It connects to the node via the RPC port and
requires the node to run with `-rpcenable -megammr -notifyalltxpow` (optionally
`-mysqlalltxpow`). MEG has a `-minkeyuses` startup parameter for the same WOTS-statefulness
reason described above. See the official "MEG" docs under Run a Node for setup.

---

## Node-recovery decision tree

```
Is the node running and the wallet correct, but behind / on the wrong chain?
├── YES → chain resync, keep the seed:
│         megammrsync action:resync host:<your-megammr-host:9001>
└── NO ↓

Do you have a backup file?
├── YES → restore file:backup.bak password:...
│         (older than ~1 week? use megammrsync ... file:backup.bak to land on the tip)
└── NO ↓

Do you have the 24-word seed phrase?
├── YES → megammrsync action:resync host:<your-megammr-host:9001>
│                    phrase:"..." keyuses:<HIGHER than your true signature count>
│         ── keyuses too low loses funds; go generously high. anyphrase:true if non-BIP39.
└── NO  → the wallet is unrecoverable. Nothing on-chain or in any archive can restore keys.

After any restore: node shuts down → restart it → wait a few minutes to connect →
verify with:  status   and   balance

Coin visible but spend fails ("Script Missing from TxPoW")?
└── Untracked address on a megammr/fresh node:
    newscript trackall:true → coinexport (FULL coinid) → coincheck → coinimport track:true → spend

Corrupt/broken local DBs, chain state unrecoverable, seed is safe?
└── Stop node → move the -data folder aside → start clean → megammrsync with phrase+keyuses
    (same fund-critical keyuses rule applies).

Need history older than the node keeps?
└── Archive node (blocks/coins) or MySQL export (full txpow) — but remember: deep-history
    spend destinations are not recoverable from the archive (witnesses are pruned).
```


---

## reference: protocol-architecture
<!-- (was references/protocol-architecture.md) -->

# Minima Protocol Architecture

A conceptual reference for how the Minima blockchain works. Minima is a complete, decentralized blockchain designed to be so compact that **every user runs a full constructing-and-validating node** — on a phone if they like. There are no miners, no master nodes, no delegates, and no ever-growing database.

## Key concepts at a glance

- **Every node is a Complete node.** All Minima nodes construct blocks *and* validate every transaction. There are no special user classes and no block rewards — security comes from millions of users each doing a small amount of Proof-of-Work.
- **TxPoW = Transaction + Proof-of-Work.** Users mine their own transactions (~a few seconds of hashing). If the resulting hash is, by chance, also below the block difficulty, that same TxPoW unit *becomes a block*. Blocks are just lucky transactions.
- **50-second block target**, adjusted every block (max ±10%), with consensus via **GHOST** (heaviest branch wins, not longest chain).
- **The Cascading Chain** compresses history: old blocks are pruned, but "Super Blocks" (blocks that by chance achieved 2x, 4x, 8x… the required difficulty) are kept in a 32-level logarithmic chain that preserves the total cumulative PoW without storing every block.
- **The MMR (Merkle Mountain Range)** is a "storage-less" proof database of all coins (UTxOs). Users store only proofs for *their own* coins plus the tree peaks — like keeping your own page of a book plus its spine — and present a CoinProof when spending.
- **Coins are UTxOs.** Every coin has a CoinID, amount, address (always Pay-to-Script-Hash), tokenid, and optional state variables. The Minima tokenid is `0x00`.
- **Tokens are Coloured Coins.** Custom tokens and NFTs are tiny fractions of Minima "coloured" to represent a supply; the network processes them identically to Minima with no extra storage cost.
- **Quantum-safe by design**: SHA3-256 everywhere, and Winternitz One-Time Signatures (WOTS) arranged in Merkle trees of keys. Each private key must only sign **once** (see the key-security reference).
- **The Magic numbers** are the chain's self-governing parameters (max TxPoW size, max txns/block, min TxPoW work, max KISS VM ops), recalculated every block from a heavily weighted average of the network's desired values — no hard forks needed to scale.
- **Layers**: Minima (L1, on-chain value), Maxima (off-chain P2P information transport), Omnia (L2 payment channels), and MiniDapps (Web3 apps via the MiniDapp System, MDS).

---

## What Minima is

Minima's whitepaper premise: paying a small group of miners (or validators, or delegates) to run a chain inevitably centralizes it. Minima's answer is a network where *every single user is an equal and Complete member* — each node validates the whole chain **and** participates in constructing it. Disrupting the network would mean attacking, bribing, or coercing the entire user base.

This forces the design: the protocol must be resource-efficient enough that anyone can run a Complete node at all times (including on a phone); there are no paid miners; and the protocol must be *complete* (finished) from inception — scalable from day one and quantum secure — because with everyone as a miner there are no soft forks, and hard forks become impossible at scale. Ossification is the goal, as with TCP/IP or SMTP.

There are no block rewards and no minimum holding required to run a node. Maximum supply is one billion Minima, all created at genesis; the supply is deflationary via the Burn (see below).

## TxPoW: users mine their own transactions

**TxPoW (Transaction Proof-of-Work)** is Minima's unifying primitive: the transaction and the proof-of-work are one object. When a user sends a transaction, their own device hashes the TxPoW header, incrementing a nonce until the hash meets the **transaction difficulty** — roughly a second of work on the device. Only then may the TxPoW be propagated to the network and enter the mempool.

Crucially, the TxPoW header is *also* a candidate block header representing the node's current view of the chain. If the mined hash happens to also be below the much harder **block difficulty** target, the TxPoW becomes a block (a **TxBlock**) and is added to the chain. Sometimes, when you send a transaction, you also find a block. This is how the chain is constructed with no dedicated miners: everyone's small contributions sum to the chain's total PoW (an idea descended from P2Pool and HashCash).

A TxPoW unit contains:

- **Header**: nonce, Chain ID (must be `0x01`), timestamp, block number, block difficulty, cascade levels (32), Super Parent references, MMR root hash, MMR total (the sum of all coins — making total supply provable every block, eliminating inflation bugs), the Magic numbers, and the hash of the body.
- **Body**: a random number (so every node mines a different candidate), the transaction difficulty, the **main transaction** (inputs, outputs, state variables, link hash), its **Witness** (signature proofs, coin proofs, script proofs — each an MMR proof), an optional **Burn transaction** with its witness, and a **txn list** of mempool transaction *hashes* to include if this unit becomes a block.

Blocks reference transactions only by hash (like Bitcoin compact blocks) since the transactions were already relayed — so blocks are tiny.

**The Pulse.** Nodes that aren't transacting still secure the chain: periodically every node mines a TxPoW with an *empty* transaction (a Pulse), sharing its latest chain view and mempool with peers — and if it happens to meet block difficulty, the Pulse becomes a block. Peers that fail to Pulse are dropped. There are no fees; instead the **Burn** (any excess of inputs over outputs, destroyed from circulation) orders mempool transactions, regulates congestion, and makes DDoS spam expensive.

## The blockchain: TxPoW Tree + Cascading Chain

The chain has two parts:

1. **The TxPoW Tree** — the recent, unpruned portion: the most recent 1024 blocks, plus any natural branches caused by simultaneous block finds. Its root connects to the tip of the Cascade.
2. **The Cascading Chain (the Cascade)** — a compressed, unbroken chain of older block headers that preserves the chain's total cumulative Proof-of-Work without storing every block.

### How the Cascade works

Random hashing means some blocks, by luck, vastly exceed the required difficulty. A block that achieved at least 2^L times the block difficulty is a **Super Block** capable of standing in for 2^L ordinary blocks' worth of PoW. The Cascade defines **32 levels (0–31)**, each twice as hard as the one below, with a maximum of **128 blocks per level**. The probability of reaching level L is 1/2^L (a level-L Super Block appears on average every 50×2^L seconds), so the Cascade grows only logarithmically while its total weight tends toward the weight of the full unpruned chain.

Every block header carries **Super Parent** references — a pointer to its immediate parent and to the most recent block at each existing Super level — keeping the Cascade unbroken after pruning.

**Cascading process**: once the main chain reaches 1124 blocks (1024 + 100), the oldest 100 blocks are folded into the Cascade. Working backwards, blocks fill level 0 (128 slots); beyond that, only blocks meeting level-1 difficulty survive into level 1, then level 2, and so on — everything else is pruned. Before pruning, the new tree root's MMR set is updated with CoinProofs for tracked unspent coins, so no coin proofs are lost. A Super Block's **current weight** = base weight × 2^(current level).

The Cascade gives any newcomer objective, independently verifiable proof of the heaviest (hence valid) chain — Nakamoto consensus without the full history.

## Mining, difficulty, and the Magic numbers

Minima has three difficulty tiers, all evaluated against the hash of the TxPoW header (the TxPoW ID):

| Tier | Purpose | Target |
|---|---|---|
| **Transaction difficulty** | Admission to the network; anti-spam | ~1 second of work on the sending device (minimum equivalent to 1M hashes); mining stops once met |
| **Block difficulty** | Keeps blocks at ~50-second intervals | Adjusts every block by up to ±10% based on the median-smoothed speed of the last 256 blocks |
| **Super Block difficulty** | Positions blocks in the Cascade | Level L = 2^L × block difficulty, achieved by chance |

The node never *tries* to meet block difficulty — it mines only until the transaction target is met, then checks whether it lucked into a block.

**Magic numbers** are the four consensus parameters that future-proof the network without forks:

- `CurrentMaxTxPoWSize` — max TxPoW size in bytes (default/minimum 64 KB)
- `CurrentMaxTxnPerBlock` — max transactions per block (default/minimum 256)
- `CurrentMinTxPoWWork` — minimum PoW for a TxPoW to be relayed (minimum ≈ 1M hashes)
- `CurrentMaxKISSVMOps` — max KISS VM script operations per TxPoW (default/minimum 1024)

Each node may state a *Desired* value (between half and double the current value); every block, the Current values are recalculated as a 16383:1 weighted average favoring the network value. If the whole network agrees on a change, the chain converges to it over ~50 days. As phones get more powerful, Minima grows with them — "magically," with no protocol upgrade.

**Consensus (GHOST).** Branches occur naturally when two blocks of the same height are found. Minima uses **GHOST** (Greedy Heaviest Observed SubTree): the main chain is the *heaviest* branch — the most cumulative PoW weight — not the longest. This tolerates fast block times and defeats secret attacker chains, since a long-but-light chain never outweighs the branch the whole network is building on.

**Validation.** Every node validates every TxPoW it mines or receives: chain ID, difficulty, signatures, scripts, MMR CoinProofs, no double-use of coins, timestamps, burn-transaction link hash. Invalid units get the sender disconnected; borderline units (e.g. non-monotonic scripts that may become valid later, or coins also seen in the mempool) are kept but not forwarded.

## The MMR: a storage-less proof database

Minima has **no global UTXO database**. Instead it uses a **Merkle Mountain Range (MMR)** — an append-only hash-sum tree (based on Peter Todd's design) containing every transaction output (spent or unspent) as a leaf.

The book analogy from the docs: *if all coins were pages in a book, each user keeps only their own pages plus the spine. To spend, you present your page (CoinProof); anyone can check it fits their copy of the spine (the MMR peaks/root).*

Mechanics:

- Coins are hashed in pairs into the largest possible binary trees; unequal leaf counts create multiple peaks — the "mountain range." Bagging the peaks left-to-right yields a single root, which every block header commits to.
- Each tree node holds **MMRData** = (hash, value): for a leaf, the hash of the coin object and its Minima value (0 if spent); for parents, the hash-sum of children. The root's value is thus the total Minima in existence, checked every block.
- Every entry has a coordinate [Row, Entry]; the parent of [R,E] is [R+1, floor(E/2)]. Max 256 rows → up to 2^256 coins ever.
- A **CoinProof** is a list of proof chunks (sibling hashes + left/right flags) from a coin up to a peak. Any node can recompute the path; if the computed peak matches its own, the coin provably exists and is unspent.
- Spending flips the coin's spent flag (changing its leaf hash); outputs append new leaves. So **proofs change every block** — each block carries an MMR Set of updated entries, and users must stay reasonably in sync. A user offline too long doesn't lose coins, but needs an archive/mega node (or a friend tracking their proofs — coin proofs are not security-sensitive) to refresh them.
- Separate MMRs store **Signature Proofs** and **Script Proofs**.

This is why Minima is called *storage-less*: a normal node stores only its own coins' proofs plus the peaks — orders of magnitude less than a full ledger — while a **Mega-MMR node** opts to hold the proofs for *every* unspent coin on the network, so it can serve proofs to others and help users recover.

## The coin (UTXO) model and state

Every coin (UTxO) has:

| Attribute | Meaning |
|---|---|
| `coinid` | Globally unique ID: hash(first input coin of the creating txn \| output index) |
| `amount` | Amount of Minima (token transactions are just amounts of coloured Minima) |
| `address` | Hash of a script — **all Minima addresses are P2SH**. Default script: `RETURN SIGNEDBY(owner_public_key)` |
| `tokenid` | `0x00` for Minima; a full 64-byte hash for any custom token |
| `floating` | If true, the CoinID is ignored — any coin matching amount/address/tokenid can satisfy the input (used by advanced contracts, e.g. ELTOO) |
| `storestate` / `state` | Whether/which **state variables (0–255)** from the creating transaction are stored on the coin — readable from scripts, enabling stateful contract sequences |
| `mmrentry`, `spent`, `created` | MMR leaf number, spent flag, creation block |

A transaction = inputs (coins being spent, each requiring a CoinProof, script proof, and signature) + outputs (new coins: payments and change) + state variables + link hash. Outputs must not exceed inputs; **any difference is burned**, permanently reducing supply. Every coin is guarded by a KISS VM script that must return TRUE to be spent — the default is simply "signed by the owner," but any contract (HTLCs, covenants, MAST, oracles, vaults) is expressible. Script execution is bounded by the `CurrentMaxKISSVMOps` magic number.

## Coloured coins: native tokens and NFTs

Minima supports custom tokens **natively** — no token contract standard, no extra load on the network. A token is created by *colouring* a fraction of Minima:

- Minima carries 44 decimal places, so colouring even `1e-33` Minima can represent, say, 1000 tokens with 8 decimals (via a **scale** factor).
- The **tokenid** is created by hashing the creating coin's ID and total amount — globally unique. Minima itself is always tokenid `0x00`.
- Token metadata: name/description (a string or full JSON), total supply, decimals, scale, and an optional **token script** validated *in addition to* the coin's address script on every spend.
- **NFTs** are simply tokens with 0 decimals — spendable only whole.

Token transactions are stored, proved, and validated exactly like Minima transactions in the MMR; the network doesn't distinguish them.

## Quantum-safe signatures (overview)

Minima's consensus-critical cryptography must never need replacing, so it is post-quantum from genesis:

- **Hashing**: SHA3-256 everywhere — TxPoW mining, block and transaction hashes, proof chains, signing. (KISS VM also exposes SHA2-256 for cross-chain hash-lock contracts with legacy chains.)
- **Signatures**: the **Winternitz One-Time Signature scheme (WOTS)**, parameter 8 — hash-based and quantum-resistant, but **each key pair may safely sign only once**. Signatures are large (400–800 bytes, 10–20x ECDSA) but are pruned like almost everything else — a bandwidth cost, not a permanent one.
- **Trees of keys (Merkle Signature Scheme)**: many WOTS key pairs are stored as leaves of MMR key trees whose root hash is a reusable **root public key**. Minima builds a 3-level *tree of trees* with 64 keys per tree, giving 64^3 = **262,144 one-time signatures per root public key**, each signature carrying a proof path to the root.

The one-time property is a real operational constraint (why key reuse and certain backup/restore patterns are dangerous) — see the **key-security** reference for the practical rules.

## The network: four layers

1. **Layer 1 — Minima**: the blockchain; on-chain value transfer, flood-fill propagation, processed by every node. Where transacting relationships are established and L2 disputes settle.
2. **Maxima**: the off-chain, point-to-point **information transport layer** over the same P2P network — encrypted user-to-user messaging and data. Maxima traffic *also* pays PoW, so off-chain activity strengthens Layer 1 security instead of starving it (the inverse of fee-based chains).
3. **Layer 2 — Omnia**: bi-directional payment channels using **ELTOO** (Lightning-style, more flexible) for instant, effectively unlimited-TPS off-chain payments and contract sequences, settled on L1 only when needed.
4. **Layer 3 — MiniDapps**: decentralized applications built with JavaScript/HTML/CSS on the **MiniDapp System (MDS)**, combining value transfer (Minima), messaging (Maxima), scaling (Omnia), and KISS VM contracts.

The default P2P port is 9001; nodes accepting incoming connections additionally act as relays strengthening the network backbone.

## Node types

**All Minima node types are full, Complete nodes** — every one creates blocks, validates all transactions, and contributes PoW. They differ only in how much *extra* history/proof data they keep:

| Type | Keeps | Typical host | Notes |
|---|---|---|---|
| **Full node** (default) | ~24h of full blocks + the Cascade + proofs for *its own* coins only | Phone, Pi, desktop | Minimal storage; the standard experience |
| **Mega-MMR node** | Everything a full node has, **plus proofs for the complete unspent coin set** (the Mega MMR) | Server (must stay online; <24h downtime) | Serves coin proofs, resyncs/recovers other users, hosts public MiniDapps and web wallets, posts transactions on behalf of external/light users |
| **Archive node** | All sync blocks (headers + coin proofs) from when it started — no pruning; full txn details still pruned after ~2 months | Server/desktop | Resyncs nodes back to genesis (if seeded from a genesis archive file); exports chain history to bootstrap new archive/mega nodes |

"Light/read-only" usage (e.g. a web wallet) is served *by* a Mega-MMR node, which can create and post transactions on behalf of external users — the light user still relies on a Complete node, keeping the chain itself uniform.

## Glossary of Minima terms

- **TxPoW** — Transaction Proof-of-Work: the unified transaction+candidate-block unit every user mines themselves. Its header hash is the **TxPoW ID**.
- **TxBlock** — a TxPoW whose hash also met block difficulty, added to the chain with MMR peaks, spent-coin proofs, and new-coin list.
- **Coin** — a UTxO: `coinid`, amount, P2SH address, tokenid, optional state.
- **CoinProof** — the MMR path chunks proving a coin exists and is unspent; required per input.
- **MMR** — Merkle Mountain Range: the append-only hash-sum proof tree of all coins (also used for signatures, scripts, and key trees).
- **Mega MMR** — the complete unspent-coin proof set, held by Mega-MMR nodes to serve the network.
- **Cascade** — the 32-level compressed chain of Super Block headers preserving total cumulative PoW after pruning.
- **Super Block** — a block that by chance achieved ≥2^L times block difficulty, earning Cascade level L.
- **Magic** — the four self-adjusting network parameters (max TxPoW size, max txns/block, min TxPoW work, max script ops) converged via weighted per-block voting.
- **GHOST** — Greedy Heaviest Observed SubTree; heaviest-branch chain selection.
- **Pulse** — the periodic empty-transaction TxPoW every node mines to sync peers and add PoW; no Pulse, no peering.
- **Burn** — inputs minus outputs, destroyed from supply; orders transactions and throttles spam. No minimum.
- **Maxima** — the off-chain, point-to-point information layer of the Minima network.
- **MDS** — the MiniDapp System: the runtime and API through which MiniDapps (JS/HTML apps) use the node.
- **KISS VM** — Minima's simple, Turing-complete scripting language; every address is the hash of a KISS VM script (see the smart-contracts reference).
- **tokenid** — a token's unique 64-byte identifier; `0x00` is Minima itself.
- **State variables** — up to 256 per-transaction data registers (0–255), readable by scripts and storable on coins, enabling stateful contracts.
- **WOTS** — Winternitz One-Time Signature scheme; quantum-safe, strictly single-use keys organized into Merkle trees of keys.
- **Mempool** — unconfirmed valid TxPoW units, ordered by burn.
- **Chain ID** — the network identifier in every TxPoW header; `0x01` on the Minima main chain.


---

## reference: transactions-utxo
<!-- (was references/transactions-utxo.md) -->

# Minima UTXO Model & Manual Transaction Construction

The canonical reference for Minima's coin (UTXO) model, token creation, and building
transactions by hand with the `txn*` command family. Everything here is proven on-chain.

---

## Transaction gotchas

Read these before touching funds. Each one is a real, fund-losing (or fund-locking) trap.

1. **The amounts triad.** For a **token** coin the real value is `tokenamount`; the
   `amount` field is the underlying coloured-Minima dust (~1e-37 at typical scales).
   `c.amount || c.tokenamount` is a **bug** — `amount` is always present so it always
   wins and you silently read dust. MINIMA (`0x00`) has **no** `tokenamount` — its
   `amount` IS the value. Correct read:
   `tid === "0x00" ? c.amount : (c.tokenamount != null ? c.tokenamount : c.amount)`.
2. **Sign BEFORE basics.** The 3-step post is `txnsign` → `txnbasics` → `txnpost`,
   in that order, never combined.
3. **`txndelete` on every error path.** Once a coin is added with `txninput`, it is
   locked to that transaction. If anything fails, `txndelete id:<id>` or the inputs
   stay locked and later spends mysteriously fail with "insufficient funds".
4. **`txnpost status:true` is mempool-accept only** — it is NOT on-chain success. An
   invalid transaction broadcasts fine and is silently rejected by every node. Verify
   by watching for the spent/unspent coin events or re-querying `coins`.
5. **State is a LIST**, `[{port, type, data}, ...]` — never a map. Always iterate to
   find a port. And state is **per-transaction**, not per-output: N distinct state
   stamps require N transactions.
6. **Never fund a script address you have not proven parses.** Before sending a single
   Minima to a derived covenant/script address, run the exact script bytes through
   `runscript`/`newscript` and require `parseok:true`. A coin at an unparseable-script
   address is **permanently unspendable** — no key, node rebuild, or seed resync can
   recover it.
7. **Never `txnpost auto:true` for script-address coins** — returns `status:true` but
   silently fails on-chain. Only safe for plain wallet sends.
8. **Grain:** every token amount stored on-chain is FLOORED to the token's grain
   (10^-decimals). When a covenant pins an output amount, quantize to the grain —
   round reserves UP, proceeds/change DOWN — or the spend is rejected.

---

## The UTXO / coin model

Minima keeps track of **coins**. Not users, not balances — coins. A coin is an unspent
transaction output: some amount of value locked by a script.

- A **transaction** consumes input coins and creates output coins. Per tokenid, inputs
  must equal outputs; any MINIMA difference is **burned** (burn buys mempool priority —
  burnt coins are destroyed, not paid to the block finder). Tokens may NOT be burned:
  per-tokenid input must equal output exactly, so never drop a positive token "change"
  amount as dust — pay it back to yourself.
- A coin's **address is the hash of the script** that locks it. The coin can be spent —
  always in full — if that script returns TRUE when the transaction is validated.
  Default wallet addresses use `RETURN SIGNEDBY(<publickey>)`; each node has 64 of them.
- Every address has two interchangeable formats: hex `0x...` (`address`) and
  `Mx...` (`miniaddress`). Both are accepted anywhere an address is expected.
- **`tokenid: 0x00` is MINIMA** itself (44 decimal places). Any other tokenid is a
  custom token — a **coloured coin**: a tiny fraction of Minima coloured to represent
  the token's supply.
- Each coin carries: `coinid` (unique id), `address`, `amount`, `tokenid`, an optional
  `state` list, a `storestate` flag, and an **MMR entry** — nodes only track coins
  relevant to them and present a Merkle Mountain Range proof of validity when spending.
- A transaction also carries a **state variable list (ports 0–255)** which can be
  stamped into its output coins and read back by scripts via `PREVSTATE` — the basic
  memory mechanic for counters, sequences, and covenants.

Practical consequences of "spend in full":

- Change is a second output back to one of your own addresses. Forget it and the
  difference is burned forever.
- Spending the same coin twice fails — the second transaction references a spent
  UTXO. After sending, wait for confirmation before spending "the same" funds again
  (the change coin is `unconfirmed` until mined).

---

## Coin JSON anatomy

A real coin object from `coins relevant:true`, for a **token** coin (addresses and
coinids replaced with placeholders; the tokenid is mxUSDT, a real mainnet token,
`decimals:8 scale:36`):

```json
{
  "coinid":      "0xFFEE01...",
  "amount":      "0.00000000000000000000000000000000000028497316",
  "address":     "0xFFAA02...",
  "miniaddress": "MxFF...",
  "tokenid":     "0x7D39745FBD29049BE29850B55A18BF550E4D442F930F86266E34193D89042A90",
  "token": {
    "name":        { "name": "mxUSDT", "..." : "..." },
    "coinid":      "0xFFCC03...",
    "total":       "...",
    "decimals":    8,
    "script":      "RETURN TRUE",
    "totalamount": "...",
    "scale":       36,
    "tokenid":     "0x7D39745FBD29049BE29850B55A18BF550E4D442F930F86266E34193D89042A90"
  },
  "tokenamount": "0.28497316",
  "storestate":  false,
  "state":       [],
  "spent":       false,
  "mmrentry":    "728",
  "created":     "203728"
}
```

Field by field:

| Field | Meaning |
|---|---|
| `coinid` | Globally unique id of this UTXO. What you pass to `txninput`. |
| `amount` | The raw underlying Minima. For `0x00` this IS the value. For a token it is the coloured dust (real value ÷ scale) — see the amounts triad below. |
| `address` / `miniaddress` | Hash of the locking script, in hex and Mx form. |
| `tokenid` | `0x00` = MINIMA; otherwise the token's globally unique id. |
| `token` | `null` for MINIMA; for tokens, the full token descriptor: `name` (string or arbitrary JSON metadata), creation `coinid`, `total` supply, `decimals`, token `script`, `totalamount` (Minima coloured), `scale`. |
| `tokenamount` | Token coins only — the decoded, human-scale value. Absent on `0x00` coins. |
| `state` | List of `{port, type, data}` stamped by the transaction that created this coin. Empty for plain sends. |
| `storestate` | Whether the state list was stored in the MMR with the coin — required for a later spend's script to read it via `PREVSTATE`. |
| `spent` | `false` = unspent (a live UTXO). `coins` can also show spent history rows. |
| `mmrentry` | The coin's leaf in the MMR — the basis of its inclusion proof. |
| `created` | Block number in which the coin was created. |

Note: coins returned inside `history` txpows carry `tokenamount` too — a history-only
consumer can decode real values without extra lookups.

---

## The amounts triad (fund-critical)

Three related facts, each independently capable of producing a fund bug:

### 1. `tokenamount` is the value; `amount` is coloured dust

For the mxUSDT coin above, the coin really holds **0.28497316 USDT**:

```
"amount":      "0.00000000000000000000000000000000000028497316"   ← raw, looks like dust, DO NOT USE
"tokenamount": "0.28497316"                                        ← the real value
```

MINIMA (`0x00`) has **no** `tokenamount` — `amount` is already real. So the correct
read, everywhere, is:

```js
const value = (tid === "0x00")
  ? c.amount
  : (c.tokenamount != null ? c.tokenamount : c.amount);
```

`c.amount || c.tokenamount` is a **BUG**: `amount` is always present, so it always
wins, and a token leg silently reads as ~1e-37 — which then gets grain-floored to 0
and disappears. Code forked from a native-MINIMA app that reads `coin.amount` for coin
selection, change, or claims will produce wrong sums, burned change, and broken locks
the moment it touches a token.

**Scope of the trap:** only the raw `coins`-JSON `amount` is the coloured-Minima.
`balance` (`confirmed`/`sendable`), `send amount:` and `txnoutput amount:` all use
TOKEN units directly. So you *read* value via `tokenamount`, but you *write* outputs
in ordinary token units.

### 2. Grain: token amounts are floored to 10^-decimals

Minima stores every token coin amount **floored to the token's grain**
(grain = 10^-decimals; an 8-decimal token → 1e-8). Internally amounts are truncated
downward, and a script's `GETOUTAMT` reads back the floored value. Write a token
output at 16 decimal places and the chain silently truncates it to 8 when the
covenant reads it.

Consequence for covenants: if a script pins an output with
`VERIFYOUT(... amount ...)` or enforces an invariant like a constant-product check,
and the client computed the output at finer precision than the grain, the on-chain
floored value is SMALLER than the client's — the check reads below threshold and the
spend is **rejected** (atomically; funds stay put, but the tx never lands, with a
cryptic node error).

The proven fix — quantize every token output to the token's exact `decimals`:

- **Pinned/recreated reserves → round UP** to the grain, so the stored (floored)
  value still clears the invariant.
- **Proceeds and change → round DOWN** to the grain, so outputs ≤ inputs and
  conservation holds.
- **Clamp token inputs down to the grain too** — an over-precise input makes the
  recreated reserve off-grain.

Read the grain from the coin's `token.decimals` field (present on every token coin
from `coins`/`tokens`).

### 3. MINIMA is exempt

`0x00` is full 44-decimal precision — any amount ≤ 44 dp is exact, no grain issue.
And MINIMA may be burned (that's the fee mechanism) while tokens may not: per-tokenid
inputs must equal outputs.

---

## Token creation

Tokens are coloured coins: `tokencreate` builds a special transaction that colours a
tiny fraction of Minima into a new, globally unique tokenid.

### Simple token

```bash
tokencreate name:MYTOKEN amount:1000 decimals:8
```

- Consumes a small fraction of Minima as input; change returns automatically as a
  second output.
- `decimals` defaults to 8. `decimals:0` makes the token indivisible — sending 1.5
  fails; `amount:1 decimals:0` is an NFT (1 unit, non-divisible).

### The amount / scale relationship

The coloured Minima × the **scale** = the token amount. Colour 1e-33 MINIMA at
scale 36 and you have 1000.00000000 tokens with 8 decimals. This is exactly why a
token coin's raw `amount` looks like dust: it is the coloured Minima, not the token
value. mxUSDT (`decimals:8 scale:36`) is a live mainnet example of these numbers.

### Token with JSON metadata

`name` is just a string, so it can carry arbitrary JSON — stored on-chain, immutable:

```bash
tokencreate amount:10 name:{"name":"newcoin","link":"https://example.com","description":"A very cool token","icon":"https://example.com/icon.png"}
```

Common (application-defined, not protocol-enforced) fields: `name`, `description`,
`icon`/`url`, `color`, `webvalidate` (a URL hosting proof of authenticity that
wallets can check).

### Token scripts

A token can carry its own script (default `RETURN TRUE`). On every spend of every
coin of the token, **both** the coin's address script **and** the token script must
return TRUE:

```bash
tokencreate name:charitycoin amount:1000 script:"ASSERT VERIFYOUT(@TOTOUT-1 0xFFAA02... 1 0x00 TRUE)"
```

That token now demands that every transaction using it sends 1 Minima to
`0xFFAA02...` as its last output. A plain `send` of such a token fails — you must
build a manual transaction satisfying the script (see `sendable:0`, below).

### Reading the `tokencreate` response

```
inputs[0].tokenid   = "0x00"     ← Minima used as input
outputs[0].tokenid  = "0xFF..."  ← new token output (scale representation)
outputs[0].token.tokenid         ← the REAL tokenid — use this for send/balance
outputs[0].token.name            ← your metadata (string or JSON)
outputs[0].token.total           ← total supply
outputs[0].token.decimals        ← decimal places
outputs[0].token.script          ← default "RETURN TRUE"
outputs[0].tokenamount           ← human-readable supply
outputs[1].tokenid  = "0x00"     ← change back to sender
```

The token shows in `balance` as `unconfirmed` until mined.

### The tokenid is a hash — never re-serialize the descriptor

The tokenid is a hash over the token's creation descriptor (creation coin, name
bytes, total, decimals, script). The name JSON is hashed **as bytes**, so
re-serializing it with a different key order, whitespace, or number formatting yields
a **different hash — a wrong tokenid**. When you need to move token data between
nodes or reconstruct it, take the **byte-exact** token object from
`coinexport` / `tokens action:export` and import it verbatim
(`tokens action:import`). Never rebuild the JSON by hand.

---

## State: the transaction's variable list

- A transaction carries **0–255 state variables** (`port` 0–255). Output coins can
  store them (`storestate:true`), and a later spend's script reads them via
  `PREVSTATE(port)` — coins remember the transaction they came from.
- **State is a LIST**: `[{"port":0,"type":1,"data":"..."}, ...]`. It is not keyed by
  port in JSON — always iterate to find a specific port.
- **State is per-TRANSACTION, not per-output.** Every output coin of one transaction
  that stores state stores the SAME state list. To stamp N coins with N *distinct*
  states you need N transactions (one coin each; they can run in parallel).
- Set unused ports you rely on to `0` explicitly rather than leaving them absent —
  scripts read absent state as a failure case, and explicit zeros keep re-derivation
  deterministic.
- **TxPoW hard cap: 64 KB.** Everything — inputs, outputs, state, signatures, MMR
  proofs, token descriptors — must fit. With token-carrying outputs (each output
  repeats the full token descriptor) the practical ceiling is about **3
  token-carrying outputs + 1 signature per transaction**. If a coin embeds data in
  state (e.g. a base64 image), remember a transfer carries it twice: once in the
  input coin's proof and once in the recreated output state — keep such payloads
  ≤ 8 KB.
- `storestate:true` for outputs whose state must be readable at the next spend
  (phase-transition coins staying at a script address); `storestate:false` for plain
  payout outputs — smaller, cheaper, nothing to leak.

---

## The canonical manual build sequence

For anything beyond a plain wallet send — precise coin control, script addresses,
state stamping — build the transaction by hand. The terminal commands below are the
canonical interface; `MDS.cmd("txncreate id:...")` in a MiniDapp and native IPC
integrations wrap the **same** commands with the same semantics, so this sequence is
the reference for all three.

```bash
# 1. Create the empty transaction shell
txncreate id:mytxn

# 2. Add input coin(s) — get coinids from `coins relevant:true`
txninput id:mytxn coinid:0xFFEE01...
# (shortcut: txninput ... scriptmmr:true adds the MMR proof + script here —
#  but then you MUST NOT run txnbasics later; see rules below)

# 3. Add outputs — recipient first, then change back to yourself
txnoutput id:mytxn amount:10 address:0xFFAA02...
txnoutput id:mytxn amount:<input_total - 10> address:<your_change_address> \
          storestate:false
# For a token output add tokenid:<tokenid> (amount in TOKEN units).
# For an output that must retain state at a script address: storestate:true

# 4. Set transaction state variables (if the script needs them)
txnstate id:mytxn port:0 value:0xFFDD04...

# 5. SIGN — before basics, always
txnsign id:mytxn publickey:auto

# 6. Add MMR proofs + scripts
txnbasics id:mytxn

# 7. (optional) inspect
txncheck id:mytxn      # burn, per-token in/out, valid.* booleans
txnlist  id:mytxn      # full transaction incl. witness

# 8. Post
txnpost id:mytxn                 # optionally burn:<amount> for mempool priority

# ON ANY ERROR at any step:
txndelete id:mytxn               # or the input coins stay locked
```

### Critical rules (proven on-chain)

- **3-step post: `txnsign` → `txnbasics` → `txnpost`. Sign BEFORE basics. Never
  combine.**
- **Never `txnpost auto:true` for script-address coins** — it returns `status:true`
  but silently fails on-chain. Only for simple wallet sends.
- **Never combine `txninput ... scriptmmr:true` with `txnbasics`** — duplicate MMR
  proof error. Pick one mechanism.
- **`txnpost` returns `status:true` even for invalid transactions.** Status means
  "broadcast", not "accepted into a block". Confirm success by watching the spent /
  new-unspent coin events or re-querying `coins`.
- **`txncheck` cannot evaluate `@BLKNUM` / `@COINAGE`** — scripts using those globals
  always show `scripts:false` in txncheck. Don't gate posting on it for such scripts.
- **Always `txndelete` on error paths** after `txncreate` — inputs stay locked
  otherwise.
- **Inputs must equal outputs per tokenid** — MINIMA shortfall is burned; token
  shortfall is invalid. `txncheck`'s `burn` field shows exactly what you're about to
  destroy; a forgotten change output shows up here as a huge burn.
- **`txnsign publickey:auto` signs with every wallet key matching an input coin's
  address.** Convenient, but a trap with covenants — see below.

### `txncheck` — read it correctly

```
"coins":[{ "tokenid":"0x00", "input":"...", "output":"...", "difference":"..." }],
"scripts": 1,                 ← COUNT of distinct input scripts — NOT a verdict
"burn":"0",
"validamounts":true,
"valid":{
  "basic":true, "signatures":true, "mmrproofs":true,
  "scripts":true              ← THE script pass/fail verdict is HERE
}
```

The top-level `scripts` field is a **count** of distinct input scripts, not a
pass/fail. The real covenant verdict is the boolean `valid.scripts`. Gating on
`scripts == 1` both blocks legitimate multi-script transactions (covenant coin +
wallet funding = 2 scripts) and falsely passes doomed single-script ones (two coins
at the same failing address = 1 script). If your JSON library coerces types
defensively (integer vs boolean), use a truthy helper on `valid.scripts` —
`optBoolean`-style APIs return their default for an integer and silently lie.

`valid.basic` only becomes true after signatures + MMR proofs are added. Before
`txnbasics` you'll also see a `Wrong Number of MMR Proofs` warning — expected.

### Covenant traps when funding and signing

- **The owner-key auto-sign trap.** If a covenant has an owner branch
  (`IF SIGNEDBY($OWNERKEY) THEN ... ENDIF`) and you fund an *anyone-can-spend*
  transaction from a coin at the owner's own address, `txnsign publickey:auto` adds
  the owner key's signature — `SIGNEDBY($OWNERKEY)` becomes TRUE and the covenant
  runs the OWNER branch instead of the intended one, typically rejecting the spend.
  This bites an operator interacting with their own contract from the same node and
  is invisible when a third party tests. Fixes: exclude the covenant owner's
  addresses from funding-coin selection, and send change to a non-owner address, so
  `auto` never has a reason to add that key.
- **Canonical literals.** Any value baked into a covenant script AND stored elsewhere
  (state, an announce, a registry) for re-derivation must be in the same canonical
  form the node uses — the node normalizes numbers (e.g. `716.2041200000000` →
  `716.20412`). One differing character → different script hash → different address
  → your coin is at an address nobody re-derives. Strip trailing zeros and use plain
  (non-exponent) decimal strings.
- **Prove `parseok` BEFORE funding.** The prevention pattern that makes stranding
  structurally impossible: before any funds move to a derived script address, submit
  the exact script bytes via `runscript`/`newscript` on the node and require
  `parseok:true` (ideally also confirm it re-derives the exact funding address).
  A script that fails to parse — e.g. because a JSON encoder escaped `/` as `\/` on
  the way in — still yields *an* address, and coins sent there are **unrecoverable**:
  the parse failure fires at load time, so no branch (not even an owner escape hatch)
  can ever execute. When a covenant spend fails with `valid.scripts:false`, the FIRST
  diagnostic is `scripts address:<X>` → run the exact stored bytes → check `parseok`,
  before chasing keys or node state.

---

## The splitter pattern and consolidation

### Splitting: one input → N outputs

When you need many small UTXOs (per-action game spends, parallel workers, avoiding
UTXO-locked waits), split one coin into N in a single transaction.

Easiest — the built-in:

```bash
send address:<your_own_address> amount:<total> split:10
# one transaction, 10 equal outputs of total/10 each
```

Manual, for exact control:

```bash
txncreate id:splitter
txninput  id:splitter coinid:<big_coin>
txnoutput id:splitter amount:0.1 address:<recipient>
txnoutput id:splitter amount:0.1 address:<recipient>
# ... repeat N times ...
txnoutput id:splitter amount:<coinAmount - N*0.1> address:<origin_address>
txnsign   id:splitter publickey:auto
txnbasics id:splitter
txnpost   id:splitter
```

Afterwards `coins relevant:true` shows N coins of 0.1. Remember the 64 KB TxPoW cap
bounds N — especially for token outputs, where ~3 per transaction is the practical
ceiling; split adaptively over several transactions for large N.

### Consolidation: N inputs → one output

The reverse — sweep dust into one coin. Add multiple `txninput`s and a single output
back to yourself for the total (every input's script must be satisfiable, one
`txnsign publickey:auto` covers all your own default addresses). Recent nodes also
provide a `consolidate` command that does this sweep for you (see
`help command:consolidate` on your node version). Consolidation matters because coin
selection needs a single coin ≥ the send amount, or multiple inputs per transaction —
and many tiny coins bloat proofs.

---

## Reading coins and scripts

### Listing UTXOs

```bash
coins relevant:true                # coins the node tracks for you
coins relevant:true simple:true    # only coins at your own SIGNEDBY addresses
coins tokenid:0x00                 # filter by token
coins coinid:0xFFEE01...           # one specific coin
```

Two scope caveats:

- `coins relevant:true` shows coins the wallet currently **tracks** — your default
  addresses plus any script/covenant coins you imported or track. It is NOT a full
  view of everything you own: a wallet-controlled script address with `track:false`
  is invisible here. Fix with `cointrack enable:true coinid:<coinid>` per coin.
- Conversely, "relevant" can include coins that aren't yours at all (imported
  watch-coins, tracked covenants). To compute *your* net worth or history, filter to
  your own signature addresses: `scripts` marks them `simple:true` (your own
  `RETURN SIGNEDBY(key)` address; `default:true` for the 64 per-key defaults) versus
  `simple:false` (covenant / imported watch address). Build the set of `simple:true`
  addresses and sum only coins at those.

### Inspecting an address's script

```bash
scripts address:0xFFAA02...
# → { "script":"RETURN SIGNEDBY(0xFFBB05...)", "address":"0x...",
#     "miniaddress":"Mx...", "simple":true, "default":true,
#     "publickey":"0xFFBB05...", "track":true }
```

This tells you exactly what must be TRUE to spend coins at that address, and (for
simple addresses) which public key `txnsign` must use.

### The address-derivation trick

Derive the address for any pubkey — or any script — **without touching the wallet**:

```bash
newscript clean:true trackall:false script:"RETURN SIGNEDBY(0xFFBB05...)"
# → { "address":"0x...", "miniaddress":"Mx...", "script":"..." }
```

`clean:true trackall:false` computes the hash without registering the script or
creating keys — no wallet mutation. Uses: derive all 64 default addresses from
`keys action:list` output in seconds; pre-compute a covenant's address (and check
`parseok`!) before funding it; verify a counterparty's claimed address matches their
claimed script.

---

## Script-token coins and `sendable:0`

`balance` reports three numbers per token: `confirmed`, `unconfirmed` (mined but not
yet spendable-deep), and `sendable`. Coins locked by a non-trivial **token script**
(anything beyond `RETURN TRUE`) show **`sendable:0`** — the plain `send` command
refuses them, because a naive send would violate the token script and be rejected
on-chain anyway. This is a feature: it protects users from building invalid
transfers.

Moving such coins requires **manual transaction construction** that satisfies the
token script — recreating required outputs, preserving state
(`storestate:true` + identical state values where the script demands `keepstate`),
and posting via the canonical sequence above. The same applies to coins at covenant
addresses: the wallet cannot auto-spend what it cannot auto-satisfy.

A worked consequence (state-carrying NFTs): a token script of the form

```
IF SIGNEDBY(0xFFBB05...) THEN RETURN TRUE ENDIF
RETURN VERIFYOUT(@INPUT GETOUTADDR(@INPUT) @AMOUNT @TOKENID TRUE)
```

lets the creator move coins freely, but forces everyone else to recreate each spent
coin at the same output index with the same amount, tokenid and identical state — a
spend that strips the state posts with `status:true` and is then **rejected on-chain**
(the coin never moves). Which is gotcha #4 from the top of this file, observed live:
never trust `txnpost` status alone.

---

## Common failures, quickly diagnosed

| Symptom | Cause / fix |
|---|---|
| "Insufficient funds" right after a send | UTXO locked/unconfirmed — wait for the previous tx to confirm; or a stale `txncreate` locked the coin — `txndelete`. |
| Second send fails immediately | Both spends target the same UTXO; only one can win. Split coins ahead of time. |
| Huge `burn` in txncheck | Missing change output — add it before posting. |
| "Wrong Number of MMR Proofs" | Expected before `txnbasics`; if it persists at post, you skipped `txnbasics` (or doubled it with `scriptmmr:true`). |
| Posted OK, coin never moved | `txnpost` status ≠ success. Check `valid.scripts` via `txncheck`; then `scripts address:` + `runscript` → `parseok`. |
| Token output "disappears", value ~1e-37 | You read `amount` instead of `tokenamount`, and/or wrote a sub-grain amount that floored to 0. |
| Covenant rejects a mathematically-correct spend | Grain flooring — quantize outputs to `token.decimals` (reserves UP, proceeds DOWN); or the owner-key auto-sign trap flipped the branch. |
| Token creation fails | Needs a small amount of spendable Minima as input. |
