RPC reference (v1.13)

JSON-RPC 2.0 over POST /. GET /healthz is unauthenticated and returns ok for liveness probes.

Single requests and batches (per JSON-RPC 2.0 § 6) are both accepted. Notifications (requests with no id field) get no response per spec.

Every method below follows the same envelope:

Request

{
  "jsonrpc": "2.0",
  "method":  "<method-name>",
  "params":  { ... },
  "id":      1
}

jsonrpc must be exactly "2.0". id, when present, must be a JSON string, number, or null; object, array, and boolean ids are rejected with -32600 and response id: null. Omitting id makes the request a JSON-RPC notification and walletd returns no response.

Response (success)

{ "jsonrpc": "2.0", "result": { ... }, "id": 1 }

Response (error)

{ "jsonrpc": "2.0",
  "error":   { "code": -32xxx, "message": "...", "data": { ... } },
  "id":      1 }

data is omitted for most errors; populated for the structured ones listed in Error codes.

Amounts and fees are integers in exfers, where 1 EXFER = 100_000_000 exfers. Consensus dust threshold is 200 exfers.

Examples below assume:

URL='http://127.0.0.1:7448'
SPEND=$(cat ~/.exfer-walletd/token-spend)
READ=$(cat ~/.exfer-walletd/token-read)

Scope mapping

spendmanageread — a token at a higher scope satisfies every lower scope. The authoritative source is Scope::for_method in src/auth.rs; the per-method scope is shown in the catalog below.

Method catalog

Every dispatched method, grouped by family. Methods marked (node) or (indexer) proxy the upstream node / the embedded indexer; the rest act on the local keyring or the swap engine. Detailed request/response sections follow for the core wallet, HTLC, simulation, observability, and payment-URI methods; the keyring, swap, LP, and BSC families are documented at the end of this page.

Chain reads (node) — read

MethodPurpose
pingliveness + version
get_status / get_follower_statusdaemon + indexer-follower health
get_block_heighttip height + genesis_block_id
get_block_by_id / get_block_by_height / get_block_id_at_heightblock lookups
get_transactiontransaction by id
get_balance / get_wallet_balanceone address / whole-keyring balance
get_address_utxos / get_script_utxosspendable outputs
get_address_mempoolunconfirmed entries touching an address
get_output_spent_bythe input that spent an outpoint (indexer)
validate_addressaddress well-formedness check
wait_for_tx / wait_for_paymentlong-poll for confirmation / incoming funds

Indexer reads (indexer) — read

MethodPurpose
get_address_historyconfirmed tx history for an address
list_settlementssettlement records the daemon tracks
htlc_status / htlc_list / htlc_lookup_by_hashlockHTLC lifecycle observability
contract_statsaggregate contract counters
get_attestation_edgesattestation graph edges
detect_in_chain_swapsscan for on-chain swap legs

Keyring — manage to create, spend to reveal/export/delete

MethodScopePurpose
generate_standard_addressmanagedefault: 1:1 address from a standard BIP-39 phrase (exfer.dev-compatible)
generate_independent_addressmanage1:1 address, raw secret as its own phrase
generate_addressmanageHD-derive next index (seeded keyrings only)
import_private_keymanageimport a raw 32-byte secret
import_mnemonicmanageimport an independent 24-word phrase
import_standard_mnemonicmanageimport a standard 24-word phrase
list_addressesreadlist keyring addresses + labels
reveal_address_mnemonicspenda single address's 24-word phrase
reveal_mnemonic / reveal_private_keyspendlegacy seed mnemonic / raw key
export_address / export_vaultspendseal one key / the whole keyring to a vault blob
import_vaultspendrestore a vault blob
delete_addressspenderase a key (refuses on non-zero balance unless force)

Transactions & messages

MethodScopePurpose
transferspendbuild, sign, broadcast a payment
send_raw_transactionspendbroadcast a pre-built tx
abandon_transfermanagedrop a stuck in-flight transfer
sign_message / verify_messagespend / readproof-of-ownership signatures
simulate_transfer / simulate_htlc_lockreadcost/feasibility dry-run

HTLC

MethodScopePurpose
htlc_lock / htlc_claim / htlc_reclaimspendopen / claim / reclaim an HTLC
htlc_forgetmanagedrop local HTLC tracking state

Payment URI — read

payment_uri_encode / payment_uri_decodeexfer: URI codec.

Cross-chain swap & LP (swap engine / pool)

MethodScopePurpose
swap_get_quotespendreserve a preimage + seal a swap quote
swap_execute / swap_refundspendlock/claim / reclaim both legs
swap_status / swap_listreadswap journal
swap_pool_info / swap_price_klinesreadpool reserves, fees / price chart
lp_pool_info / lp_position / lp_deposit_statusreadLP pool + position views
lp_deposit_startreadbegin an LP deposit (returns funding instructions)
lp_withdraw_selfspendcash out LP shares to the user

BSC / EVM side (native BNB counter-asset)

MethodScopePurpose
bsc_get_address / bsc_get_balancesreadEVM address / BNB+token balances
bsc_tx_historyreadnative-BNB transfer history
bsc_create_address / bsc_import_mnemonic / bsc_import_keymanageprovision the independent EVM key
bsc_reveal_mnemonicspendreveal the EVM recovery phrase
reveal_evm_private_keyspendexport the EVM key (MetaMask import)
bsc_delete_keyspenddelete the EVM key (can strand BNB)
bsc_send_bnbspendwithdraw native BNB

ping

Scoperead
Params{}
Returns{ ok: true }

validate_address

Pure-function check that address is a syntactically well-formed 64-character hex string (32 bytes). No upstream call.

Scoperead
Params{ address: string }
Returns{ valid: bool, normalized: hex64 | null }

normalized is lowercased on success, null on failure.


generate_address

Derive the next HD address from the keystore seed and persist its index. Optionally tag it with a label.

Scopemanage
Params{ label?: string }
Returns{ address: hex64, pubkey: hex64, index: u32 }

Sequential calls return index: 0, 1, 2, …. The address is fully determined by (seed, index) — back up the 24-word mnemonic (shown once at first start) and every present and future address is recoverable.


list_addresses

Enumerate every known address (derived + imported).

Scoperead
Params{}
Returns{ addresses: AddressEntry[] }
type AddressEntry = {
  address: hex64,
  index?: u32,      // present for derived; absent for imported
  label?: string,
  imported: bool,
};

get_wallet_balance

Aggregate confirmed balance across every managed address.

For each known address, walletd calls upstream get_balance and (by default) get_address_utxos, so it can return both balance and utxo_count. That is 2 upstream scan RPCs per address, executed concurrently with cap 8. On public/community nodes with per-IP scan quotas, large wallets can hit upstream rate limits.

Pass { "utxos": false } to skip the per-address get_address_utxos call: this returns balances only (1 scan RPC per address) and omits utxo_count / truncated. Use it for frequent balance polling (e.g. a live deposit watcher) and fetch UTXO counts on demand when you actually need them.

Pass { "addresses": [hex64, …] } to scan only a subset of managed addresses (unknown addresses are ignored). The scan count then tracks how many addresses you actually poll, so a client can skip hidden addresses and poll a single visible address far more often without tripping the node's rate limit. Absent ⇒ every managed address.

Scoperead
Params{ utxos?: bool, addresses?: hex64[] }utxos defaults to true; addresses defaults to all
Returns{ entries: WalletEntry[], total: u64 } (only the scanned addresses; total sums them)
type WalletEntry = {
  address: hex64,
  index?: u32,
  label?: string,
  imported: bool,
  balance: u64,
  utxo_count?: u32,  // omitted when called with { utxos: false }
  truncated?: bool,  // upstream UTXO list was clipped at 1000
};

get_status

Operator dashboard in one call: daemon version, chain tip, wallet count, upstream URLs, in-flight counters.

Scoperead
Params{}
Returnssee below
{
  version:             string,
  tip: { block_id: hex64 | null, height: u64 | null },
  upstream_ok:         bool,
  upstream_nodes:      string[],
  wallet_count:        u32,
  in_flight_utxos:     u32,
  in_flight_transfers: u32,
}

tip.* is null if the upstream RPC fails — the status call still succeeds so a dashboard can show partial state.


get_balance

Confirmed balance for one address.

Scoperead
Params{ address: hex64 }
Returns{ address: hex64, balance: u64 }

Mempool entries are NOT counted (upstream design). For pending balance, walk get_address_utxos and inspect mempool transactions manually.


get_address_utxos

List confirmed UTXOs locked to an address.

Scoperead
Params{ address: hex64 }
Returnssee below
{
  address:    hex64 | null,
  script_hex: hex   | null,
  tip_height: u64,
  truncated:  bool,
  utxos: [
    { tx_id: hex64, output_index: u32, value: u64,
      height: u64, is_coinbase: bool },
    ...
  ]
}

If truncated is true, the upstream node hit its 1000-entry result limit and there is no pagination cursor (upstream limitation — see README for the open RFC).


get_script_utxos

Same shape as get_address_utxos, keyed by raw script bytes (hex).

Scoperead
Params{ script_hex: hex }

get_block_height

Chain tip.

Scoperead
Params{}
Returns{ height: u64, block_id: hex64 }

get_block_by_id

Scoperead
Params{ block_id: hex64 }
ReturnsBlockSummary (see below)

get_block_by_height

Scoperead
Params{ height: u64 }
ReturnsBlockSummary
type BlockSummary = {
  block_id:          hex64,
  height:            u64,
  prev_block_id:     hex64,
  state_root:        hex64,
  tx_root:           hex64,
  timestamp:         u64,
  nonce:             u64,
  difficulty_target: hex64,
  tx_count:          u64,
  transactions:      hex64[],   // tx_ids
};

get_block_id_at_height

Explicit height → block_id lookup. Same shape as get_block_height.

Scoperead
Params{ height: u64 }
Returns{ height: u64, block_id: hex64 }

Performance: the upstream node has no native height→id index, so walletd fetches the full block and discards everything else. Same network cost as get_block_by_height. If your next step is to read the block body, call get_block_by_height directly — one round trip instead of two.


get_transaction

Fetch a single transaction by id. Returns confirmed-chain or mempool entries; in_mempool distinguishes.

Scoperead
Params{ tx_id: hex64 }
Returnssee below
{
  tx_id:        hex64,
  tx_hex:       hex,
  in_mempool:   bool,
  block_id:     hex64 | null,
  block_height: u64   | null,

  // Decoded view (added for accounting / explorers — no upstream
  // calls beyond parent-tx fetches for value resolution).
  inputs: [
    {
      prev_tx_id:    hex64,
      output_index:  u32,
      address?:      hex64,
      script_hex?:   hex,
      value?:        u64,
      witness?: {
        pubkey?:       hex64,
        signature?:    hex128,
        witness_hex?:  hex,
        redeemer_hex?: hex,
      },
    }, ...
  ],
  outputs: [
    { address?: hex64, script_hex?: hex, value: u64 },
    ...
  ],
  total_out:  u64,
  total_in?:  u64,    // omitted if any input failed to resolve
  fee?:       u64,    // omitted with total_in
  size:       u64,
}

transfer

Build, sign, and broadcast a multi-output payment.

Scopespend

Params

FieldTypeRequiredDescription
fromhex64yesSender address (HD-derived or imported).
outputs[{ to: hex64, amount: u64 }] (1..=16)yesRecipient list. Each amount ≥ DUST_THRESHOLD (200).
fee_rateu64noexfers per cost-unit. Mutually exclusive with fee.
feeu64noAbsolute fee in exfers. Mutually exclusive with fee_rate.
max_feeu64noCap; default 2_000_000 (0.02 EXFER).
client_tokenstring (8..=128 ASCII)noIdempotency key.
datumhex (≤ 4096 bytes)noGeneric app-defined on-chain blob attached to the primary (first) recipient output. The chain validates only size; meaning is the application's. Read it back from get_transaction (outputs[].datum).

Defaults: if neither fee nor fee_rate is set, fee_rate=1 (consensus minimum). Fee is always floored at consensus::cost::min_fee and refused if it would exceed max_fee.

Returns

{
  tx_id:           hex64,
  size:            u64,
  fee:             u64,      // effective fee (incl. folded sub-dust change)
  fee_rate:        u64,      // effective fee × MIN_FEE_DIVISOR / tx_cost
  inputs:          [{ tx_id: hex64, output_index: u32, value: u64 }],
  outputs:         [{ to: hex64, amount: u64, is_change: bool }],
  built_at_height: u64,      // tip at UTXO-listing time (not inclusion)
}

Idempotency: when client_token is supplied, the receipt is cached for 1 hour. A repeat call with the same token + same params returns the cached receipt without re-running. Same token + different params → -32035 IdempotencyConflict.

Common errors

CodeWhen
-32001Wrong scope token.
-32602Param shape error (from not 64-hex, outputs[] missing, fee+fee_rate both set, …).
-32010Wallet not found — from is not a known address.
-32020Upstream node unreachable / RPC error.
-32030UTXO authentication failed.
-32031Insufficient balance (with in_flight_reserved hint).
-32032Fee exceeds max_fee.
-32033An outputs[].amount is below dust.
-32034outputs[] longer than 16.
-32035Same client_token used with different params.

HTLC methods

Hash time-locked contracts over JSON-RPC, so an agent can run HTLC payments (atomic swaps, escrow, conditional settlement) without re-implementing Exfer Script or the signing transcript in its own language. walletd builds and signs in-process and broadcasts via the node — identical wire output to the exfer script htlc-* CLI.

The HTLC script has two spend arms:

  • hashlock — the receiver claims by revealing a preimage p with sha256(p) == hash_lock, plus the receiver's signature.
  • refund — after timeout (an absolute block height), the sender reclaims with their signature.

Lifecycle (receiver B claims; otherwise sender A reclaims):

  1. B picks a secret, shares hash_lock = sha256(secret) with A.
  2. A: htlc_lock { from: A_addr, receiver: B_pubkey, hash_lock, timeout: H+N, amount }tx_id. 3a. B: htlc_claim { from: B_addr, lock_tx_id: tx_id, preimage: secret, sender: A_pubkey, timeout: H+N }. 3b. or, if B never claims, after height H+N — A: htlc_reclaim { from: A_addr, lock_tx_id: tx_id, receiver: B_pubkey, hash_lock, timeout: H+N }.

For a cross-chain atomic swap, the same preimage unlocks the mirror HTLC on the other chain — htlc_claim reveals it on-chain in plaintext.

Fee note. htlc_claim/htlc_reclaim spend a script input, which the node prices with the spent script's evaluation cost (min_fee_with_script_cost), so their minimum fee is higher than a plain transfer. walletd computes it automatically when fee is omitted; pass fee only to override (it must still clear the minimum).


htlc_lock

Fund an HTLC output payable to receiver against hash_lock, refundable to from after timeout. Funds from from's UTXOs exactly like transfer (auto-change, same fee handling).

Scopespend

Params

FieldTypeRequiredDescription
fromhex64yesSender wallet address (funds + signs).
receiverhex64yesReceiver's 32-byte pubkey (the key that can claim).
hash_lockhex64yessha256(preimage).
timeoutu64yesAbsolute block height after which from may reclaim.
amountu64yesAmount to lock (exfers), ≥ DUST_THRESHOLD (200).
fee_rateu64noexfers per cost-unit. Mutually exclusive with fee.
feeu64noAbsolute fee. Mutually exclusive with fee_rate.
max_feeu64noCap; default 2_000_000.

Returns

{
  tx_id:             hex64,
  htlc_output_index: u32,   // always 0 (change, if any, is output 1)
  amount:            u64,
  hash_lock:         hex64,
  timeout:           u64,
  receiver:          hex64,
  size:              u64,
  fee:               u64,
  fee_rate:          u64,
  built_at_height:   u64,
  change?:           u64,   // present iff change was returned to `from`
}

Common errors: -32001, -32602, -32010 (from unknown), -32020, -32031 (insufficient balance), -32032 (fee > max_fee), -32033 (amount < dust).


htlc_claim

Claim an HTLC's hashlock arm by revealing the preimage. from is the receiver wallet (also where the funds land).

Scopespend

Params

FieldTypeRequiredDescription
fromhex64yesReceiver wallet address (claims + receives).
lock_tx_idhex64yesThe htlc_lock transaction id.
output_indexu32noHTLC output index in the lock tx (default 0).
preimagehex (1..=1024 bytes)yesSecret whose sha256 equals the lock's hash_lock.
senderhex64yesSender's pubkey — reconstructs the script.
timeoutu64yesThe lock's timeout — reconstructs the script.
feeu64noAbsolute fee. Default = script-aware consensus minimum.

Returns

{ tx_id: hex64, kind: "claim", value: u64, fee: u64,
  lock_tx_id: hex64, output_index: u32, size: u64 }

value is paid to from (htlc_value − fee).

walletd reconstructs the HTLC script from (sender, from's pubkey, sha256(preimage), timeout) and authenticates the on-chain output against it before spending — a wrong preimage/sender/timeout (or a lying node) yields -32036 and nothing is broadcast.

Common errors: -32001, -32602, -32010, -32020, -32036 (output auth / script mismatch), -32030, -32603.


htlc_reclaim

Reclaim an HTLC's refund arm after timeout. from is the original sender wallet.

Scopespend

Params

FieldTypeRequiredDescription
fromhex64yesSender wallet address (reclaims).
lock_tx_idhex64yesThe htlc_lock transaction id.
output_indexu32noHTLC output index (default 0).
receiverhex64yesReceiver's pubkey — reconstructs the script.
hash_lockhex64yesThe lock's hash_lock — reconstructs the script.
timeoutu64yesThe lock's timeout height.
feeu64noAbsolute fee. Default = script-aware consensus minimum.

Returns: same shape as htlc_claim, with kind: "reclaim".

walletd checks get_block_height first and rejects with -32037 (timeout not reached) when current_height ≤ timeout, before building anything. Output authentication (-32036) applies as in htlc_claim.

Common errors: -32001, -32602, -32010, -32020, -32037 (timeout not reached), -32036 (output auth), -32030.


send_raw_transaction

Broadcast a pre-signed transaction. Passes through to the upstream.

Scopespend
Params{ tx_hex: hex }
Returns{ tx_id: hex64 }

abandon_transfer

Release outpoints from walletd's in-flight set (the local "soft reserve" that prevents two concurrent transfers from picking the same UTXO). Use after a transfer's broadcast appears to have failed and you've confirmed via get_transaction(tx_id) that the network never accepted it.

Scopemanage
Params{ outpoints: [{ tx_id: hex64, output_index: u32 }] }
Returns{ released_count: u32, remaining_in_flight: u32 }

In-flight outpoints also auto-expire on TTL (10 minutes); this call is for explicit / faster release.


sign_message

Sign an arbitrary UTF-8 message with the Ed25519 key of a managed wallet. Domain-separated under EXFER-MSG, so a message signature can never be mistaken for a transaction signature (transactions sign under EXFER-SIG).

Scopespend
Params{ address: hex64, message: string }
Returns{ signature: hex128, pubkey: hex64, address: hex64 }

sign_message is gated behind spend even though it doesn't move funds, because the artifact is a verifiable proof of key ownership — value-bearing in exchange / KYC contexts.


verify_message

Verify an Ed25519 message signature. Pure crypto, no wallet access.

Scoperead
Params{ pubkey: hex64, signature: hex128, message: string, address?: hex64 }
Returns{ valid: bool, address: hex64 }

address (in the response) is always the address derived from pubkey, so a verifier sees what the key actually hashes to even on valid: false. If the optional request address is supplied, valid is true iff signature verifies AND H(DS_ADDR || pubkey) == address.


reveal_mnemonic

Re-supply the keystore passphrase and receive the 24-word BIP-39 mnemonic that produced this keystore. Sensitive — only call after a deliberate user action.

Scopespend
Params{ passphrase: string }
Returns{ mnemonic: string[] } (24 lowercase BIP-39 words)

The passphrase is verified by re-unsealing seed.enc with it. Wrong passphrase surfaces as -32012 Keystore locked. The in-memory passphrase from daemon start is not reused — clients must pass it freshly, which mirrors standard wallet "type your password to reveal" gating.

Walletd's HD path is m/44'/9527'/0'/0'/i', so the returned mnemonic is not directly portable to most third-party wallets (their default coin-type-44 derivation uses a different coin_type slot). It is the canonical recovery secret for re-running walletd against the same keystore.


reveal_private_key

Re-supply the keystore passphrase and receive the raw 32-byte ed25519 secret for a single managed address. Sensitive — only call after a deliberate user action.

Scopespend
Params{ address: hex64, passphrase: string }
Returns{ address: hex64, secret_hex: hex64 }

Works for HD-derived addresses (re-derives from the just-unsealed seed) and for imported addresses (re-unseals the per-key file with the same passphrase). Wrong passphrase → -32012 Keystore locked. Address not in this keystore → -32010 Wallet not found.

The returned secret_hex is a 32-byte ed25519 private key in lowercase hex, the same secret format consumed by exfer-walletd migrate --from <dir>.


Cost simulation (v1.9)

Read-scope dry-runs of the corresponding spend methods. Same fee estimation, same UTXO selection, same builder — but never broadcasts and never moves funds. Use them to prove a cost ceiling holds before committing to spend.

simulate_transfer

Scoperead

Params — identical to transfer except client_token is not accepted (there's nothing to deduplicate when no tx is broadcast):

FieldTypeRequiredDescription
fromhex64yesSender address.
outputsarrayyes1..=16 {to: hex64, amount: u64}.
fee_rateu64noSame as transfer.
feeu64noSame as transfer.
max_feeu64noSame as transfer.

Returns

{
  size:             u64,
  fee:              u64,
  fee_rate:         u64,
  inputs:           [{tx_id, output_index, value}],
  outputs:          [{to, amount, is_change}],
  total_in:         u64,   // sum of inputs.value
  total_out:        u64,   // sum of outputs.amount
  change:           u64,   // 0 if no change output
  built_at_height:  u64,
}

tx_id is intentionally omitted — nothing was broadcast. total_in = total_out + fee is invariant for a well-formed result.

simulate_htlc_lock

Scoperead

Same params as htlc_lock. Returns the same fee / size / change shape as simulate_transfer, plus the HTLC-specific fields:

{
  size, fee, fee_rate,
  htlc_output_index: u32,
  amount:            u64,
  hash_lock:         hex64,
  timeout:           u64,
  receiver:          hex64,
  total_in:          u64,
  change:            u64,
  built_at_height:   u64,
}

HTLC observability (v1.9)

The block follower watches every accepted block, identifies HTLC outputs paying any owned key, and tracks their lifecycle in a local index. These methods read that index.

htlc_status

Scoperead

Params

FieldTypeRequiredDescription
lock_tx_idhex64yesThe lock transaction's id.
output_indexu32noDefault 0.

Returns — full HtlcRecord (see htlc_list for the shape).

Errors-32010 WalletNotFound if the index has no record for that outpoint.

htlc_list

Scoperead

Params — every field optional:

FieldTypeDescription
roleenumsender / receiver / both / any (default).
stateenum / array of enumFilter to one or more of locked / locked_expired / claimed / reclaimed / unknown.
since_heightu64Only entries with lock_block_height ≥ this.
limitu32Default 100, capped at 1000.
cursorstrOpaque cursor from a previous response.
addresshex64Reserved for the indexer integration (v1.9.1+).

Returns

{
  htlcs: [
    {
      lock_tx_id:           hex64,
      output_index:         u32,
      params: {
        sender:         hex64,   // pubkey
        receiver:       hex64,   // pubkey
        hash_lock:      hex64,
        timeout_height: u64,
      },
      amount:               u64,
      lock_block_height:    u64 | null,
      state:                "locked"|"locked_expired"|"claimed"|"reclaimed"|"unknown",
      claim:                { tx_id, preimage, block_height, input_index } | null,
      reclaim:              { tx_id, block_height, input_index } | null,
      role:                 "sender"|"receiver"|"both"|"observer",
      last_indexed_height:  u64,
    },
    ...
  ],
  next_cursor?: str,
}

Records are returned in ascending (lock_block_height, lock_tx_id, output_index) order. If next_cursor is present, pass it back as cursor to fetch the next page.

htlc_forget

Scopemanage

Remove a settled (Claimed / Reclaimed) HTLC from the local index. Refuses to forget still-Locked entries — an active wallet must not silently lose track of an open obligation.

Params

FieldTypeRequiredDescription
lock_tx_idhex64yes
output_indexu32noDefault 0.

Returns

{ removed: bool }

removed = false when no such record existed.

Errors-32602 BadParams for a non-settled record.

get_follower_status

Scoperead

Operator / agent dashboard for "how caught up is the follower."

Params{}.

Returns

{
  last_indexed_height:   u64,
  last_indexed_block_id: hex64,
  tip_height:            u64,
  lag:                   i64,   // = tip_height - last_indexed_height
  indexed_htlc_count:    u64,
  follower_started_at:   u64,   // unix seconds
  full_scan_complete:    bool,
}

wait_for_tx

Scoperead

Subscribes to the follower's tip channel and returns as soon as the named transaction has at least min_confirmations blocks behind it. No client-side polling required.

Params

FieldTypeRequiredDefaultDescription
tx_idhex64yesThe transaction id.
min_confirmationsu32no1Block depth required.
timeout_secsu64no60Max wait, capped at 600.

Returns

{
  tx_id:         hex64,
  block_id:      hex64,
  block_height:  u64,
  confirmations: u64,   // ≥ min_confirmations on success
}

Errors-32040 WaitTimeout if the budget expires before the transaction reaches min_confirmations. The error's data payload includes {tx_id, min_confirmations, elapsed_secs} so a client can retry or escalate programmatically.

Unknown-tx and in_mempool=true responses from the node are treated as "not yet visible" — wait_for_tx keeps waiting up to the timeout.


wait_for_payment

Scoperead

Blocks until a new credit to address is observed, then returns immediately. The fast path wakes on the in-process push bus fed by the node's /sse endpoint — a script_changed nudge arrives within a network RTT of the paying transaction hitting the node's mempool, so an agent learns "I was paid" sub-second, with no polling. When the node has no /sse (pre-1.12) the method falls back to waking on each follower tip advance.

Returns as soon as the payment is seen in the mempool (0 confirmations) — this is a liveness/receipt signal, not settlement finality. Pair it with wait_for_tx when you need the credit buried to a confirmation depth.

Params

FieldTypeRequiredDefaultDescription
addresshex64yesThe address (script) to watch for incoming credit.
min_amountu64no1Only report a credit of at least this many exfers.
timeout_secsu64no60Max wait, capped at 600.

Returns — on a credit:

{
  address:       hex64,
  received:      true,
  timed_out:     false,
  tx_id:         hex64 | null,  // null when detected via a confirmed-balance delta
  amount:        u64,           // value of the new credit
  confirmations: u64,           // 0 = mempool-seen, ≥1 = already confirmed
  tip_height:    u64,
}

On timeout (a quiet window — a normal outcome for a watcher, not an error):

{ address: hex64, received: false, timed_out: true, waited_secs: u64 }

Payment URI codec (v1.9)

Pure functions — no upstream calls, no key access. Round-trip payment requests through a canonical BIP21-style string:

exfer:<address>[?amount=N&memo=...&hash_lock=...&timeout=N&label=...]

payment_uri_encode

Scoperead

Params

FieldTypeRequiredDescription
addresshex64yesRecipient address.
amountu64noBase units (exfers).
memostrnoFree-form, percent-encoded.
hash_lockhex64noFor HTLC requests.
timeoutu64noPair with hash_lock.
labelstrnoShort payee label.

Returns{ uri: str }.

payment_uri_decode

Scoperead

Params{ uri: str }.

Returns — the same shape as payment_uri_encode's input. Unknown query keys are silently dropped (forward-compatible). Address / hash_lock are normalised to lowercase hex.


Batch requests

Send a JSON array of envelopes; receive a JSON array of responses, with notifications (no id) omitted. JSON-RPC 2.0 permits batch responses in any order, so clients should correlate by id. Walletd currently preserves request order in the response array, but callers should not rely on order when using generic JSON-RPC tooling.

curl -s $URL \
  -H "Authorization: Bearer $READ" \
  -H 'content-type: application/json' \
  -d '[
        {"jsonrpc":"2.0","method":"ping","id":1},
        {"jsonrpc":"2.0","method":"get_block_height","id":2}
      ]'

Empty batches return a single top-level -32600 response. Batches consisting entirely of notifications return 204 No Content. Mixed batches return HTTP 200 with per-item result / error objects in the array.


Keyring management

The keyring is a flat set of 1:1 keys; see Keystore for the model. All addresses are 64-hex pubkey hashes (lowercase).

generate_standard_address

manage. The default way to mint an address. Derives a fresh 1:1 key from a random standard BIP-39 phrase (the exfer.dev-compatible derivation), so its recovery phrase restores the same address in any Exfer wallet.

  • Params: { "label": "<string, optional>" } (or none).
  • Result: { "address": "<64 hex>", "pubkey": "<64 hex>", "imported": true }.

generate_independent_address

manage. Same shape, but the key's recovery phrase is its raw 32-byte secret encoded as BIP-39 — self-contained, walletd-restore only.

generate_address

manage. Legacy HD derivation: bumps next_index and derives the next address from the keyring's seed. Only meaningful on a seeded keyring (see Keystore → Seeded vs seedless). Params { "label": "<optional>" }.

import_private_key

manage. Register a raw 32-byte ed25519 secret. Params { "private_key": "<64 hex>", "label": "<optional>" }.

import_mnemonic / import_standard_mnemonic

manage. Register a 24-word BIP-39 phrase as a key — import_mnemonic treats it as an independent phrase, import_standard_mnemonic derives it through the standard exfer.dev domain.

  • Params: { "mnemonic": "<24 words>", "label": "<optional>" }.
  • Result: the imported address.

reveal_address_mnemonic

spend. Return one address's own 24-word recovery phrase. Sensitive.

  • Params: { "address": "<64 hex>", "passphrase": "<keystore passphrase>" }.
  • Result: { "address": "<64 hex>", "mnemonic": ["word", …] }.

export_address / export_vault

spend. Seal key material to a WDV1 vault blob. export_address covers a single address; export_vault covers every key in the keyring (single-file backup that survives adding addresses).

  • export_address params: { "address": "<64 hex>", "passphrase": "<vault passphrase>" }{ "address": "<64 hex>", "vault_hex": "<hex blob>" }.
  • export_vault params: { "passphrase": "<vault passphrase>" } → the sealed vault_hex blob.

The vault passphrase is independent of the keystore passphrase: it protects the portable blob.

import_vault

spend. Restore keys from an export_vault / export_address blob; each lands as an independent key, and addresses already present are skipped.

  • Params: { "vault_hex": "<hex blob>", "passphrase": "<vault passphrase>" }.
  • Result: the list of restored addresses.

delete_address

spend. Erase a key from the keyring. Destructive — refuses while the address holds a confirmed balance (or when the upstream balance can't be checked) unless force is set. Back the key up first (export_address or reveal_address_mnemonic).

  • Params: { "address": "<64 hex>", "passphrase": "<keystore passphrase>", "force": false }.

Swap, LP, and BSC

The cross-chain swap engine settles EXFER against native BNB over a pool, using HTLCs on the Exfer leg and an EVM key on the BSC leg. These methods are summarized in the catalog above; their request/response shapes track the pool protocol and the EVM side, and the fund-moving ones (swap_get_quote, swap_execute, swap_refund, bsc_send_bnb, lp_withdraw_self, and the EVM key reveals/deletes) require spend. LP read views (lp_pool_info, lp_position, lp_deposit_status) and swap views (swap_status, swap_list, swap_pool_info, swap_price_klines) are read. The LP deposit/withdraw calls key off the caller's EXFER and BSC addresses (lp_deposit_start(exfer_address, bsc_address), lp_withdraw_self(exfer_address, shares)).