Wallet API Reference

Every DAI miner exposes an HTTP API on walletApiPort (default 3456). Balance responses are in μDAI (raw) — 1 DAI = 1 000 000 000 μDAI. Input amounts for /api/wallet/send and staking endpoints are in DAI (the server converts internally).

Base URL
http://127.0.0.1:3456

Public miners (e.g. miner.iamai.kg) expose the same routes over HTTPS. CORS is open for browser clients.

Generate Wallet

A DAI wallet is an ed25519 keypair. The address is derived from the signing public key — no seed phrases, no custodians. Follow these four steps to create a wallet and start submitting jobs.

STEP 1 Generate the keypair

Run this Node.js script (requires tweetnacl — npm i tweetnacl). It deterministically derives an ed25519 signing keypair from 32 random bytes, then computes the canonical DAI address as 'dai' + sha256(signingPublicKey).slice(0,40).

// generate-wallet.js const crypto = require('crypto'); const nacl = require('tweetnacl'); function hexToUint8(hex) { const a = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) a[i/2] = parseInt(hex.slice(i, i+2), 16); return a; } const b64 = (u8) => Buffer.from(u8).toString('base64'); const sha256hex = (s) => crypto.createHash('sha256').update(s).digest('hex'); // 1. 32 random bytes → private key const privateKeyHex = crypto.randomBytes(32).toString('hex'); // 2. Deterministic ed25519 signing keypair // (same derivation as dai/wallet/src/services/signing.js) const seedHex = sha256hex(privateKeyHex + ':dai-ed25519-signing-v1'); const { publicKey, secretKey } = nacl.sign.keyPair.fromSeed(hexToUint8(seedHex)); const signingPublicKey = b64(publicKey); // 32-byte raw ed25519 pubkey in base64 const signingSecretKeyB64 = b64(secretKey); // 64-byte secretKey — keep secret // 3. Canonical address const address = 'dai' + sha256hex(signingPublicKey).slice(0, 40); // 4. Registration proof — ed25519 signature over the address string. // Required by /api/wallet/register-key (Step 2). const proof = b64(nacl.sign.detached(new TextEncoder().encode(address), secretKey)); const wallet = { address, privateKeyHex, signingPublicKey, signingSecretKeyB64, proof }; require('fs').writeFileSync('dai-wallet.json', JSON.stringify(wallet, null, 2)); console.log('Address:', address);
node generate-wallet.js # Address: daiaf4c36bfb8ee60d019251b613d1e5cd18a241d76

Keep dai-wallet.json secret. The signingSecretKeyB64 is the private signing key — never share or commit it.

STEP 2 Register the signing key on-chain

Post your address, signingPublicKey, and the proof from Step 1 to any miner. The proof is an ed25519 signature over your address — it proves you hold the private key. This binds your key to your address so the network can verify your job payment signatures.

curl -X POST https://miner.iamai.kg/api/wallet/register-key \ -H 'Content-Type: application/json' \ -d '{ "address": "dai...", "signingPublicKey": "<signingPublicKey from dai-wallet.json>", "proof": "<proof from dai-wallet.json>" }' # Response: { "ok": true }

Missing the proof returns 400 { "error": "address, signingPublicKey, and proof required" }. An invalid proof returns 403 { "error": "invalid proof" }. To rotate to a new key later, include a rotationProof signed by your existing key.

Registration is free and only needs to happen once per keypair. It does not require any DAI balance.

STEP 3 Fund the address

Compute and skill jobs require a signed paymentTx with sufficient budget. Fund your address via the P2P exchange, a transfer from another wallet, or a faucet. Check your balance at any time:

curl "https://miner.iamai.kg/api/wallet/balance?address=dai..." # Response: { "balance": 5000000000 } # 5 000 000 000 μDAI = 5 DAI (1 DAI = 1 000 000 000 μDAI)
STEP 4 Submit your first compute job

Sign a payment proof and post to /job. The snippet below uses the wallet file from Step 1.

// submit-job.js (requires tweetnacl) const crypto = require('crypto'); const nacl = require('tweetnacl'); const wallet = JSON.parse(require('fs').readFileSync('dai-wallet.json')); const secretKey = new Uint8Array(Buffer.from(wallet.signingSecretKeyB64, 'base64')); const MINER = 'https://miner.iamai.kg'; const sha256hex = (s) => crypto.createHash('sha256').update(s).digest('hex'); const sign = (data, sk) => { const msg = new TextEncoder().encode(typeof data === 'string' ? data : JSON.stringify(data)); return Buffer.from(nacl.sign.detached(msg, sk)).toString('base64'); }; async function run() { // 1. Who am I paying? The payment proof is bound to this exact miner + model. const { minerAddress, model } = await fetch(`${MINER}/api/miner/info`).then(r => r.json()); // 2. Current nonce (the proof is verified against this value, unincremented) const { nonce } = await fetch(`${MINER}/api/wallet/nonce?address=${wallet.address}`) .then(r => r.json()); // 3. Build payment proof — a CLIENT-generated jobId, hashed with the miner, // amount and nonce: sha256({ jobId, requesterAddress, minerAddress, amount, nonce }). // (Append a `currency` key when paying in a stablecoin instead of DAI.) const jobId = `job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const maxBudget = 500_000_000; // 0.5 DAI in μDAI const txHash = sha256hex(JSON.stringify({ jobId, requesterAddress: wallet.address, minerAddress, amount: maxBudget, nonce })); const signature = sign(txHash, secretKey); // 4. Submit job — `id` MUST be the jobId you signed above const r = await fetch(`${MINER}/job`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: jobId, type: 'compute', model, payload: { prompt: 'Summarise the DAI network in one sentence.' }, maxBudget, requesterAddress: wallet.address, paymentTx: { txHash, signature, nonce, timestamp: Date.now() } }) }); const res = await r.json(); if (!r.ok) { console.error('Rejected:', r.status, res.error, res.code); return; } console.log('Job submitted:', res.jobId); // 5. Poll for result while (true) { await new Promise(rs => setTimeout(rs, 2000)); const { status } = await fetch(`${MINER}/job/${res.jobId}/status`).then(x => x.json()); if (status === 'done') { const result = await fetch(`${MINER}/job/${res.jobId}/result`).then(x => x.json()); console.log('Result:', result.profile?.computeOutput ?? result.output); break; } if (status === 'error') { console.error('Job failed'); break; } console.log('Status:', status); } } run();
Quick reference
1Generate keypair & address locally — no network needed
2POST /api/wallet/register-key — once, free, any miner
3Fund address — GET /api/wallet/balance to verify
4POST /job with signed paymentTx — poll /job/:id/status

Chain & Wallet

Stablecoins & multi-currency

Fourteen regional stablecoins ride on the DAI chain alongside DAI: KGST (Kyrgyzstan som, bridged across DAI/BSC/Tron) plus αιETB, αιBTN, αιVES, αιPYG, αιBDT, αιPKR, αιEGP, αιIQD, αιAOA, αιCUP, αιLYD, αιSDG, αιIRR (wire tickers aiETB, aiBTN, aiVES, aiPYG, aiBDT, aiPKR, aiEGP, aiIQD, aiAOA, aiCUP, aiLYD, aiSDG, aiIRR). All use 2 decimals (1 unit = 100 raw); DAI keeps 9 (1 DAI = 1e9 μDAI). Off-chain on-ramps: USDT (USDT-ERC20, USDT-TRC20, USDT-TON, USDT-SOL, USDT-BEP20), USDC, and bank transfer.

GET /api/assets lists every asset with decimals, display names and per-currency gas prices. GET /api/wallet/balance returns an additional assets map with each stablecoin's raw and display balance.

Transfers: POST /api/wallet/send (and /api/tx/submit) accept a currency field — omit it for DAI. A transaction's signed hash includes currency ONLY when non-DAI, so existing DAI transactions and signatures remain valid unchanged.

GET /status

Node status — chain height, sync state, reputation.

curl http://127.0.0.1:3456/status
GET /api/miner/info

Version, uptime, peer count, wallet address.

GET /api/wallet/balance?address=<addr>

Balance in μDAI for a DAI wallet address.

curl "http://127.0.0.1:3456/api/wallet/balance?address=dai..."
GET /api/wallet/nonce?address=<addr>

Current nonce for transaction / payment signing.

GET /api/wallet/transactions?address=<addr>&limit=<n>

DAI transfer history, read from the chain — every node returns the same result for an address, and both sender and recipient see a transfer once it is mined. Mempool entries appear first with status: "pending".

GET /api/tx/<id>

One transaction by hash. Falls back to the mempool before returning 404, so a just-submitted transfer resolves immediately. Mining rewards have no hash of their own and carry a synthetic id of the form coinbase-<height>-<kind>-<address>.

GET /api/wallet/history?address=<addr>

Full history with block confirmations.

GET /api/wallet/jobs?address=<addr>&limit=20

Public job history — joins job-submitted transitions with mined scanResults. Returns { jobs, latestSkillMemory, chatTurns }.

POST /api/wallet/register-key

Register an ed25519 signing key for an address. Required before signed job payments. See Generate Wallet for how to produce the proof. proof = ed25519 signature over the address string.

{ "address": "dai...", "signingPublicKey": "base64...", "proof": "base64...", "rotationProof": "base64 (only when changing an existing key)" }
POST /api/wallet/send

Transfer DAI between wallets. The sender wallet must be stored on this node (loaded via walletManager). Amount is in DAI (not μDAI). Optional fee and memo fields.

{ "from": "dai...", "to": "dai...", "amount": 1, "memo": "optional" }
POST /api/tx/submit

Submit a pre-signed DAITransaction to the mempool.

GET /api/tx/pending

Inspect pending mempool transactions.

GET /api/chain/supply-audit

Chain supply audit — total issued vs. accounted balances.

Jobs

Submit compute work to the network. Types: verdict (identity scan, free), skill, compute. skill and compute jobs require a signed payment proof (maxBudget in μDAI).

Pricing & fees

Paid jobs are metered per AI compute token at 1 μDAI per token (the default gasPrice; μDAI is the smallest unit, so this is the floor). Since 1 DAI = 1 000 000 000 μDAI, one DAI buys up to 1 billion tokens.

The fee is tokensUsed × gasPrice and is never less than the tokens actually used (1 000 tokens ⇒ ≥ 1 000 μDAI). Unused budget is refunded. A maxBudget too small to cover the job's token cost is rejected with 402 FEE_TOO_LOW.

Block reward. On top of the fee, each block's ~1 DAI subsidy is split among the workers who completed jobs, weighted by delivered compute (a deterministic token estimate — splitting into many nodes earns nothing extra), minus a small (~10%) proposer cut. Blocks with no real work mint only a small keepalive, so emission tracks demand.

DAI has no fixed price — the market defines it via the best open P2P order (see GET /api/p2p/price).

Job fees can be paid in DAI or any stablecoin: set currency on the job payload. The fee floor is priced per currency, and the miner receives exactly the currency paid — no conversion.

POST /job

Submit a job. Returns { jobId, statusUrl, resultUrl }. id is generated by the client and signed into the payment proof: paymentTx.txHash is a SHA-256 of JSON.stringify({ jobId, requesterAddress, minerAddress, amount, nonce }) — not a blockchain transaction hash. minerAddress comes from GET /api/miner/info; nonce is the wallet's current /api/wallet/nonce value; the signature is over the hex hash string. When paying in a stablecoin, append currency to the hashed object.

{ "id": "job-<client-generated>", "type": "compute", "model": "qwen3-0.6b", "payload": { "prompt": "Summarize the dataset" }, "maxBudget": 500000000, "requesterAddress": "dai...", "paymentTx": { "txHash": "<computeJobPaymentHash>", "signature": "base64..." } }
POST /api/estimate  ·  GET /api/estimate?prompt=…

Estimate a job's fee before you sign it — the eth_estimateGas of DAI. Send the same fields you would submit; the node sizes the whole pipeline (attachment text, skill and MCP output, dataset rows, planner and synthesis calls) and returns the AI tokens it will use, the minimum fee it accepts and a recommended budget. Read-only — nothing runs and nothing is paid. Amounts are raw units of the fee currency (μDAI for DAI; 1 DAI = 1e9 μDAI).

{ "prompt": "Summarize the report and compare it with the latest news", "attachments": [{ "name": "report.md", "content": "..." }], "skillId": "web_search", // optional — else routed from the prompt, as a job would "mcp": ["shop__search"], // optional MCP tools "dataset": "some-org/some-dataset", // optional, must be installed "currency": "DAI", // or a stablecoin — quoted off the live P2P book "maxOutputTokens": 512, "route": true }
{ "route": { "mode": "routed-skill", "predicted": true, "skillId": "web_search" }, "tokens": { "prompt": {"min":542,"max":2042}, "output": {"min":1,"max":512}, "skillCompute": {"min":10,"max":1050}, "total": {"min":553,"max":3604} }, "breakdown": [{ "id": "skill:web_search", "kind": "skill", "tokens": {"min":0,"max":1500}, "basis": "bounded" }], "fees": { "minimum": { "tokens": 990, "raw": 990, "currency": "DAI", "gate": "/job" }, "recommended": { "tokens": 3604, "raw": 3604, "currency": "DAI" } }, "outputCap": { "budgetCapApplies": false }, "warnings": ["routing is predicted from the deterministic planner"] }

Each contributor is tagged measured (counted exactly — prompt, attachment text, dataset rows), bounded (what a skill fetched or an MCP tool returned is capped by the executing code, so it is a {min,max} range) or assumed. fees.minimum is what the node rejects below; fees.recommended covers the worst case. A currency nobody quotes on the P2P book is reported unavailable rather than priced. Errors: 422 dataset_not_installed, 404 skill_not_found, 400 for bad input. Available on nodes newer than 0.4.36.

Sealed replies

A public job can set payload.requesterEncryptionPublicKey (the requester's X25519 key). The chain then stores the prompt as promptCipher and the reply as profile.replyCipher, with computeOutput null. The job is still listed — encryption does not remove it. Only that key opens the text. Poll GET /job/:id/status (this miner, then the board, then the chain) or GET /api/wallet/jobs?address=. GET /jobboard/status?jobId= is the live queue: it keeps a finished job until a block includes it, then for 10 minutes.

POST /jobboard/submit

Same body as POST /job, sent to a bootnode so any miner can pull the work. The payment hash is sha256 of JSON.stringify({ jobId, requesterAddress, amount, nonce }) — no minerAddress, because the worker is not known yet. Poll GET /jobboard/status?jobId= while it is queued. After inclusion the same job is on GET /job/:id/status and in the wallet job list.

GET /job/:id/status

Poll job state: queued, computing, done, error. A miss in this process's memory is looked up on the job board and then on chain, so a board job stays visible after the queue drops it.

GET /job/:id/result

Full result when the job is done. Cleartext replies are in profile.computeOutput. Sealed replies are in profile.replyCipher (computeOutput is null) — open that envelope with the requester's X25519 key.

GET /jobs

List active jobs on this node.

POST /gossip

Receive P2P gossip envelopes from peer miners.

Chat & Routing

POST /chat/route

Route a message to the best matching skill.

{ "message": "audit this skill code", "budget": 0, "wallet": "dai..." }
POST /chat/ask

Full AI reply — routes to a skill or free chat. Accepts optional attachments (text files or images, ≤1 MB total). Images force a vision-capable model. May return { fromChainHistory: true } for cached on-chain replies.

{ "message": "What is my on-chain reputation?", "wallet": "dai...", "requesterAddress": "dai...", "history": [], "attachments": [ { "type": "image", "data": "<base64-image>", "filename": "screenshot.png" } ] }

Attachments are an optional array of { type: 'text'|'image', data: '<base64>', filename?: '...' }. Text is inlined into the prompt; images select a vision model automatically. Max 1 MB per request.

Private vs public context
Private chat uses in-memory session history. Public (paid) jobs also pull on-chain history via /api/wallet/jobs so follow-ups work across devices.

Model Context Protocol (MCP)

Every node ships a builtin pack (22 public-data tools + onion search). Chat picks from that address book instead of dumping every schema into the prompt. Extra servers overlay via Electron Settings or config.json. HTTP connectors accept arbitrary custom headers, not only Authorization: Bearer.

GET /api/mcp/status

List connected MCP servers and their available tools, plus the builtin pack. Returns { servers, tools, builtin }.

{ "servers": [ { "name": "web_search", "status": "connected", "tools": [ { "name": "search", "description": "Web search query" } ] } ] }
GET /api/mcp/catalog?q=

Search the address-book cards (builtin + overlay). Cards are names and one-liners, not full schemas.

POST /api/mcp/execute

Run a tool. Peers may call this for builtin tools so picked MCPs disperse across nodes.

MCP in chat
When you ask a question in /chat/ask or /chat/route, the miner picks 0–few tools from the catalog (never the whole pack), may run each on a different peer, then synthesizes. Tool names are namespaced: <server>__<tool>.

Configuration

Add servers to config.json under mcpServers (standard Claude Desktop / Cursor format):

{ "mcpServers": { "web_search": { "command": "node", "args": ["web-search-mcp/index.js"], "env": { "API_KEY": "..." } }, "remote": { "url": "https://example/mcp", "headers": { "Authorization": "Api-Key xyz", "X-User": "1" } } } }

Electron users: paste server config into Settings → MCP Servers instead of editing config.json manually.

Blockchain Explorer

GET /api/explorer/blocks?page=0&limit=20

Recent blocks (newest first). Each entry includes jobCount when the block contains mined results.

GET /api/explorer/block/:height

Full block JSON — scanResults, stateTransitions, hash, miner, reward.

GET /api/explorer/search?q=<query>

Lookup by block height, tx/block hash, or wallet address. Address search returns balance, transfers, and completed jobs.

Skills

On-demand agent modules. Deploy a public skill for 1 DAI; community staking reaches 10 DAI to graduate a skill network-wide.

GET /api/skills

List skills with staking totals and economics: { proposeFeeDai, graduationThresholdDai }.

curl https://miner.iamai.kg/api/skills
GET /api/skills/prefs?address=<addr>

Skill enable/disable and staking preferences for a wallet.

POST /api/skills/propose

Propose a new skill. Public proposals cost 1 DAI (escrowed for code_audit).

{ "manifest": { ... }, "code": "...", "private": false }
POST /api/skills/:id/stake

Stake DAI toward skill graduation. Amount is in DAI (not μDAI). Uses the node's own wallet.

{ "amount": 1000 }
POST /api/skills/:id/unstake

Withdraw staked DAI from a proposed skill.

POST /api/skills/:id/enable  |  /disable

Toggle whether this node runs a skill.

HF Datasets

Hugging Face datasets installed on this node. Compute jobs can reference them by ID. Chat will suggest installs when a dataset is missing. Datasets are stored under ~/.dai-miner/brain-data/hf-datasets/.

GET /api/hf-dataset

List installed datasets. Returns { datasets: [{ id, files, rowCount, source }] }.

POST /api/hf-dataset/:id/download

Install a dataset by Hugging Face ID. Falls back to pulling from a peer miner if HF is unreachable. Returns { ok: true, manifest } on success.

curl -X POST http://127.0.0.1:3456/api/hf-dataset/imdb/download
DELETE /api/hf-dataset/:id

Remove an installed dataset from this node.

LLM & Brain

Inference runs in-process via QVAC. Brain state stores human feedback and signal weights.

POST /api/chat

Streaming chat via local QVAC (Ollama-compatible body).

POST /api/generate

QVAC generate endpoint (Ollama-compatible body).

GET /api/models

Models available on this node (built-in + loaded + registry).

GET /api/network-models

Aggregated model list from connected peers.

GET /api/brain/state

Weights count, feedback count, model info.

GET /api/brain/weights

Full weights.json for the identity brain.

POST /api/brain/feedback

Submit a human correction to improve signal weights.

{ "address": "dai...", "aiVerdict": "human", "correction": "bot", "comment": "obvious bot pattern", "signals": [{ "methodId": "...", "result": true }] }
POST /api/brain/vote

Vote on signal weight adjustments.

P2P Exchange

On-chain DAI assets (DAI, aiETB, aiBTN, aiBDT, aiPKR, …) + bridged KGST (DAI/BSC/Tron) trade as base. When both legs settle on DAI (e.g. KGST/aiBDT or DAI/aiETB) the trade settles atomically in one step — no payment-sent/confirm phase; proceeds go directly to the taker's dai address. Off-chain legs (USDT, USDC, bank transfer) keep the manual escrow flow.

Peer-to-peer DAI trading with on-chain escrow.

GET /api/p2p/currencies

Supported quotes and on-chain bases. Includes KGST and USDT wires.

GET /api/p2p/markets

Pair list with last price (mid from the open book) and 24h change. change24h is null until history exists. Optional ?pair=BASE-QUOTE (e.g. KGST-USDT-TRC20).

GET /api/p2p/price?quoteCurrency=<cur>

Reference DAI price — the market price derived only from the best open P2P order(s): { price (mid), bestBid, bestAsk, source }. Omit quoteCurrency for a per-currency map. DAI has no oracle/fixed price.

GET /api/p2p/orders

Open order book. Filters: side, quoteCurrency, baseAsset, pair.

GET /api/p2p/orders/my?address=<addr>

Orders created by this address.

POST /api/p2p/orders

Create a buy or sell order.

POST /api/p2p/orders/:id/cancel

Cancel an open order.

POST /api/p2p/orders/:id/select

Taker selects an order to begin a trade. Optional takerPayoutAddress is the taker's off-chain receive address when taking a buy.

GET /api/p2p/trades/my?address=<addr>

Active and completed trades for an address.

POST /api/p2p/trades/:id/:action

Advance trade: payment-sent, release, cancel, dispute.

Push Notifications

POST /api/push/register

Register an Expo push token for a wallet address.

{ "address": "dai...", "token": "ExponentPushToken[...]" }
POST /api/push/send

Send a push notification. Optional address filter.

Errors & Auth

Responses are JSON. Errors return { "error": "message" } with an appropriate HTTP status (400, 401, 402, 404, 500).

Payment-required (402): skill/compute jobs without a valid signed paymentTx are rejected before execution.

Signing: register your ed25519 key via /api/wallet/register-key, then sign payment proofs binding jobId + minerWallet + maxBudget + nonce.

SDKs: use runCompute() / submitJob() in dai-miner SDK to handle signing automatically, and estimate() to price a job first.