Integrations
Four shapes cover almost everything: add ZKAS to an existing wallet, take deposits as an exchange, accept merchant payments, or publish authenticated app data. You implement no cryptography in any of them.
Pick a tier
| Tier | What the user gets | What you run |
|---|---|---|
| Receive + view | an address, a balance, incoming history | keys on device, a walletd registered with the viewing key |
| Full non-custodial | the above plus sending, seed never leaves the device | the same, plus the signer WASM for on-device verify-and-sign |
| Fully local | the above with nothing outside seeing even a viewing key | walletd embedded in your app as a library; only a node is external |
| Custodial | you hold the funds | walletd with the seed, plus a payout strategy — see exchange |
Add ZKAS to your wallet
Three ready-made pieces, no cryptography to write:
| Piece | Role |
|---|---|
zkas-signer (WASM) | on-device keys: derive address, derive viewing key, verify and sign a payment |
@zkas/sdk (npm) | typed client for the whole send flow, with progress callbacks and auto-chunking |
zkas-walletd | keyless daemon that scans, proves and broadcasts — hosted, or run your own |
npm install @zkas/sdk
import { ZKasClient, wasmPaymentSigner, DEFAULT_MAX_FEE_SOMPI } from "@zkas/sdk";
import { generateWallet, fvkHex, verifyAndSignPayment } from "./signer";
// 1. Keys on the device. seedHex is the secret - store it like a private key.
const { seedHex, address } = await generateWallet("mainnet");
// 2. Register the VIEWING KEY with a daemon. Never the seed.
const client = new ZKasClient({ baseUrl: WALLETD_URL, token: perWalletToken });
await client.watch(await fvkHex(seedHex), /* birthday DAA */ 0);
// 3. Balances are decimal strings. BigInt them.
const bal = BigInt((await client.balance()).balance_sompi);
// 4. Send: prepare -> verify-on-device -> sign -> submit, all inside send().
const signer = wasmPaymentSigner({ seedHex, fvkHex, verifyAndSignPayment });
const res = await client.send(
signer,
{ to: "zkas:...", amountSompi: 5_000_000_000n, maxFeeSompi: DEFAULT_MAX_FEE_SOMPI },
(stage) => console.log(stage), // "proving" | "signing" | "broadcasting"
);
console.log(res.txids, res.feeSompi);That is a complete non-custodial integration.
If you drive the WASM yourself — another framework, another language over FFI, or no dependency at all — the flow is four HTTP calls plus one local signature. The checks are the point.
// 1. Register the viewing key.
POST /api/wallet/watch { fvk_hex, birthday }
header: X-Wallet-Token: <random 16-byte hex>
// 2. Daemon builds AND PROVES an unsigned bundle. It cannot sign it.
POST /api/wallet/prepare { fvk_hex, to, amount_sompi, allow_partial: false }
-> { session, sighash, bundle_hex, spend_auth[], disclosure[],
amount_sompi_exact, fee_sompi_exact, remaining_sompi_exact }
// 3. ON DEVICE, before signing anything:
// - paid === requested and remaining === 0, else refuse
// - fee <= your ceiling, else refuse
// - recompute the sighash from bundle_hex; do NOT trust the returned one
// - check the bundle against disclosure (recipient, amounts)
// then sign each spend_auth[].alpha
// 4. Hand the signatures back.
POST /api/wallet/submit { session, sigs: [{ index, sig }] }
-> { txid, txids, tx_count, amount_sompi, fee_sompi }- Never send the seed anywhere. Register
fvk_hexonly. - Refuse a daemon that changed the amount. Compare
amount_sompi_exactto what you asked for, and requireremaining_sompi_exact === 0on the single-transaction path. - Cap the fee before signing, not after.
- Recompute the sighash from
bundle_hex. The returnedsighashis a convenience; a compromised daemon could lie about it, and blind-signing lets it authorize a payment to an attacker. - Verify the bundle against
disclosureso the device checks what it signs. - Parse balances with BigInt — they are decimal strings precisely so JavaScript cannot lose integer precision.
- Set a long client timeout. Proving takes tens of seconds; a send that looks hung is usually progressing.
zkas-walletd in-process as a native library and the desktop app embeds the same crate, so the only outside party is a node serving compact blocks. See self-hosting.Integrate an exchange or custodian
/api/wallet/address takes no parameters and always returns the same string; nothing in the stack mints per-customer addresses. Deposit attribution must therefore come from memos, not from addresses.- One deposit wallet + memo
- Howevery customer gets the same address plus a unique memo / payment idVerdictUse this. The XRP/XLM destination-tag pattern — one wallet, one scan
- One wallet per customer
- Howa token per customer, each its own
.scanVerdictDoes not scale. Every wallet syncs independently; 350 loaded wallets already strain a 4-core box
- Split visibility from spend authorityRun a watch-only walletd from the FVK alone to detect and credit deposits. The spending key never touches the deposit scanner. Sign withdrawals on separate infrastructure holding the seed, via
prepare→submit. - Credit deposits by memoRead
/api/wallet/historyand match the memo to a customer. Memos are readable per transaction on the receive side, up to 512 bytes. - Gate withdrawals on
spend_readyNot onsynced. And treatmissing_history:trueas "this balance is a lower bound" — never as final. - Consolidate continuouslyWithdrawal latency is
2.4 s × notes spent ÷ cores. Leave--auto-consolidateon, and issue one call for the full amount rather than splitting by hand. - Decide your cold-backup conventionZKas has no BIP-39. You may derive the 32-byte seed from a mnemonic yourself and import it as
seed_hex, but the derivation is your private convention and the official wallet cannot restore from it. Test the round-trip before funding anything.
Accept merchant payments
The gateway issues invoices, watches for payment and fires webhooks. Same API hosted or self-hosted. Call it from your server — the API key must never reach browser code.
POST /api/v1/invoices
Authorization: Bearer YOUR_API_KEY
Idempotency-Key: order-123
Content-Type: application/json
{
"orderId": "order-123",
"amountSompi": "10000000",
"expiresIn": 900,
"requiredBlueScore": 10,
"redirectUrl": "https://shop.example.com/order/123"
}
-> { "id": "inv_...", "address": "zkas:...", "amountSompi": "10000000",
"status": "new", "checkoutUrl": "https://pay.example.com/checkout/inv_..." }10000000 sompi is 0.1 ZKAS. Amounts are decimal strings so JavaScript cannot lose integer precision.
GET /api/v1/invoices/inv_...
| Status | Meaning |
|---|---|
new | no payment received |
partial | some value received, not enough |
paid | full amount detected, confirmation policy not yet reached |
confirmed | full amount received and confirmed |
overpaid | more than requested received |
expired | unpaid invoice expired |
paidLate | payment arrived after expiry |
confirmed or overpaid.Set ZKAS_GATEWAY_WEBHOOK_URL and ZKAS_GATEWAY_WEBHOOK_SECRET. Events arrive as JSON with:
ZKas-Signature: t=TIMESTAMP,v1=HEX_HMAC expected = HMAC-SHA256(secret, timestamp + "." + exact_request_body)
- Use a constant-time comparison.
- Reject timestamps outside a short tolerance.
- Process event ids idempotently — delivery may repeat.
const response = await fetch("/api/create-zkas-invoice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId: "order-123" })
});
const invoice = await response.json();
location.href = invoice.checkoutUrl;gateway/integrations/web/zkas-pay.js ships the same redirect flow as a small browser helper. For WordPress, install gateway/integrations/woocommerce/zkas-gateway.php, enable ZKAS Gateway in WooCommerce payment settings, and supply the gateway URL, API key, webhook secret, conversion rate and required blue-score distance. Point the gateway webhook at https://shop.example.com/wp-json/zkas/v1/webhook.
Idempotency-Key for every order.Atomic multi-party settlement not wired
A sale, swap or escrow where both sides move or neither does — the seller spends their note, the buyer spends payment notes, both receive, and a public record is written, in one bundle.
Why the bundle shape allows it
Orchard authorizes each action separately: every action carries its own spend_auth_sig over its own randomized alpha, so one bundle can hold spends belonging to different people. What welds them together is one sighash over every action, one proof, and one binding signature that only verifies if the bundle's true net value equals its declared value_balance. Because spend_auth_sig is excluded from the sighash, signatures can be collected independently and cannot be lifted onto a different bundle.
build_for_pczt → create_proof → per-action sign → finalize_io → apply_binding_signature — and the finalizer applies a list of (action_index, signature) pairs without caring which key produced each one. The non-custodial flow you use today is already a partial-signing protocol with a prover that holds no spend authority.What is missing
Wrapper-level work only — no consensus change, no circuit change:
- the prepare entry point takes one viewing key and applies it to every spend, although the underlying Orchard builder takes an FVK per spend;
- all spends must share one anchor, so a two-party build needs an explicit shared-anchor parameter instead of each wallet choosing its own;
- there is no transport for exchanging partial bundles between parties.
ak, nk, rivk, the note and its Merkle path. It never needs the secret spend key, but whoever proves sees both sides. There is no MPC proving here, so one party or a coordinator holds the counterparty's viewing key for the trade.
The fix is a per-trade key, and on ZKas that is the natural unit anyway. With no ZIP-32 and one address per wallet, a fresh unlinkable address already means a fresh 32-byte seed. Fund a single-use trade key, and the viewing key you disclose reveals nothing but the trade.
What atomicity does and does not buy
Note that adaptor signatures, FROST and cross-curve DLEQ are all unbuilt, so there is no cross-transaction atomic swap primitive. The single bundle is the only atomic settlement path the chain has.
Prove a payment you made
rseed is one-time, memory-only and never persisted, and out_ciphertext is sealed under a random key. If you may need to prove an outgoing payment later, you must capture it at send time; it cannot be reconstructed afterwards.- A third party can verify
- Build the send through
POST /api/wallet/prepareand store the returned disclosure —spend_value, out_value, out_recipient, out_rseed, rcvper action. A verifier recomputescmxandcv_netfrom it and matches them against the on-chain action. No keys needed. There is no flag on/api/wallet/sendthat produces this - Your own records
POST /api/wallet/settings {"recoverable_history":true}attaches your outgoing viewing key at send time so your wallet can re-read its own sends. Opt-in per wallet, not retroactive, and proves nothing to a third party- Recipient proving receipt
- Always possible with their IVK
Publish authenticated public app data no fork needed
For protocols that need publicly indexable data — listings, prices, token metadata — while addresses and balances stay private.
The 512-byte memo travels in enc_ciphertext, which is hashed into the shielded sighash, so memo bytes are already committed by the bundle's signatures. They are only encrypted, not unauthenticated. Make them public by publishing a viewing key:
- Create a registry address and publish its incoming viewing keyAnyone can now decrypt notes sent to it. Spending still requires the spend key, so this is safe.
- Send a value-0 note to that addressWith your protocol data in the memo. A zero-value note is a real note.
- Indexers recover it with
scan_bundle(ivk, bundle)Integrity comes free from the signature.
Budget: 512 B per action, each action costs 3,156 B of a 124,744 B transaction, and each announcement displaces a spend slot. Keep bulk metadata off-chain behind a content hash. Build these with the SDK — the walletd send endpoint rejects zero amounts.
SDK and repositories
@zkas/sdk·firecash/zkas-sdk- typed client: prepare/verify/sign/submit, progress, auto-chunking, fee ceiling
firecash/zkas-signer- signer WASM — key derivation and on-device verify-and-sign. No circuit, so it is small
firecash/zkas-rusty- node, consensus,
shielded-core,zkas-walletd,shielded-payCLI firecash/zkas-payment-gateway- merchant gateway plus web and WooCommerce integrations
firecash/zkas-pool- reference pool and stratum bridge
firecash/zkas-paper-wallet- paper wallet, served from its own GitHub Pages so the page matches the audited source
zkas-rusty at zkas-v1.0.9. Byte layouts, endpoint shapes and parameters are read from source; figures marked measured come from the live mainnet node. If this page contradicts the code, the code is right — report it.