opaqueAPI

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
  1. YoursYour app or agentCalls the SDK, or the MCP server's tools.
  2. On your machineOpaque SDK · MCP serverThe account key, the notes, the ring proofs. Nothing that can spend leaves this process.
  3. Opaque's networkSix relays, three hopsEvery read and payment onion-routed: the services learn what was asked, never who asked it.
  4. 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.

WalletsA private balance beside the public one. Your users deposit, send and swap without their history on display.
FintechsPayouts, payroll and supplier payments that don't publish who paid whom, or how much you hold.
StablecoinsPrivate transfers for holders of USDC, EURC, RLUSD and tokens on the XRP Ledger, from pools of each asset.
AgentsAn MCP server: an agent pays, deploys contracts and funds deployers from a budget of its own, within limits you set.

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 });
Payments are whole notes. Each amount is made from notes of fixed sizes, spent whole with no change, each with its own proof (a few seconds) and upload. 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.

CallWhat 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.
Why an account each. An Arc account signs with few-time post-quantum keys and keeps a log of every signature. Two programs signing for one account could use a key twice, so the SDK keeps one process per wallet, and you keep one host per account.

Arc

USDC, in private notes. The account is an ERC-4337 account whose key is post-quantum (FORS+C), checked on chain.

CallWhat it does
arc.addressThe 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.

CallWhat 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.

CallWhat 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"] } } }
  1. It runs on your machine, over stdio. The agent sees amounts, addresses and handles. It never sees a key, a note or a phrase.
  2. Its account is its budget. Fund it with what you'd let the agent spend. Your own wallet's accounts are never in reach.
  3. 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

ToolWhat the agent can do
wallet_infoNetwork, Arc address, limits and what's been paid today
arc_balance, arc_depositRead the balance; turn public USDC into notes
arc_send, payment_status, arc_activityPay privately, within limits; follow it to settlement
get_deployer, fund_deployerA fresh deployer address; fund it privately
get_deployer_keyWrites 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_riskDeploy 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_swapSwap tokens at the address through LI.FI; larger swaps ask you
arc_withdrawEverything at the address to one place; always asks you
xrpl_balance, xrpl_deposit_address, xrpl_send, xrpl_activityThe same on the XRP Ledger

Limits and approval

Kept in the agent's wallet, under its password: its tools read them, never change them.

AssetPer paymentPer dayAsks you above
arc:USDC10505
xrpl:XRP2010010
xrpl:RLUSD, xrpl:USDC10505
  • An asset with no limit can't be paid. A list of recipients narrows 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.json changes them, with the password. opaque-mcp deployer-key 0 shows a deployer's key in your terminal, even while the server runs.
The boundary. Limits stop an agent that's been misled through its tools. An agent that can also run commands as you (Claude Code can) can read what you can: keep the password in a keychain entry that asks you, and fund the agent's account with only what you'd let it spend. That account is what caps the loss.

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

WhatWho can see it
Keys, note secrets, proofs before they're sentYour process only
Which note paidNobody: one of eight of the same size
A deposit, and a payment's recipient and amountEveryone, on chain: a payment comes from the pool
Your server's IPThe first relay. The services see the last one
What a read asks aboutThe 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.