How to troubleshoot common errors

Who this page is for: you ran a pyrxd command (or called the library directly) and got an error, or a symptom that looks like an error but isn’t one (a balance that’s silently wrong). This page maps concrete error text and symptoms to a cause and a one-line fix. It is not a full API or exception reference — for that, read the cited source files directly.

Every entry below quotes a real string found in the pyrxd source at the time of writing, with a file and line citation. Where a plausible-sounding error doesn’t actually exist in the code, this page says so rather than inventing one — grep the source yourself if a string here looks stale; line numbers drift as the codebase changes.


How pyrxd reports errors

Every CLI-level failure is a CliError (a click.ClickException subclass) with up to three parts — a one-line message, a cause (what went wrong, sensitive values redacted), and a fix (a concrete next step). format_message renders them as:

error: <message>
  cause: <cause>
  fix: <fix>

— src/pyrxd/cli/errors.py

The exit code tells you which family the error is from, before you even read the text:

Code

Meaning

0

success

1

user error (bad input, missing file, insufficient funds)

2

network error (couldn’t reach ElectrumX, broadcast rejected)

3

wallet decryption failed

4

unexpected error (a bug — should not happen; file an issue)

Pass --debug to any command to also print the underlying Python traceback (no captured local variables — just function names, line numbers, and source lines, the same exposure as any uncaught exception).

Library code (used directly, outside the CLI) raises typed exceptions from pyrxd.security.errors instead — ValidationError, NetworkError, KeyMaterialError, and so on. The CLI wraps those into the three-line CliError block above; the entries below give you both forms where they differ.


1. Zero balance after restoring a wallet (no error at all)

Symptom: you restore a mnemonic, pyrxd balance shows 0, but a block explorer shows the address holding funds — except it’s not the address your wallet derived, because the funds landed on a different BIP44 path.

Cause: the Radiant ecosystem never agreed on a BIP44 coin_type, so the same seed produces different addresses depending on which wallet derived it:

coin_type

Used by

0

Photonic ≤ v2.x (legacy), Electron-Radiant, Chainbow

512

SLIP-0044 spec, Tangem, Photonic ≥ v3.0.0 (pyrxd’s default)

236

pre-#14 pyrxd (BSV’s coin type)

See the module docstring in src/pyrxd/hd/discovery.py for the full explanation and DEFAULT_COIN_TYPES = (0, 512, 236).

Fix: pyrxd wallet recover --scan — scans every coin-type/account combination and reports which path actually holds funds. Full walkthrough, including wallet sweep to move the funds once found: Recover funds across wallet paths.


2. Wallet won’t decrypt after upgrading pyrxd (non-ASCII passphrase)

Symptom: a wallet file created before pyrxd 0.12.0 with a non-ASCII BIP39 passphrase (accents, umlauts, CJK — anything beyond plain ASCII) now fails to decrypt, even though you’re certain the mnemonic and passphrase are correct.

What you actually see at the CLI: every wallet-opening command (balance, utxos, any glyph or wallet subcommand that decrypts the wallet file) funnels a decrypt failure through the same generic block, exit code 3:

error: Could not decrypt wallet file
  cause: wrong mnemonic, wrong passphrase, or wallet file is corrupt
  fix: check the mnemonic and passphrase exactly, then try again

— WalletDecryptError’s default message (src/pyrxd/cli/errors.py:179-184). Verified: the CLI’s wallet loader deliberately discards the library’s more specific decrypt-failure message (which, called directly, includes an NFKD hint pointing at this exact cause) and always re-raises this generic text — src/pyrxd/cli/prompts.py:166-174 and src/pyrxd/cli/wallet_cmds.py:193-201 both do this on purpose (never echo the user’s input back). So at the CLI, this generic message is the only symptom you get — nothing on-screen mentions NFKD or normalization.

Cause: BIP39 requires the mnemonic and passphrase to be NFKD-normalized before they enter PBKDF2. pyrxd versions before 0.12.0 skipped that step, so a non-ASCII passphrase could derive a different seed than the spec requires — and the wallet file’s AES-GCM key is derived from that seed, so a file saved under the old seed only decrypts by reproducing it.

Fix: the escape hatch is normalize=False, reachable from HdWallet.load, load_or_create, from_mnemonic, the bip32_*/bip44_* derivation helpers, and pyrxd.hd.discover — but not exposed as a CLI flag, so this recovery is library-only. Full recipe (how to tell if you’re affected, and how to sweep funds off the legacy seed permanently) is the “Non-ASCII passphrases” section of Recover funds across wallet paths.


3. “no single UTXO is large enough to fund the mint / deploy”

Symptom, verbatim (one of three, depending on command):

error: no single UTXO is large enough to fund the mint
error: no single UTXO is large enough to fund the deploy
error: no single UTXO is large enough to fund the dMint deploy

From glyph mint-nft (_mint_nft_inner), glyph deploy-ft (_deploy_ft_inner) and glyph deploy-dmint (_deploy_dmint_inner) respectively, all in src/pyrxd/cli/glyph_cmds.py. The cause line names the exact shortfall (need ≥ N photons in one UTXO; largest is M; mint-nft says in one plain-RXD (token-free) UTXO and largest in the wallet is M, because it never spends a UTXO that carries a token). When the metadata registers a WAVE name, mint-nft’s cause also names the registration fee it includes (…, including the 5 RXD WAVE registration fee for alice-name.rxd, which the reveal pays from this UTXO's change; …).

Cause: the commit/deploy builder needs one single UTXO that covers the commit value plus both the commit’s own fee estimate and slack for the reveal — it does not combine multiple smaller UTXOs for this step. For a WAVE name the same UTXO must also hold the registration fee (100, 50, 10 or 5 RXD for a name of up to 3, 4, 5 or 6+ characters). The commit output does not carry it: it stays in the commit’s change, and the reveal spends that change as a second input to pay the WAVE treasury.

Fix (all three, same text): consolidate UTXOs first, or fund the wallet from a single source.


4. “commit value cannot cover the reveal fee” (reveal too big to pay for itself)

Symptom, verbatim (from glyph mint-nft and glyph deploy-ft):

error: commit value cannot cover the reveal fee — refusing to broadcast the commit
  cause: commit value cannot fund the reveal: <N> photons available, <M> required
         (<carrier> carrier + <fee> reveal fee for a <size>-byte reveal carrying
         <cbor> bytes of CBOR at <rate> photons/byte) — short by <shortfall>
  fix: shrink the metadata (the reveal scriptSig carries the whole CBOR payload) or lower --fee-rate

— _assert_reveal_is_fundable in src/pyrxd/cli/glyph_cmds.py wraps the library’s InsufficientFundsError from check_reveal_funding (src/pyrxd/glyph/fees.py:620-656). Before that, the whole reveal is measured by assert_reveal_balances (same file): when the metadata registers a WAVE name its reveal also spends the wallet input that pays the fee, and a shortfall reads the reveal does not balance: inputs … ; outputs … + <photons> WAVE registration fee for <name>.rxd to <treasury>; miner fee … — short by <n> (or balances only by dropping its change). The registration fee never comes out of the commit.

Cause: the reveal transaction’s scriptSig carries the entire CBOR metadata payload, so a large metadata.json (a long image URL, several attributes) makes the reveal itself big — and therefore its own fee, which is paid entirely out of the commit output. check_reveal_funding computes the real fee (via the same SatoshisPerKilobyte.compute_fee the actual transaction uses, so it can’t drift) and refuses before the commit is broadcast if the commit can’t cover it. Without this guard, the failure would happen after the commit was already on-chain, stranding it — see the module docstring in fees.py for the history.

Fix: shrink the metadata (drop a long image URL, trim attributes), or raise the effective fee-model headroom. Note: the --fee-rate mentioned in the fix text is not an actual flag on mint-nft or deploy-ft — verified: neither command declares a --fee-rate option (src/pyrxd/cli/glyph_cmds.py). The real lever is the global fee rate, set via fee_rate in ~/.pyrxd/config.toml or the PYRXD_FEE_RATE environment variable (photons per byte; default 10000) — see src/pyrxd/cli/config.py. In practice, shrinking the metadata is the more useful fix, since fee rate is a network-relay floor, not something you generally want lower.

Scope: this pre-broadcast guard runs for glyph mint-nft and glyph deploy-ft. glyph deploy-dmint sizes its commit differently (multiple contract ref-seeds, not a single reveal-fee-vs-carrier trade) and does not call this same check.


5. Insufficient funds on glyph claim-dmint

Two distinct failures, both from glyph claim-dmint:

a) No funding UTXO found at all — verbatim:

error: could not find a plain-RXD funding UTXO for the mint
  cause: <InvalidFundingUtxoError text>
  fix: fund <miner_address> with >= <needed> photons of plain RXD, or pass --reward-address

— _claim_prepare in src/pyrxd/cli/glyph_cmds.py. needed = contract.reward + 10_000_000 + 546 photons — the reward, a fee buffer, and dust for the change output. This scans for a UTXO that is plain RXD only (token-bearing UTXOs at the same address are excluded).

b) A funding UTXO exists but is too small — the library’s PoolTooSmallError, message shape:

funding_utxo (<value> photons) too small to cover reward (<reward>) + fee (<fee>):
change would be <change> photons, below pyrxd's 546-photon uneconomic-change floor
(a pyrxd send policy, NOT a Radiant relay limit — Radiant's floor is 1 photon).
Fund the mint from a larger UTXO.

— raised by build_dmint_mint_tx in src/pyrxd/glyph/dmint/miner.py (both the V1 and V2 paths). It can happen even after (a) found a UTXO, because (a)’s needed allows a flat 10,000,000 photons for the fee while the mint pays size x fee_rate: a raised fee rate can outgrow that allowance. claim-dmint reports it as:

error: funding can't cover the mint reward + fee
  cause: <PoolTooSmallError text>
  fix: fund the reward address with one plain-RXD UTXO of at least the reward + the fee above + 546 photons. ...

claim-dmint has no --fee-rate flag; the rate is the configured one (fee_rate in the config file, or PYRXD_FEE_RATE). The default rate is the relay floor and the config refuses anything lower, so lowering it only helps if you raised it.

Fix: fund the reward/miner address (--reward-address, or the wallet address with the largest UTXO by default) with one plain-RXD UTXO of at least the reward + the fee in the cause + 546 photons. The funding scan picks the largest qualifying UTXO, so a new, larger one is the one it uses.

Not a funding problem, and no longer reported as one. Before 2026-09-23 two more errors printed the “funding can’t cover” headline although neither is about the amount; each now has its own:

  • The grind stopped without a nonce (V2 claims; V1 rerolls its OP_RETURN instead and reports no nonce found within N preimage rerolls — see below). When the wall clock ran out — the bundled or in-process miner reaching --timeout, or an external --miner-cmd not answering within it:

    error: mining timed out after <timeout>s without finding a nonce
      fix: allow a longer grind with --timeout SECONDS (this run allowed <timeout>); ...
    

    When a count ran out instead (the in-process --max-attempts cap, or a miner that swept its nonce space): error: mining stopped without finding a nonce. Its fix names a different --op-return, because the proof-of-work preimage binds the contract, the funding script and the OP_RETURN: re-running with the same inputs hands the miner the same preimage. With --miner-cmd in-process it also names --max-attempts, which reaches no other miner. Run pyrxd glyph dmint-estimate against the contract to see how long its target is likely to take on your machine.

  • A V1 claim ran out of rerolls (no nonce found within N preimage rerolls). Reroll i grinds the --op-return value with i appended, so the same --op-return repeats every search of the run before; the fix names a different one. It names --timeout only when some grind was stopped by the clock (a swept nonce space is not helped by more time), and --max-attempts only for --miner-cmd in-process.

  • The funding UTXO carries a token (InvalidFundingUtxoError from the mint builder): error: the funding UTXO carries a token and cannot pay for the mint. The funding scan in (a) already skips token-bearing UTXOs, so this is the builder’s second line of defence rather than something (a)’s choice produces.

Contracts claim-dmint refuses before any funding work. Right after it reads the contract, before it scans the wallet or grinds:

  • error: pyrxd cannot mine this contract: its proof of work is BLAKE3 (or K12). Every miner claim-dmint can use grinds and verifies SHA256d, including the external-miner protocol behind --miner-cmd, whose request carries no algorithm. Minting such a contract needs a miner for that hash; pyrxd does not ship one.

  • error: pyrxd will not mint this contract — the cause line says why:

    • this dMint contract can never be minted: its target ... — a target the covenant cannot read as a number: wider than 8 bytes, or not minimally encoded. pyrxd deployed both before 2026-09-23 — BLAKE3/K12 V2 contracts with 33-byte targets, and V1 contracts at difficulty 256 or more with the target pushed as a fixed 8 bytes. No miner can mint one.

    • this dMint contract can never be minted: its maxHeight ... (or its reward ...) — the other two state numbers the covenant reads on every mint, pushed wider than 8 bytes or not minimally. No miner can mint one.

    • this dMint contract's target is 0 — the covenant accepts only a proof of work whose number is exactly 0, and pyrxd does not grind for that.

    • this V1 dMint contract cannot be minted further — a V1 contract at height 2^31−1 whose max height is above 2^31. Its next mint would have to write height 2^31 into a 4-byte field, which the covenant cannot do, so every further spend fails. Every height below that mints normally.

    pyrxd glyph dmint-estimate refuses the same contracts rather than print an ETA.

All of these are mapped in claim_dmint_cmd, _claim_prepare and _grind_stopped_error in src/pyrxd/cli/glyph_cmds.py.


6. The node rejected your broadcast

Broadcast a transaction already documents the four rejection symptoms you’ll actually hit, in its “Handling broadcast errors” section — bad-txns-inputs-missingorspent, txn-mempool-conflict, min relay fee not met, mandatory-script-verify-flag-failed — read that page for what each one means and how to recover. This section restates what pyrxd raises for them, which is the same answer that page gives.

Current behavior (verified): ElectrumXClient now classifies a node’s JSON-RPC error. If the error code is one of {1, -25, -26, -27} (the codes ElectrumX/bitcoind use for “the node evaluated your tx and said no”), or the sanitized reason matches a known reject-reason marker (bad-txns-, txn-mempool-conflict, min relay fee not met, mandatory-script-verify-flag-failed, dust, non-final, and others — see _POLICY_MESSAGE_MARKERS), it raises the typed PolicyRejection instead of a bare NetworkError:

from pyrxd.security.errors import PolicyRejection, NetworkError

try:
    txid = await client.broadcast(raw_tx)
except PolicyRejection as exc:
    print(exc.code, exc.reason)   # e.g. -26, "bad-txns-inputs-missingorspent"
except NetworkError:
    ...  # a genuine transport fault (dropped socket, timeout, etc.)

— src/pyrxd/network/electrumx.py, _rpc_error and _POLICY_MESSAGE_MARKERS. PolicyRejection is a subclass of both NetworkError and CovenantError, so existing except NetworkError handlers still catch it. The node’s rejection reason is sanitized (first line only, control characters stripped, long tokens redacted, clipped to 200 chars) but is attached to the exception via .reason — it is the one deliberate carve-out from pyrxd’s “never embed server text in an exception” rule, because discarding it previously hid a real covenant-rejection bug for weeks. All four rejections named in the broadcast-a-transaction.md table match a marker above, so all four are distinguishable via exc.reason — you no longer need to inspect the block explorer to tell them apart. DEBUG logging would not help either way: the reader loop’s error branch logs nothing before setting the exception.

Fix: catch PolicyRejection specifically and branch on .reason; fall back to the broadcast-a-transaction.md symptom table for what each reason means.


7. Can’t reach ElectrumX, or requests keep timing out

Three real strings from ElectrumXClient:

  • "Failed to connect to any ElectrumX server" — src/pyrxd/network/electrumx.py:1055: every server in your URL list failed to connect. Check the URL(s), your network, and that the server is up.

  • "ElectrumX connection lost" — :1143: the WebSocket dropped mid-session; all pending requests fail with this. Reconnect (a fresh ElectrumXClient / async with block).

  • "ElectrumX request timed out" (or "... (send)") — :1192, :1197: a single RPC call exceeded its timeout. Retry, or pass a longer timeout if the server is just slow.

A different shape you’ll see during commit→reveal waits: glyph mint-nft, deploy-ft, and deploy-dmint all poll for the commit tx’s confirmation before broadcasting the reveal, via pyrxd.network.confirm.wait_for_confirmation (src/pyrxd/network/confirm.py). This poller swallows any NetworkError from an individual poll and keeps retrying — right after a broadcast, the tx is routinely not yet visible to the server, and that’s expected, not fatal. It remembers the last such error and names it in the eventual timeout message, so a persistently broken transport is still diagnosable instead of a bare “gave up.” At the CLI, a poll that never confirms surfaces as:

error: timed out waiting for confirmation
  cause: tx has <N> confirmations, required <M> (timeout (last poll error: <text>))
  fix: the transaction <txid> was broadcast and has not confirmed yet. It may still confirm, so do
       not simply re-run the command: that would commit, and spend, again. …

— src/pyrxd/cli/glyph_cmds.py, exit code 2. Fix: check the commit txid on a block explorer, and do not re-run the mint while the commit may still confirm.

  • glyph mint-nft or glyph timelock-mint. The command saved a record of the commit before broadcasting it, in pending-mints/ beside the wallet file (~/.pyrxd/pending-mints for the default wallet). The error names the commit’s txid, its value and that record, and prints the exact command to run once the commit confirms: pyrxd --network <net> --wallet <path> glyph resume-mint <commit txid> (the wallet path home-relative, ~/…; with --electrumx if the mint named a server, and --passphrase if the wallet was opened with one — the passphrase itself is never printed), and with the mint’s WAVE fee choice STATED: --wave-registration-fee if it paid the published treasury, --no-wave-registration-fee if it declined, and --wave-treasury <address> with the address the mint was given on its own command line if it named another treasury. That is the only place pyrxd prints a treasury other than the published one: every command printed from a record read off disk — anything resume-mint prints, and the refusals below — shows --wave-treasury '<ADDRESS>' for you to fill in, because a record can be changed after the mint. --allow-unverified-wave-name is repeated if the mint went ahead without an indexer’s answer. resume-mint takes the fee choice from its command line, never from the record: a bare resume-mint <txid> of a WAVE claim is refused and prints the commands that state it, and a choice that disagrees with the record (paying what it says was declined, or another treasury) is refused. Declining is always accepted. For a WAVE name it pays for, it asks an indexer again — before and after its confirmation prompt — and pays from a plain wallet input only if the answer is no confirmed registration; if the indexer says the name has been taken, the same command with --no-wave-registration-fee reveals the commit without the fee (if the name is taken, a duplicate claim the indexer does not register; if that one server’s answer was wrong, the reveal registers the name unpaid; either way the carrier and the change come back to the wallet). A second mint-nft for a name that still has an unrevealed record on the same network is refused unless it is given --ignore-pending-mint. Records carry their network (one directory serves every network a wallet is used on); resume-mint refuses a record for another network and names the --network to use. Those two refusals print commands for a record read off disk, so they never print one command carrying the record’s fee choice: unless the record says the mint declined, they print both — one that pays and one that declines — and you choose.

  • The window no check closes. RXinDexer answers wave.check_available from the blocks it has indexed, not from the mempool (electrumx/server/glyph_api.py:1498-1515 calls WaveIndex.check_available, electrumx/server/wave_index.py:1773-1800 at ca8a6a4e). A rival claim that is broadcast but not yet mined is invisible to every check, and the last check runs just before the reveal is broadcast, not when it is mined. If a rival claim for the name is mined first, this reveal is a duplicate the indexer does not register, and the registration fee it paid buys nothing.

  • What resume-mint checks first. The record must be the one for that txid, its commit script must re-derive from its payload and this wallet’s key, the NFT must go to this wallet, the server must list the commit output at the value the record says, and the record’s fee rate must be between Radiant’s relay floor and 10 times it. The reveal’s miner fee comes out of the commit alone; the wallet input pays only the registration fee and its own change. Any mismatch is refused, naming the field, before anything is signed. A value the server lists differently from the record is refused without blaming either: retry against another server (--electrumx) before deciding the record was changed.

  • The record is never deleted. Once a reveal is reported confirmed — by mint-nft or resume-mint, or by resume-mint finding a confirmed spend of the commit that pushes the record’s exact payload — the record is moved to pending-mints/done/, not deleted. “Confirmed” is the server’s word: a server can echo a reveal it never relayed, or invent a spend around a payload it reconstructed. If an explorer shows the commit output still unspent, run resume-mint <commit txid> against another server (--electrumx): it finds an archived record as well as a live one, moves it back, and reveals the commit. If the server lists the commit output as neither unspent nor spent, it says so and keeps the record where it is. That is why a finished mint-nft, resume-mint or timelock-mint says the server reports the reveal confirmed, names where the record is kept, and prints that command with --electrumx '<another-server-url>' (in --json: reveal_confirmed, record, recover_if_not_mined). The SDK’s GlyphMinter does the same: it calls PendingStore.archive() once a reveal is reported confirmed, never delete(), and JsonFilePendingStore moves the record to done/.

  • pending-mints/done must be absent or a real directory. It is checked (with lstat, so a symbolic link is seen as one) before the commit is broadcast and again before the reveal is, and a regular file or a link there is refused before that broadcast: move it aside and run the command again (after a commit, the refusal also names the commit’s recovery). resume-mint refuses it before it reads any record, live or archived. On POSIX every archive operation — archiving, reading, listing and restoring a record — acts on a descriptor of done opened with O_NOFOLLOW, so a link there is never followed and nothing it points to changes mode. (A pending-mints directory that is itself a link is used as it is; pyrxd tightens a real one to 0700 and leaves a link’s target alone.) If archiving still fails after a reveal is reported confirmed, the command succeeds and says the record was kept where it was (record_archive_error in --json).

  • Every exit after the commit is broadcast — a declined reveal prompt, a network error, a node rejecting the reveal, this timeout, Ctrl-C — prints the recovery, and in --json mode also writes it to stdout as a JSON document. A commit broadcast that fails or is interrupted (Ctrl-C while it is in flight) says the commit may or may not have reached the network, with what to do in each case.

  • It never confirms. If the commit leaves the mempool without confirming, its inputs were never spent, and resume-mint will not find it confirmed.

  • glyph deploy-ft or glyph deploy-dmint, or no record. There is no --resume flag and no COMMIT_TXID environment variable for these — earlier versions of this page and of the error hint said otherwise, and were wrong. (COMMIT_TXID is read by the standalone examples/*.py demo scripts, which carry their own hard-coded metadata and cannot resume a CLI mint.) Recover through the SDK instead:

    from pyrxd.glyph.builder import GlyphBuilder, RevealParams
    from pyrxd.glyph.payload import encode_payload
    
    cbor_bytes, _payload_hash = encode_payload(metadata)   # the SAME metadata file, unmodified
    scripts = GlyphBuilder().prepare_reveal(
        RevealParams(
            commit_txid=commit_txid,      # from the explorer
            commit_vout=0,
            commit_value=commit_value,    # the commit output's value
            cbor_bytes=cbor_bytes,
            owner_pkh=owner_pkh,          # the minting wallet's PKH
            is_nft=True,                  # False for an FT deploy
        )
    )
    

    Then build the reveal transaction spending commit_txid:0 with scripts.scriptsig_suffix appended to a normal P2PKH unlock — see examples/glyph_mint_demo.py for the shape. If the metadata registers a WAVE name, scripts.registration_fee_output is the fee: add it, funded from a plain wallet input, only if WaveResolver.check_available says the name is still free; if it is taken, build with pay_registration_fee=False instead.

    The metadata file must be byte-identical. The commit output’s script is OP_HASH256 <payload_hash> OP_EQUALVERIFY … before its P2PKH tail, so it can only ever be spent by a reveal that pushes CBOR hashing to that exact payload_hash. Re-encoding an edited metadata file produces a different hash and the output becomes unspendable — there is no owner-only escape path.

    For glyph deploy-dmint, use the code its error prints, not the snippet above. The commit checks the metadata body, the owner key and that the token ref is carried as an FT. It checks none of the dMint contract parameters, so a reveal built with other code or other values can still spend the commit, and the token it creates is permanent. When the commit broadcast’s reply names a different txid, deploy-dmint prints the prepare_dmint_deploy code with every parameter the command used, including the lastTime it stamped. deploy-ft names prepare_ft_deploy_reveal with its treasury and supply.


7b. “no ElectrumX endpoint is configured for network ‘regtest’”

error: no ElectrumX endpoint is configured for network 'regtest'
  cause: pyrxd ships no default regtest server (shipped defaults exist for: mainnet), and it
         will not fall back to another network's endpoint — that is exactly how a regtest
         command ends up talking to mainnet
  fix: add [networks.regtest] with electrumx = "wss://your-regtest-server:50022/" to
       ~/.pyrxd/config.toml, or pass --electrumx wss://your-regtest-server:50022/ (or set
       PYRXD_ELECTRUMX) for a one-off run

This is deliberate, and it replaced something worse. --network testnet and --network regtest used to “work” without any per-network configuration — by using the mainnet ElectrumX server while reporting themselves as testnet or regtest. A developer testing on regtest could broadcast a real transaction to mainnet with nothing on screen to suggest it. The top-level electrumx key now belongs to the top-level network and is never carried across a --network change; when nothing resolves, pyrxd refuses rather than guessing.

Fix — one of:

# ~/.pyrxd/config.toml
[networks.regtest]
electrumx = "ws://127.0.0.1:50022/"
allow_insecure = true          # required for plaintext ws://; regtest only
$ pyrxd --network regtest --electrumx ws://127.0.0.1:50022/ ...

--network mainnet needs no change.

Related: "ElectrumX endpoint is on the wrong chain: genesis <a> != expected <b>" means the endpoint answered, but block 0 is not the network you selected — ElectrumXClient.assert_chain checked, rather than trusting the URL. Either the [networks.<name>] entry points at the wrong server, or --network is wrong. Both hashes are printed; compare them against pyrxd.network.registry.GENESIS_BLOCK_HASHES.


7c. --wave-name degrades: two configured servers are “one source”

HashMark §7.6 form 2 (verify --wave-name, glyph inspect --wave-name) needs two configured ElectrumX servers of different operators. pyrxd counts sources as distinct operators, as declared, or by registered domain:

  • an operator pyrxd ships knowledge of (pyrxd.network.registry.KNOWN_OPERATORS — radiant4people, radiantcore and bladenet) is one source however many servers it runs, so electrumx.radiant4people.com and electrumx2.radiant4people.com are one;

  • any other hosts under one registered domain are one source (x.example.com and y.example.com), while a.co.uk and b.co.uk are two, because co.uk is a public suffix; an IP address is its own source;

  • every spelling of this machine (localhost, 127.0.0.1 or anything else in 127.0.0.0/8, ::1) is ONE source.

If your two servers really are run by different parties under one domain, or you want two domains you know to be one operator counted once, say so per server in the config file:

# ~/.pyrxd/config.toml
electrumx_servers = [
  { url = "wss://x.example.com:50022/", operator = "alice" },
  { url = "wss://y.example.com:50022/", operator = "bob" },
]

An operator is 1-64 lower-case letters, digits, . or -. Unknown keys in a server table, one host counted as two sources (two operators on its URLs, or an operator on one and not another), and a declaration that contradicts a shipped operator (e.g. electrumx2.radiant4people.com as anything but radiant4people) are refused, and so is a declared server next to an UNDECLARED server of the same registered domain (declare rpc.acme.io and leave backup.acme.io bare, and the two would count as two sources where without the declaration they are one — a declaration may merge sources, and may split a domain only when every server of that domain in the list is declared). All are refused when the list is read: at load for the top-level electrumx_servers, and when the network is selected for a [networks.<name>] list — before any server is contacted. pyrxd believes the declaration: it cannot see who runs a server, so declaring two operators that are really one makes the check agree with itself. The limit is stated in the pyrxd.network.source_identity module docstring.

A declaration applies to form 2 and to nothing else. The watchtower (--rxd-electrumx-url), the BTC Esplora quorum and the ETH RPC quorum take no operator declaration: they count by registered domain, or by an operator pyrxd ships knowledge of. If one of them refuses two URLs as one source, use URLs of two different registered domains.


8. dMint v2 flags and premine

--daa-mode requires --v2, verbatim:

error: --daa-mode requires --v2 (V1 dMint is FIXED difficulty only)

— src/pyrxd/cli/glyph_cmds.py, deploy_dmint_cmd. V1 dMint contracts are fixed-difficulty only; a difficulty-adjustment mode (fixed/asert/lwma/epoch/schedule) only makes sense on a V2 contract. Fix: add --v2 to glyph deploy-dmint, or drop --daa-mode.

The historical V2 opt-out is now a non-blocking warning, not an error. Verified in src/pyrxd/glyph/builder.py, GlyphBuilder._prepare_dmint_v2_deploy: calling prepare_dmint_deploy with allow_v2_deploy=False on DmintV2DeployParams used to refuse; as of 0.9.0 (V2 consensus-proven on regtest + mainnet, #219) it only emits a UserWarning:

prepare_dmint_deploy was called with allow_v2_deploy=False for a
DmintV2DeployParams; V2 deploy is no longer blocked (consensus-proven
on regtest + mainnet, #219) and proceeds anyway. Drop the
allow_v2_deploy=False argument to silence this warning.

This only matters if you’re calling GlyphBuilder.prepare_dmint_deploy directly with that argument — the CLI’s glyph deploy-dmint doesn’t expose allow_v2_deploy and always deploys V2 when --v2 is passed.

Premine ships, for both V1 and V2. This section used to say premine was deferred and quoted two ValidationError strings “verbatim”. Neither string exists in the source, and neither ever fires: _validate_premine (src/pyrxd/glyph/builder.py, called from both DmintV1DeployParams.__post_init__ and DmintV2DeployParams.__post_init__) is a bound check, not a refusal — it accepts any premine_amount >= 1 photon. The CLI exposes it as glyph deploy-dmint --premine <photons> [--premine-to <address>].

The refusals that do exist, verbatim:

premine_amount must be >= 1 photon when set (use None for no premine), got 0
premine_pkh was set without premine_amount — the premine output would not be emitted at all. Set premine_amount, or drop premine_pkh.

and, from the CLI’s own pre-check (src/pyrxd/cli/glyph_cmds.py, deploy_dmint_cmd):

--premine must be >= 1 photon (omit the flag for no premine)
--premine-to was given without --premine

The floor is 1 photon, not 546 — Radiant-Core has no dust threshold (GetDustThreshold returns 1 satoshi, IsDust is nValue <= 0), which is why every mainnet dMint contract sits at 1 photon. The 546-unit guard in prepare_ft_deploy_reveal is a pyrxd heuristic on a different flow (a whole FT supply below 546 units is almost certainly a decimals mistake) and does not apply here.

Fix: if you wanted a premine, pass --premine. Those photons come out of your own wallet on top of the mineable supply — the total issued supply becomes reward * max_height * num_contracts + premine.


9. “My FT shows up as plain RXD” (no error — a real gap)

Symptom: you hold a Glyph FT (or NFT), but pyrxd balance counts its value as ordinary RXD, and pyrxd utxos lists its UTXO like any other, with no indication it’s a token carrier.

There is no error string for this — it’s an honest gap, not a bug that raises anything. Verified: balance_cmd sums client.get_balance(...) across every used address’s raw scripthash (src/pyrxd/cli/query_cmds.py:234-294), and utxos_cmd lists whatever wallet.collect_spendable(client) returns (:297-354, backed by HdWallet.collect_spendable at src/pyrxd/hd/wallet.py:1285-1343). Neither path decodes the locking script to check whether it’s a Glyph FT/NFT envelope versus plain P2PKH — both just report the UTXO’s raw value field from ElectrumX. A Glyph carrier’s photon value (the 546-dust NFT carrier, or an FT’s premined supply amount) gets counted exactly like spendable RXD, which it is not — spending it as a plain-RXD input burns the token.

Fix: pyrxd glyph list --type ft (or --type nft, or --type all) — list_cmd in src/pyrxd/cli/glyph_cmds.py — runs the same gap-limit scan as balance and the spend commands, then decodes the Glyph envelope at each address it finds used, so token holdings show up as tokens, separate from the plain-RXD balance. Use this whenever you need to know what’s actually spendable versus what’s a token carrier.


References