Build/Integrations

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.

Integration tiers4receive → fully local
Crypto to implementnonesigner WASM + SDK
Memo size512 Bencrypted, signed
Addresses per wallet1attribute by memo
Seed phrasesnoneraw 32-byte seed

Pick a tier

TierWhat the user getsWhat you run
Receive + viewan address, a balance, incoming historykeys on device, a walletd registered with the viewing key
Full non-custodialthe above plus sending, seed never leaves the devicethe same, plus the signer WASM for on-device verify-and-sign
Fully localthe above with nothing outside seeing even a viewing keywalletd embedded in your app as a library; only a node is external
Custodialyou hold the fundswalletd with the seed, plus a payout strategy — see exchange

Add ZKAS to your wallet

Three ready-made pieces, no cryptography to write:

PieceRole
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-walletdkeyless daemon that scans, proves and broadcasts — hosted, or run your own
Trust model in one line. You send the daemon a viewing key so it can watch; only the seed can spend; and the device re-checks and signs every payment, so a hostile daemon can neither redirect funds nor inflate the fee.
Device seed · ask can spend cannot prove recomputes the sighash and checks the disclosure Daemon fvk · proving key can watch and prove cannot spend builds the bundle, signs only its dummies Chain verifies the proof, every spend-auth signature and the binding signature all or nothing fvk bundle + sighash + disclosure spend_auth_sig broadcast
The daemon proves what it cannot authorize; the device authorizes what it cannot prove. A compromised daemon leaks visibility into that wallet, never its coins — provided the device recomputes the sighash from the bundle instead of trusting the one it was handed.
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_hex only.
  • Refuse a daemon that changed the amount. Compare amount_sompi_exact to what you asked for, and require remaining_sompi_exact === 0 on the single-transaction path.
  • Cap the fee before signing, not after.
  • Recompute the sighash from bundle_hex. The returned sighash is a convenience; a compromised daemon could lie about it, and blind-signing lets it authorize a payment to an attacker.
  • Verify the bundle against disclosure so 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.
Fully local option. The Android app runs 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

Read this before designing anything. One wallet has exactly one address. /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
  1. 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.
  2. Credit deposits by memoRead /api/wallet/history and match the memo to a customer. Memos are readable per transaction on the receive side, up to 512 bytes.
  3. Gate withdrawals on spend_readyNot on synced. And treat missing_history:true as "this balance is a lower bound" — never as final.
  4. Consolidate continuouslyWithdrawal latency is 2.4 s × notes spent ÷ cores. Leave --auto-consolidate on, and issue one call for the full amount rather than splitting by hand.
  5. 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.
Proof of reserves is scoped disclosure, not zero-knowledge. You hand an auditor the viewing key of a dedicated reserve seed. That reveals every flow of that seed permanently and irrevocably, so never use a seed holding anything else.

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_...
StatusMeaning
newno payment received
partialsome value received, not enough
paidfull amount detected, confirmation policy not yet reached
confirmedfull amount received and confirmed
overpaidmore than requested received
expiredunpaid invoice expired
paidLatepayment arrived after expiry
Fulfil an order only for 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.

Non-negotiables. One unique gateway address per invoice · never expose the merchant API key in frontend JavaScript · store the invoice id against your order id · use a unique, stable 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.

The plumbing is already here. ZKas builds payments as a PCZT — 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.
The privacy cost you cannot engineer away. Proving a spend needs the spender's full viewing key — the circuit takes 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

Theft
Impossible. There is no partial state — the whole bundle confirms or nothing moves.
Stalling
Costs time, not money. If one party never signs, no funds move; your wallet may soft-lock the selected notes.
Double-spend race
The real residual risk. A counterparty can sign your bundle and also spend the same note elsewhere; whichever confirms first wins and the loser is dropped with its fee uncollected. Denial, not loss.
Signing window
Not unlimited. The shared anchor must be matured (~10 min) and used before it exceeds the maximum age (~7.5 h), after which consensus drops the transaction.

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

A default send is unprovable — even to the sender. The 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/prepare and store the returned disclosure — spend_value, out_value, out_recipient, out_rseed, rcv per action. A verifier recomputes cmx and cv_net from it and matches them against the on-chain action. No keys needed. There is no flag on /api/wallet/send that 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:

  1. 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.
  2. Send a value-0 note to that addressWith your protocol data in the memo. A zero-value note is a real note.
  3. Indexers recover it with scan_bundle(ivk, bundle)Integrity comes free from the signature.
Authenticated is not authorized. Signatures stop anyone altering your data, not posting it — and the sender is shielded, so an indexer cannot tell who wrote a memo. Make the announcement action spend the note that represents the object; the nullifier then proves the poster controlled it, inside the same signed bundle.

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-pay CLI
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
Verified against 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.