Paraphrased notes from the Midnight team on Discord (July 2026). These are moving targets — always cross-check the official support matrix and open a Service Desk ticket with your pinned versions before assuming a workaround still applies.
recommended workaround · local devnet
For active development, Midnight DevRel currently advises running against a local standalone stack (NetworkId.Undeployed) rather than fighting Preprod DUST sync. The node mints unlimited tDUST to the genesis wallet and every version is guaranteed to match the SDK bundle you're building against. Start with the Undeployed quick-start → then run bun scripts/midnight-standalone.mjs up. Verify in your browser at /undeployed-preflight, and see the Choreo Ledger (Local) demo.
Docker + Git setup for macOS, Windows, and Linux. The Windows tab keeps the real blockers hit on a Windows 11 HP laptop before the Midnight stack would even start — BIOS virtualization, WSL update, Node.js + PowerShell execution policy.
External references · Midnight-skills
Browsable skill registry from the community (Kali-Decoder / Tusharpamnani). Each page is a full scaffold: contract, wallet wiring, indexer patterns, troubleshooting tables.
Indexer stuck at block 0 with no error (hosted stack)
Symptom
Every read returns nothing and the UI reports block 0 forever, so it looks like your anchor never landed — but no request errors and no log line complains.
Cause
The indexer does not fail loudly when it cannot reach the node RPC: it serves an EMPTY chain. On Fly this normally means the indexer is pointed at `<app>.internal:9944` (IPv6-only 6PN) or `<app>.flycast:9944` (no private IP allocated) while the node binds IPv4.
Workaround
Publish the node's 9944 as a pure tls service on the host edge and point every consumer at wss://<app>.fly.dev:9944. Then make node-reachable-from-indexer its own explicit preflight step — never trust an indexer answer before that check passes.
# measured facts, not assumptions
# on the node machine
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9944/health
# on the indexer machine
curl -s -o /dev/null -w '%{http_code}\n' https://<app>.fly.dev:9944
Submit dies after minutes of proving on a hosted node
Symptom
Proving completes locally, then the submit fails or hangs — always after several minutes, never immediately. Looks like a Midnight node bug.
Cause
The host's `http` handler terminates a long-lived WebSocket mid-request. A proof submission holds that socket open for minutes, so it is the first thing to get cut.
Workaround
Serve 9944 with a tls handler only — no http handler and no http_options on that service. Keep .internal / private-network names for server-to-server calls that stay inside the stack, and probe reachability from the consuming machine instead of assuming it.
`npm error code ETARGET` — no matching version for `@midnight-ntwrk/ledger-v9`
Symptom
Installing the Compact/Midnight toolchain fails with `notarget No matching version found for @midnight-ntwrk/ledger-v9@^0.1.0-alpha.1`, and every retry fails identically.
Cause
`@midnight-ntwrk/compact-js@2.5.3` depends on a transitive alpha (`ledger-v9@^0.1.0-alpha.1`) that was never published to npm. A half-finished attempt also leaves a stale `package-lock.json` + `node_modules`, so the bad resolution is cached and reproduces on every retry.
Workaround
Pin @midnight-ntwrk/compact-js@2.5.1 — it resolves cleanly and has no ledger-v9 edge. Pin every Midnight package to an exact version: never @latest, never a caret. Before retrying, delete the stale lockfile and node_modules.
rm -rf node_modules package-lock.json
npm i @midnight-ntwrk/compact-js@2.5.1 --save-exact
On a persistent build/runner machine, expose a “Clear toolchain” action that resets the install without destroying the volume — the volume holds the chain data and the LevelDB private state.
Toolchain install stalls with no error, then exits status=1
Symptom
The install step stops part-way through, the log tail is empty, the UI retries forever, and the job eventually reports a non-zero exit. Reads exactly like a hang.
Cause
The machine was OOM-killed. The Midnight SDK install is memory-hungry and a 1 GB machine dies silently — an OOM-killed process leaves no error line, which is why it looks like a stall rather than a crash.
Workaround
Give the build/runner machine 4 vCPU / 4 GB RAM and a 10 GB volume. Install in small sequential groups with an npm cache on the volume instead of one giant npm i. Emit a heartbeat every ~30 s so “slow” is distinguishable from “dead”, and surface the host’s machine events (OOM flag, exit code) in the UI — not just the log tail.
For any detached job, print STEP_<name> markers and JOB_FAILED status=<n> during: <phase>, drive a monotonic step timeline off them, and persist the failed job’s log auto-expanded with a copy button. A toast loses the only diagnostic you had.
How do I turn my seed into a bech32 preprod/preview address?
Symptom
You've generated a master seed via wallet-sdk (e.g. `7aaa436f…`) but the preprod/preview faucet wants a bech32 unshielded address (`mn_addr_preprod1…`). Wiring up `WalletSeeds` + `createKeystore` + the address encoders yourself is a full afternoon.
Cause
There's no need to derive it by hand. The community `midnight-wallet-cli` (npm) wraps the same derivation Lace uses and prints the bech32 address directly. Recommended by Midnight dev-rel (norm) in #dev-chat, 27 July 2026.
Workaround
Two commands — same seed as Lace produces, same address the faucet accepts:
npm i -g midnight-wallet-cli
# --seed is the 64-char hex master seed (32 bytes), NOT the mnemonic
mn address --seed <64-hex-master-seed> --network preprod
# → prints your unshielded mn_addr_preprod1… (paste into the faucet)
# Verify funds landed
mn balance <mn_addr_preprod1…> --network preprod
The seed MUST be the 64-hex master seed. If you have a BIP-39 mnemonic, derive the master seed first (or use our scripts/derive-unshielded-address.mjs as an offline fallback).
The shielded address (mn_shield-addr_…) is a different identity — the faucet only accepts the unshielded one.
Never paste your seed into chat, screenshots, or issue trackers.
7.5h+ without finishing, sometimes OOM on smaller machines. Preview first sync in ~44 min is normal; Preprod is heavier, but hours-without-progress + OOM = struggling, not just slow.
Cause
No documented snapshot / fast-sync path for headless WalletFacade today. Memory scales with chain history during initial sync.
Workaround
Use Preview for throughput / latency benchmarking.
Align every dep to the Preprod row on the support matrix — version skew often surfaces as sync decode failures.
Confirm indexer + node are healthy and at chain tip before sync.
NODE_OPTIONS="--max-old-space-size=8192" helps OOM but won't fix decode loops.
Wallet sync stalls or blows up memory during initial sync
Symptom
Headless WalletFacade / SDK script hangs for hours on a fresh wallet, or the Node process OOMs while walking chain history. Every run starts from scratch and re-scans everything.
Cause
Midnight DevRel guidance (Jay Albert, Midnight Network — Dev Hangout Prep, July 2026): there is no snapshot / fast-sync path yet, and the default transaction-history storage keeps every event in memory even when your script never reads it.
Workaround
Two complementary techniques Midnight recommends today:
Sync from where you last left off. Serialize the wallet state to disk after the first successful sync, then restore it on the next run so only the delta syncs.
// After first sync
const serialized = await wallet.serializeState();
await fs.writeFile("wallet-state.bin", serialized);
// Next run — restore before wallet.start()
const restored = await fs.readFile("wallet-state.bin");
const wallet = await WalletBuilder.restore(
indexerUrl, indexerWsUrl, proofServerUrl, nodeUrl,
restored, networkId,
);
wallet.start(); // only the delta re-syncs
Don't store history you won't read. Pass NoOpTransactionHistoryStorage in your wallet config to cut memory during sync. Only safe if your script never queries transaction history.
Lace shows DUST but SDK reports 0 / unshielded never syncs
Symptom
Lace displays a healthy DUST balance while `DustWallet.balance() = 0` and/or the unshielded leg never finishes syncing from the SDK. If the SDK dust leg doesn't reach tip, deploys fail with "no fee DUST" even though Lace looks funded.
Cause
Confirmed known Preprod pattern (Midnight team, Discord, 30/06/2026). Tracked on their side; wallet-SDK fixes in progress.
Workaround
Pin every package to the current Preprod row on the support matrix.
Repeated `Could not deserialize Ledger Event` during shielded / DUST sync. Wallet SDK ↔ ledger/indexer event-format mismatch on Preprod.
Cause
Pinned ledger-v8 / wallet-sdk-dust-wallet combo can't decode current Preprod events (e.g. midnight-js 4.0.4 vs matrix 4.1.1, proof-server 8.0.3 vs matrix 8.1.0). If the matrix now shows a newer proof-server / ledger-v8 row, re-pin everything to that single row.
Workaround
Re-pin wallet + ledger + midnight-js + proof-server from the Preprod matrix in one pass.
Resync a fresh wallet dir.
Wallet config workaround that helps some setups: batchUpdates: { size: 5000, timeout: 1, spacing: 4 }
Parallelize proving, serialize balance+submit per wallet. Add app-level timeouts around submit.
1010 Custom error: 170 = InvalidDustSpendProof
Symptom
Node rejects submit with `1010 Custom error: 170` — DUST fee proof invalid or stale.
Cause
Common Preprod causes: wallet/indexer stale (DUST Merkle roots pruned while balance still looks fine), indexer lag behind node tip, or version skew between ledger / proof-server / wallet-sdk.
Workaround
Confirm WS vs HTTP: if you get `1010 Custom error: N`, WS is fine.
Fresh wallet resync (or resync right before submit).
Compare indexer block height vs RPC tip.
Pin the full stack to the current Preprod matrix.
Retry after sync completes — don't submit from a long-idle wallet.
/check 400 bad input on callTx (deploy works, callTx fails)
Symptom
Proof-server `/check` returns `400 bad input` in ~3ms with a ~461-byte body on a complete preimage. Deploy `/prove` works; callTx hits `/check` first and fails.
Cause
Compact 0.31.0 changed the ZKIR representation for Uint downcasts, byte-vector ↔ Field/Uint conversions, and relational comparisons. Local proof-server 8.0.3 / 8.1.0 `/check` parser doesn't accept the reworked ZKIR on the wrapped-ir path — client/server serialization gap, per-circuit.
Workaround
cat managed/<circuit>/compiler/contract-info.json — if it's 0.30.x / 0.22.0, confirms the 0.31 ZKIR rework is the trigger.
Point httpClientProofProvider at the public prover https://lace-proof-pub.preprod.midnight.network. If register_asset /check passes there, your local image is just behind the deployed prover. (Lace's wallet-delegated proving uses this same public backend, which is why Lace succeeds where local Docker rejects.)
Bisect the reworked op: drop decimals Uint<8>, drop the secret-key → owner-id conversion, recompile, retry /check to pin the exact op.
Fix is upstream in a later Compact release — see the toolchain 0.31.0 release notes.
/check 400 — engineering-confirmed 0.31 ZKIR serialization gap
Symptom
`httpClientProofProvider`'s `createCheckPayload(preimage, keyMaterial.ir)` is rejected by `/check` on the 8.0.3 public prover, while Lace's wallet-delegated proving of the exact same callTx succeeds against that same prover.
Cause
Midnight team confirmed (Discord, 03/07/2026): this is a client/server `/check` serialization gap for 0.31 ZKIR, not user error or version drift. Needs the engineering team.
/check bad input. Include: deploy / create_market work, register_asset fails, Uint<64> widen didn't fix (→ second reworked op), check() isn't skippable (stub → WASM unreachable), latest stable provider is 4.1.1. Ask which prover parses 0.31 ZKIR on /check, ETA for the ZKIR-format fix from the 0.31.0 notes, and the precise list of reworked ops to avoid on 8.0.3.
Matrix conflict. Align ledger-v8, wallet-sdk-dust-wallet, and proof-server to the same support-matrix row. As of the current matrix, that means ledger-v8 8.1.0, proof-server 8.1.0, and Wallet SDK 1.2.0. Ask for the coherent wallet-sdk set if you see Transaction.addIntent mismatches.
Also note in the ticket that lace-proof-pub.preprod.midnight.network from COMPATIBILITY.md doesn't resolve publicly. See the local-repro workaround section for the Lace-vs-httpClientProofProvider bisect that produced this evidence.
A circuit that touches unshielded value (e.g. `receiveUnshielded` in a `deposit`) proves fine, then the node rejects it with `1010 Invalid Transaction: Custom error: 192`. Shielded-only calls (`deploy`, a pure shielded circuit) succeed from the same wallet, so it reads like a contract bug.
Cause
Any unshielded input pulls a NIGHT UTXO into the transaction, and UTXO inputs carry Schnorr signatures. `balanceUnboundTransaction` does NOT add them — the tx arrives with one input and zero signatures. Measured on Preview by the m402 team (Hack Buenos Aires open-track winner).
One wallet cannot submit two transactions concurrently
Symptom
Two calls fired at once from a single wallet: the first lands, the second is rejected with `1010 Custom error: 170` (InvalidDustSpendProof) — or the test just hangs forever.
Cause
Both transactions build a DUST spend proof against the same wallet DUST state, and the node throws the second out before contract execution. A rejected submission also never settles its promise, so it waits for a confirmation that never arrives.
Workaround
Serialize calls per wallet — an agent loop must queue, not fan out.
Wrap every submit in an explicit timeout so a rejection surfaces as a failure, not a hang.
Contract-level write contention cannot be measured from a single-wallet harness: a low "landed" count measures the wallet. Use a second funded wallet.
LEVEL_LOCKED / "No private state found" / "Contract address not set"
Symptom
Local failures that look exactly like on-chain contention: `Error: Database failed to open … lock midnight-level-db/LOCK: already held by process { code: 'LEVEL_LOCKED' }`, or `submitCallTx` failing with "No private state found at private state ID …" / "Contract address not set", or `first argument 'location' must be a non-empty string`.
Cause
`levelPrivateStateProvider` takes two similarly-named options and only one is a directory: `midnightDbName` is the LevelDB directory on disk (default `midnight-level-db`), `privateStateStoreName` is an object store inside it (default `private-states`). LevelDB is single-writer.
Workaround
Concurrent callers need different midnightDbName values. Different privateStateStoreName values change nothing — they still open the same directory.
A fresh store is empty: call provider.setContractAddress(contractAddress) first, then await provider.set(id, emptyPrivateState()).
Passing midnightDbName: undefined is not the same as omitting it — the provider spreads your config over its defaults, so an explicit undefined wipes the default. Spread the key in only when it is set.
Rule out all four local causes before reading any result as a property of the chain.
A sub-wallet's sync dies while the command keeps waiting
Symptom
A transient indexer WebSocket error kills one sub-wallet's sync fibre (observed on Preview as `Wallet.Sync: [object ErrorEvent]` from `wallet-sdk-dust-wallet`, seconds after start). The facade keeps emitting state that never becomes strictly complete, so the command looks slow rather than failing.
Cause
Nothing ends the wait except your deadline — so the deadline's value is the whole design. A 60-minute budget is indistinguishable from a hang for an operator.
Workaround
Use a 10-minute sync deadline, overridable via an env var (m402 uses MIDNIGHT_SYNC_TIMEOUT_MS).
Implement it as an explicit Rx.race against a timer, not Rx.timeout({ each }) placed after a filter — emissions that fail the filter never reach the timeout, so its semantics silently depend on pipe position.
Restart the command after a deadline hit; a dead fibre never recovers.
ERR_PACKAGE_PATH_NOT_EXPORTED inside a tsx stack trace
Symptom
The Midnight SDK fails to resolve its ESM exports and the error surfaces deep inside a `tsx` stack trace, reading like a dependency problem.
Cause
It is a runtime-version problem. The SDK's exports fail to resolve on Node 23 and Node 26. Midnight documents 22 as the floor and pins 24 in `example-hello-world`.
Workaround
Node 22 or 24 only — 22.12.0 and 24.19.0 are both verified against Preview. Check node -v before debugging anything else, and pin it in .nvmrc / package.json engines.
Every command re-syncs from genesis (687s cold vs 54s warm)
Symptom
A single Preview deposit takes ~687s wall clock with ~644s of CPU, of which ~710s is wallet sync — proving is 27s and confirmation 1.4s. The next identical run costs exactly the same.
Cause
`FluentWalletBuilder` can only build from a seed, and a from-seed wallet starts at `appliedIndex === 0`. Both `shielded/src/v1/Sync.ts` and `dust-wallet/src/v1/Sync.ts` compute `resumeFrom = appliedIndex - 1n` and open the subscription with no cursor when that is negative — so it streams every indexer event from the beginning, every invocation.
Workaround
Persist the sub-wallet states and restore them on the next build. Measured on Preview: 687.5s cold → 53.8s warm (12.8x), CPU 644s → 12s — that is trial-decryption replay disappearing.
Use serializeState() / restore() on all three sub-wallets.
All three sub-wallets and the facade must share one txHistoryStorage — otherwise shielded and unshielded writes go to a storage the facade never reads.
Never cache before sync completes. A mid-sync position restores cleanly and resumes from somewhere the wallet never applied.
Make restore best-effort: any missing/unreadable file falls back to the from-seed build.
The cache holds the wallet's coins — key it by a hash of master seed + network id and write it 0600. It is a wallet secret.
Bonus: @midnight-ntwrk/testkit-js costs ~5.2s just to import. Load it lazily so --help, --version and dry-runs don't pay for a wallet builder they never call.
Source: Midnight team (Nasihudeen Jimoh) responses on the Midnight Discord, June–July 2026, paraphrased for reference. When escalating, open a Service Desk ticket with pinned versions, indexer URLs, and whether you're seeing unshielded/DUST sync errors or hangs.