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
spend ⊇ manage ⊇ read — 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
| Method | Purpose |
|---|---|
ping | liveness + version |
get_status / get_follower_status | daemon + indexer-follower health |
get_block_height | tip height + genesis_block_id |
get_block_by_id / get_block_by_height / get_block_id_at_height | block lookups |
get_transaction | transaction by id |
get_balance / get_wallet_balance | one address / whole-keyring balance |
get_address_utxos / get_script_utxos | spendable outputs |
get_address_mempool | unconfirmed entries touching an address |
get_output_spent_by | the input that spent an outpoint (indexer) |
validate_address | address well-formedness check |
wait_for_tx / wait_for_payment | long-poll for confirmation / incoming funds |
Indexer reads (indexer) — read
| Method | Purpose |
|---|---|
get_address_history | confirmed tx history for an address |
list_settlements | settlement records the daemon tracks |
htlc_status / htlc_list / htlc_lookup_by_hashlock | HTLC lifecycle observability |
contract_stats | aggregate contract counters |
get_attestation_edges | attestation graph edges |
detect_in_chain_swaps | scan for on-chain swap legs |
Keyring — manage to create, spend to reveal/export/delete
| Method | Scope | Purpose |
|---|---|---|
generate_standard_address | manage | default: 1:1 address from a standard BIP-39 phrase (exfer.dev-compatible) |
generate_independent_address | manage | 1:1 address, raw secret as its own phrase |
generate_address | manage | HD-derive next index (seeded keyrings only) |
import_private_key | manage | import a raw 32-byte secret |
import_mnemonic | manage | import an independent 24-word phrase |
import_standard_mnemonic | manage | import a standard 24-word phrase |
list_addresses | read | list keyring addresses + labels |
reveal_address_mnemonic | spend | a single address's 24-word phrase |
reveal_mnemonic / reveal_private_key | spend | legacy seed mnemonic / raw key |
export_address / export_vault | spend | seal one key / the whole keyring to a vault blob |
import_vault | spend | restore a vault blob |
delete_address | spend | erase a key (refuses on non-zero balance unless force) |
Transactions & messages
| Method | Scope | Purpose |
|---|---|---|
transfer | spend | build, sign, broadcast a payment |
send_raw_transaction | spend | broadcast a pre-built tx |
abandon_transfer | manage | drop a stuck in-flight transfer |
sign_message / verify_message | spend / read | proof-of-ownership signatures |
simulate_transfer / simulate_htlc_lock | read | cost/feasibility dry-run |
HTLC
| Method | Scope | Purpose |
|---|---|---|
htlc_lock / htlc_claim / htlc_reclaim | spend | open / claim / reclaim an HTLC |
htlc_forget | manage | drop local HTLC tracking state |
Payment URI — read
payment_uri_encode / payment_uri_decode — exfer: URI codec.
Cross-chain swap & LP (swap engine / pool)
| Method | Scope | Purpose |
|---|---|---|
swap_get_quote | spend | reserve a preimage + seal a swap quote |
swap_execute / swap_refund | spend | lock/claim / reclaim both legs |
swap_status / swap_list | read | swap journal |
swap_pool_info / swap_price_klines | read | pool reserves, fees / price chart |
lp_pool_info / lp_position / lp_deposit_status | read | LP pool + position views |
lp_deposit_start | read | begin an LP deposit (returns funding instructions) |
lp_withdraw_self | spend | cash out LP shares to the user |
BSC / EVM side (native BNB counter-asset)
| Method | Scope | Purpose |
|---|---|---|
bsc_get_address / bsc_get_balances | read | EVM address / BNB+token balances |
bsc_tx_history | read | native-BNB transfer history |
bsc_create_address / bsc_import_mnemonic / bsc_import_key | manage | provision the independent EVM key |
bsc_reveal_mnemonic | spend | reveal the EVM recovery phrase |
reveal_evm_private_key | spend | export the EVM key (MetaMask import) |
bsc_delete_key | spend | delete the EVM key (can strand BNB) |
bsc_send_bnb | spend | withdraw native BNB |
ping
| Scope | read |
| 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.
| Scope | read |
| 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.
| Scope | manage |
| 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).
| Scope | read |
| 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.
| Scope | read |
| 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.
| Scope | read |
| Params | {} |
| Returns | see 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.
| Scope | read |
| 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.
| Scope | read |
| Params | { address: hex64 } |
| Returns | see 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).
| Scope | read |
| Params | { script_hex: hex } |
get_block_height
Chain tip.
| Scope | read |
| Params | {} |
| Returns | { height: u64, block_id: hex64 } |
get_block_by_id
| Scope | read |
| Params | { block_id: hex64 } |
| Returns | BlockSummary (see below) |
get_block_by_height
| Scope | read |
| Params | { height: u64 } |
| Returns | BlockSummary |
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.
| Scope | read |
| 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.
| Scope | read |
| Params | { tx_id: hex64 } |
| Returns | see 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.
| Scope | spend |
Params
| Field | Type | Required | Description |
|---|---|---|---|
from | hex64 | yes | Sender address (HD-derived or imported). |
outputs | [{ to: hex64, amount: u64 }] (1..=16) | yes | Recipient list. Each amount ≥ DUST_THRESHOLD (200). |
fee_rate | u64 | no | exfers per cost-unit. Mutually exclusive with fee. |
fee | u64 | no | Absolute fee in exfers. Mutually exclusive with fee_rate. |
max_fee | u64 | no | Cap; default 2_000_000 (0.02 EXFER). |
client_token | string (8..=128 ASCII) | no | Idempotency key. |
datum | hex (≤ 4096 bytes) | no | Generic 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
| Code | When |
|---|---|
-32001 | Wrong scope token. |
-32602 | Param shape error (from not 64-hex, outputs[] missing, fee+fee_rate both set, …). |
-32010 | Wallet not found — from is not a known address. |
-32020 | Upstream node unreachable / RPC error. |
-32030 | UTXO authentication failed. |
-32031 | Insufficient balance (with in_flight_reserved hint). |
-32032 | Fee exceeds max_fee. |
-32033 | An outputs[].amount is below dust. |
-32034 | outputs[] longer than 16. |
-32035 | Same 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
pwithsha256(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):
- B picks a secret, shares
hash_lock = sha256(secret)with A. - 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 heightH+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_reclaimspend 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 plaintransfer. walletd computes it automatically whenfeeis omitted; passfeeonly 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).
| Scope | spend |
Params
| Field | Type | Required | Description |
|---|---|---|---|
from | hex64 | yes | Sender wallet address (funds + signs). |
receiver | hex64 | yes | Receiver's 32-byte pubkey (the key that can claim). |
hash_lock | hex64 | yes | sha256(preimage). |
timeout | u64 | yes | Absolute block height after which from may reclaim. |
amount | u64 | yes | Amount to lock (exfers), ≥ DUST_THRESHOLD (200). |
fee_rate | u64 | no | exfers per cost-unit. Mutually exclusive with fee. |
fee | u64 | no | Absolute fee. Mutually exclusive with fee_rate. |
max_fee | u64 | no | Cap; 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).
| Scope | spend |
Params
| Field | Type | Required | Description |
|---|---|---|---|
from | hex64 | yes | Receiver wallet address (claims + receives). |
lock_tx_id | hex64 | yes | The htlc_lock transaction id. |
output_index | u32 | no | HTLC output index in the lock tx (default 0). |
preimage | hex (1..=1024 bytes) | yes | Secret whose sha256 equals the lock's hash_lock. |
sender | hex64 | yes | Sender's pubkey — reconstructs the script. |
timeout | u64 | yes | The lock's timeout — reconstructs the script. |
fee | u64 | no | Absolute 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.
| Scope | spend |
Params
| Field | Type | Required | Description |
|---|---|---|---|
from | hex64 | yes | Sender wallet address (reclaims). |
lock_tx_id | hex64 | yes | The htlc_lock transaction id. |
output_index | u32 | no | HTLC output index (default 0). |
receiver | hex64 | yes | Receiver's pubkey — reconstructs the script. |
hash_lock | hex64 | yes | The lock's hash_lock — reconstructs the script. |
timeout | u64 | yes | The lock's timeout height. |
fee | u64 | no | Absolute 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.
| Scope | spend |
| 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.
| Scope | manage |
| 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).
| Scope | spend |
| 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.
| Scope | read |
| 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.
| Scope | spend |
| 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.
| Scope | spend |
| 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
| Scope | read |
Params — identical to transfer except client_token is not
accepted (there's nothing to deduplicate when no tx is broadcast):
| Field | Type | Required | Description |
|---|---|---|---|
from | hex64 | yes | Sender address. |
outputs | array | yes | 1..=16 {to: hex64, amount: u64}. |
fee_rate | u64 | no | Same as transfer. |
fee | u64 | no | Same as transfer. |
max_fee | u64 | no | Same 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
| Scope | read |
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
| Scope | read |
Params
| Field | Type | Required | Description |
|---|---|---|---|
lock_tx_id | hex64 | yes | The lock transaction's id. |
output_index | u32 | no | Default 0. |
Returns — full HtlcRecord (see htlc_list for the
shape).
Errors — -32010 WalletNotFound if the index has no record for
that outpoint.
htlc_list
| Scope | read |
Params — every field optional:
| Field | Type | Description |
|---|---|---|
role | enum | sender / receiver / both / any (default). |
state | enum / array of enum | Filter to one or more of locked / locked_expired / claimed / reclaimed / unknown. |
since_height | u64 | Only entries with lock_block_height ≥ this. |
limit | u32 | Default 100, capped at 1000. |
cursor | str | Opaque cursor from a previous response. |
address | hex64 | Reserved 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
| Scope | manage |
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
| Field | Type | Required | Description |
|---|---|---|---|
lock_tx_id | hex64 | yes | |
output_index | u32 | no | Default 0. |
Returns
{ removed: bool }
removed = false when no such record existed.
Errors — -32602 BadParams for a non-settled record.
get_follower_status
| Scope | read |
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
| Scope | read |
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
tx_id | hex64 | yes | The transaction id. | |
min_confirmations | u32 | no | 1 | Block depth required. |
timeout_secs | u64 | no | 60 | Max 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
| Scope | read |
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
address | hex64 | yes | The address (script) to watch for incoming credit. | |
min_amount | u64 | no | 1 | Only report a credit of at least this many exfers. |
timeout_secs | u64 | no | 60 | Max 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
| Scope | read |
Params
| Field | Type | Required | Description |
|---|---|---|---|
address | hex64 | yes | Recipient address. |
amount | u64 | no | Base units (exfers). |
memo | str | no | Free-form, percent-encoded. |
hash_lock | hex64 | no | For HTLC requests. |
timeout | u64 | no | Pair with hash_lock. |
label | str | no | Short payee label. |
Returns — { uri: str }.
payment_uri_decode
| Scope | read |
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_addressparams:{ "address": "<64 hex>", "passphrase": "<vault passphrase>" }→{ "address": "<64 hex>", "vault_hex": "<hex blob>" }.export_vaultparams:{ "passphrase": "<vault passphrase>" }→ the sealedvault_hexblob.
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)).