Skip to main content

Client SDK - OMap

The OMap client is the Merces client used on the Plasma and Stable deployments. It runs entirely in your application: it generates proofs locally, holds the signing key, and talks to the gateway, the ID registry and the MPC nodes on your behalf. Balances and amounts never leave it in the clear.

It exposes two privacy modes and the yield vault, neither of which the Monad and Worldchain client offers.

Which client do I need?

Pick by deployment. Plasma and Stable use this client, @taceo/merces2-client. Monad and Worldchain use @taceo/merces-client, which has a different API. They are not interchangeable.

Testnet

Plasma and Stable are testnets using test tokens with no real-world value. Never use mainnet keys or real funds against them.

Note that - depending on the stability of your chosen RPC provider - you may have to add retries to various calls.

Endpoints and addresses

Three services sit in front of Merces on these deployments. Plasma and Stable share the same service endpoints:

ServiceURL
ID registryhttps://id-registry.merces2.taceo.io
Gatewaywss://gateway.merces2.taceo.io
Orchestratorhttps://orchestrator.merces2.taceo.io

The vault contract differs per deployment, and exists only on these two:

DeploymentVault
Plasma0x3D2B3bC89c8D07Bb46C755bd8fde7D4271cf40d7
Stable0xE67b5933CA36273C17b7384a96bEBc2c340c9093

Node URLs and the Merces, ID registry and token contract addresses are on Deployments & status.

Note that the ID registry appears twice in a deployment's configuration, as two different things: the service URL above, and the ID registry contract address on the Deployments page. This client needs both.

Requirements

Node.js 20 or later, a wallet funded on the chain you are targeting, the endpoints and vault address above, and the Merces, ID registry and token contract addresses for that deployment.

The package is ESM-only. It has no CommonJS build, so it cannot be require()d — use import, or a bundler that handles ESM.

Install

npm install @taceo/merces2-client viem

The package bundles its own proving artifacts — the circuit wasm and the zkey ship inside it — so there is nothing else to download before you can generate a proof.

Create a client

import { createWalletClient, createPublicClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { Client } from '@taceo/merces2-client';

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const walletClient = createWalletClient({ account, chain, transport: http(RPC_URL) });
const publicClient = createPublicClient({ chain, transport: http(RPC_URL) });

const client = new Client({
idRegistryUrl: 'https://id-registry.merces2.taceo.io',
gatewayUrl: 'wss://gateway.merces2.taceo.io',
nodeUrls: [NODE_0_URL, NODE_1_URL, NODE_2_URL],
contractAddress: MERCES_ADDRESS,
tokenAddress: TOKEN_ADDRESS,
vaultAddress: VAULT_ADDRESS,
token: 'EIP3009',
walletClient,
publicClient,
});
warning

This requires you to pass your private key to the SDK. Use testing wallets or secure the key/environment variable in some manner.

Configuration

OptionRequiredNotes
idRegistryUrlYesMaps wallet addresses to their index in the tree
gatewayUrlYesWebSocket endpoint that submits transactions and reports completion
nodeUrlsYesThe MPC nodes. Shares are encrypted to their public keys
contractAddressYesThe Merces contract on your chain
tokenAddressYesThe ERC-20 being wrapped
vaultAddressYesRequired even if you never use the vault
tokenYes'ERC20' or 'EIP3009'. Both deployments use 'EIP3009'
gatewayTimeoutNoWait for the gateway's queued and completed messages. Default 30,000 ms
txReceiptTimeoutNoWait for a transaction receipt. Default 30,000 ms
mpcEventTimeoutNoWait for the on-chain ProcessedMPC event. Default 30,000 ms

client.address() returns the address the client is operating as.

Register

An address must be registered before it can hold a balance:

const index = await client.register();

This derives the client's key from a wallet signature and registers the public key against your address, returning your index in the tree. It throws RegisterError if registration fails, and the derivation is deterministic — the same wallet always produces the same key.

Privacy modes

Every operation comes in two variants. Which you choose determines what the transaction reveals and how fast it settles — see Privacy model for what each mode hides, from whom, and the throughput each sustains.

The method names predate the terminology on that page:

Method prefixMode
private*Private (default)
confidential*Partial-private

Graph-private transfers are not exposed by this client.

Pick per transaction — an account can use both.

Deposit

Deposits move tokens from the public balance into the Merces balance. The amount is visible onchain either way, since tokens are moving; the mode determines whether the crediting account is hidden.

import { parseUnits } from 'viem';

const decimals = await client.getDecimals();
const amount = parseUnits('100', decimals);

await client.confidentialDeposit(amount);
// or
await client.privateDeposit(amount);

getSymbol() and getName() are also available for token metadata.

Balances

const onchain = await client.getErc20Balance(); // public
const merces = await client.getPrivateBalance(); // reconstructed from the node shares
const vault = await client.getVaultBalance(); // vault shares

Transfer

await client.confidentialTransfer(RECEIVER_ADDRESS, amount);
// or
await client.privateTransfer(RECEIVER_ADDRESS, amount);

The receiver must already be registered. Transferring to your own address throws CannotTransferToSelfError.

Withdraw

await client.confidentialWithdraw(amount);
// or
await client.privateWithdraw(amount);

A withdrawal ends in an ordinary token transfer, so its amount and destination are visible onchain — but in private mode nothing links it to any particular transfer.

Vault

The vault is an ERC-4626 yield vault. Deposits and withdrawals are private: the holder is carried as a secret-shared identity reference, so positions are not attributable.

await client.vaultDeposit(amount);
await client.vaultWithdraw(amount);

const shares = await client.convertToShares(assets);
const assets = await client.convertToAssets(shares);

getVaultBalance() returns your holding in shares; convertToAssets turns that into the underlying token amount. See Private yield.

Transaction history

Two functions, for two different jobs. Both are standalone imports rather than methods on Client, and both take the ID registry and orchestrator URLs explicitly.

getTxHistory returns the account's own decrypted history:

import { getTxHistory } from '@taceo/merces2-client';

const history = await getTxHistory(
'https://id-registry.merces2.taceo.io',
'https://orchestrator.merces2.taceo.io',
client.address(),
);

getTxList returns a paginated list with a total count, for building a ledger view:

import { getTxList } from '@taceo/merces2-client';

const { transactions, total } = await getTxList(
'https://id-registry.merces2.taceo.io',
'https://orchestrator.merces2.taceo.io',
{ page: 1, pageSize: 25 },
);

Both accept optional startTime and endTime bounds. Results are a tagged union across the eight transaction kinds — private and confidential deposit, withdraw and transfer, plus vault deposit and withdraw — so narrow before reading fields:

for (const tx of history) {
if ('PrivateTransfer' in tx) {
console.log(tx.PrivateTransfer.amount, tx.PrivateTransfer.time_stamp);
}
}

getTxHistory decrypts to plain values. getTxList returns entries where private fields are still secret shares, so use it for counts and pagination rather than for reading amounts.

Errors

ErrorMeans
RegisterErrorRegistration failed
NotFoundErrorThe address is not registered
InvalidAmountErrorThe amount is zero or negative
InsufficientBalanceErrorNot enough balance for the operation
CannotTransferToSelfErrorSender and receiver are the same account
ProofErrorProof generation failed
InvalidTransactionErrorThe MPC network marked the transaction invalid
TimeoutErrorOne of the configured timeouts elapsed
import { TimeoutError, InsufficientBalanceError } from '@taceo/merces2-client';

try {
await client.privateTransfer(receiver, amount);
} catch (err) {
if (err instanceof InsufficientBalanceError) {
// ...
} else if (err instanceof TimeoutError) {
// may still have been processed — see below
} else {
throw err;
}
}

A TimeoutError is not a failure signal. The transaction may still be processed — check the balance or the history before retrying, or a double spend becomes possible.

Reference

The package ships TypeScript definitions, so an editor gives you full signatures inline.