Opaque API
Private payments, inside your app.
The SDK and MCP server behind the Opaque wallet. Give your users, your treasury or your agents private, post-quantum payments on Arc and the XRP Ledger. The keys and the proofs run on your machine, never on ours.
- Wallets
- Fintechs
- Stablecoins
- Agents
- YoursYour app or agentCalls the SDK, or the MCP server's tools.
- On your machineOpaque SDK · MCP serverThe account key, the notes, the ring proofs. Nothing that can spend leaves this process.
- Opaque's networkSix relays, three hopsEvery read and payment onion-routed: the services learn what was asked, never who asked it.
- On chainArc · XRP LedgerPaid from a shared pool, through a ring of eight. Nothing on chain points back to the sender.
Who it's for
The same private payments the wallet makes, from your code.
Quickstart
Node 22.18 or later. A wallet is a folder: an encrypted file holding one account, opened by one process at a time.
import { createWallet } from '@opaque/sdk';
const wallet = await createWallet({ dir: './.opaque', password: process.env.OPAQUE_PASSWORD, network: 'testnet' });
const arc = await wallet.arc();
console.log(arc.address); // send USDC here (faucet.circle.com on testnet)
await arc.deposit(10); // 10 USDC → private notes, one signature
const [payment] = await arc.send('0x5e2b…', 5); // through a ring of 8, across the relays
console.log(await arc.status(payment.handle)); // … { state: 'SETTLED' }
The same wallet on the XRP Ledger:
const xrpl = await wallet.xrpl();
const { xAddress } = await xrpl.address('XRP'); // payments to it become notes
await xrpl.send('XRP', 25, 'r…recipient', { tag: 12 });
send returns once they're submitted; settlement follows within seconds to minutes.Wallets and accounts
One wallet holds one account. Give each app or agent an account of its own.
| Call | What it does |
|---|---|
createWallet({ dir, password, network }) | A new account, sealed in dir under password (scrypt, AES-256-GCM). Or bring one: accountKey: 'opq1…', or phrase and account. |
openWallet({ dir, password }) | Opens it again. A second process is refused while one holds it. |
wallet.accountKey() | The account's key, opq1…: its backup, and whoever holds it can spend it. |
wallet.data.get(name), .set(name, value) | Your own records, sealed in the same file. |
wallet.close() | Lets the wallet go, for another process to open. |
Arc
USDC, in private notes. The account is an ERC-4337 account whose key is post-quantum (FORS+C), checked on chain.
| Call | What it does |
|---|---|
arc.address | The account's public address: send USDC here, from anywhere. |
arc.balance() | { private, waiting, notes, public } in USDC. waiting: notes whose pool doesn't hold a full ring yet. |
arc.deposit(usdc) | The address's USDC into notes, in one operation the account's key signs. Public, like any deposit. |
arc.send(to, usdc, { waitForPrivacy, deadline }) | A private payment from notes. Returns a handle per note. |
arc.status(handle) | Where a payment is: WAITING_FOR_PRIVACY … SETTLED or FAILED. |
arc.activity() | Payments sent, newest first. |
arc.swapQuote(from, to, amount), arc.swap(…) | Tokens at the address, through LI.FI. The route is asked across the relays and checked before the key signs: this account receives at least the minimum shown. |
arc.withdraw(to) | Everything at the address (not the notes) to to. |
arc.rescan() | Finds notes of this account made on another host. |
XRP Ledger
XRP, RLUSD, USDC and an MPT, in private notes. Testnet today.
| Call | What it does |
|---|---|
xrpl.address(symbol) | { xAddress, account, tag }: a reusable deposit address. What's paid to it becomes this account's notes. |
xrpl.balance(symbol) | { amount, notes }, checked against the ledger. |
xrpl.send(symbol, amount, to, { tag, waitForPrivacy, deadline }) | A private payment to any XRPL account, each note through a ring. |
xrpl.settled(sent) | Waits until every note of a payment has an outcome. |
xrpl.swapQuote(from, to, amount), xrpl.swap(…) | A private swap through the ledger's AMMs: notes in, fresh notes of the other asset out, nothing linking the two. |
xrpl.activity(), xrpl.recover() | Payments sent; deposits found since the last look. |
Deployers and contracts
Deploy without tying the contract to your treasury, or deploy from an account no classical key controls.
| Call | What it does |
|---|---|
arc.nextDeployer(), arc.deployer(i) | { index, address, key }: a plain Arc key for forge or hardhat, drawn from the account. One per project. |
arc.fundDeployer(i, usdc) | Pays it privately: on chain, the pool funded it. |
arc.deploy(initcode, salt?) | Deploys from the account itself through the CREATE2 factory, signed by its post-quantum key. { address, tx }, the address known before it's sent. |
arc.call(to, data) | Calls a contract from the account. One that hands its tokens over (approve, transfer, permit) is refused unless { allowRisk: true }. |
arc.riskOf(to, data) | What a call would hand over, in words, or null. |
const d = await arc.nextDeployer();
await arc.fundDeployer(d.index, 2); // private: nothing on chain ties it to you
// PRIVATE_KEY=d.key forge script Deploy --rpc-url https://rpc.testnet.arc.io --broadcast
A deployer's key is an ordinary key, not a post-quantum one: fund it with what a deploy costs. A contract that must outlive a quantum computer is deployed with arc.deploy and owned by the account.
MCP server
Opaque for AI agents: a local MCP server holding a wallet of the agent's own.
opaque-mcp init # an account of the agent's own, with limits
security add-generic-password -s opaque-mcp -a "$USER" -T "" -w # its password, in your keychain
claude mcp add opaque -- opaque-mcp serve --password-command "security find-generic-password -s opaque-mcp -w"
Any MCP client starts it the same way:
{ "mcpServers": { "opaque": {
"command": "opaque-mcp",
"args": ["serve", "--password-command", "security find-generic-password -s opaque-mcp -w"] } } }
- It runs on your machine, over stdio. The agent sees amounts, addresses and handles. It never sees a key, a note or a phrase.
- Its account is its budget. Fund it with what you'd let the agent spend. Your own wallet's accounts are never in reach.
- It pays within limits you set, and asks you in the client before anything larger.
With -T "", the keychain asks you each time anything reads the password: once when the server starts, and again if the agent ever tries. Never put the password in the client's config file, where an agent that runs commands can read it.
Tools
| Tool | What the agent can do |
|---|---|
wallet_info | Network, Arc address, limits and what's been paid today |
arc_balance, arc_deposit | Read the balance; turn public USDC into notes |
arc_send, payment_status, arc_activity | Pay privately, within limits; follow it to settlement |
get_deployer, fund_deployer | A fresh deployer address; fund it privately |
get_deployer_key | Writes PRIVATE_KEY into a git-ignored .env only you can read, after you approve. The key never enters the conversation. |
arc_deploy, arc_call, contract_risk | Deploy and call from the post-quantum account. A call that hands tokens over, or any call to a contract the agent didn't deploy, asks you first |
arc_swap_quote, arc_swap | Swap tokens at the address through LI.FI; larger swaps ask you |
arc_withdraw | Everything at the address to one place; always asks you |
xrpl_balance, xrpl_deposit_address, xrpl_send, xrpl_activity | The same on the XRP Ledger |
Limits and approval
Kept in the agent's wallet, under its password: its tools read them, never change them.
| Asset | Per payment | Per day | Asks you above |
|---|---|---|---|
arc:USDC | 10 | 50 | 5 |
xrpl:XRP | 20 | 100 | 10 |
xrpl:RLUSD, xrpl:USDC | 10 | 50 | 5 |
- An asset with no limit can't be paid. A list of
recipientsnarrows who can be paid, besides the agent's own deployers (one whose key was written out counts as anyone else). - Account operations (deposit, deploy, call, swap, withdraw) are capped too: 20 a day unless you set
operationsPerDay. Each costs gas and one of the key's signatures; the key rotates itself before it runs out. - Above asks you above, the server asks you in the client (MCP elicitation), and the agent can't answer for you. A client that can't ask gets a refusal.
opaque-mcp policy --set policy.jsonchanges them, with the password.opaque-mcp deployer-key 0shows a deployer's key in your terminal, even while the server runs.
The network
What the SDK talks to, so you don't have to.
Every read and payment crosses three of Opaque's six relays, onion-wrapped (ML-KEM-768, AES-256-GCM) and padded to a fixed size, to an exit that answers queries and takes payments. Each relay's key comes from a directory signed down from a root built into the SDK, never from the network. Proofs are about 1 MB and go in chunks.
There is no API key. A key would tie every request to the company that sent it, and through it to their users. Rate limits and proof of work keep the services up instead.
Who sees what
| What | Who can see it |
|---|---|
| Keys, note secrets, proofs before they're sent | Your process only |
| Which note paid | Nobody: one of eight of the same size |
| A deposit, and a payment's recipient and amount | Everyone, on chain: a payment comes from the pool |
| Your server's IP | The first relay. The services see the last one |
| What a read asks about | The exit, but not who asked |
Opaque runs every relay today, so Opaque could link them; the docs say what else holds and doesn't.
Access
The SDK and MCP server are licensed to companies building wallets, payments, stablecoins and agents. Build on testnet first; Arc mainnet is live for USDC.
