API reference
Complete ClusterAgent client API — connect, sessions, key shares, presignatures, recovery bundles. Every method, every option.
API reference — ClusterAgent client
ClusterAgent is the lower-level client in @zafeguard/mpc-sdk. It exposes the full HTTPS surface of an MPC Agent cluster node so backends can drive ceremonies directly (no embedded device share). Pair this page with Integration patterns for context on when to reach for ClusterAgent over EmbeddedAgent.
import { ClusterAgent } from '@zafeguard/mpc-sdk';
const agent = ClusterAgent.connect(host, port, apiKey, /* tls */ true);Direct HTTP access
ClusterAgent is a thin wrapper over the node's REST API. If you're integrating from a stack other than TypeScript, call that API directly.
- Authentication. Every route except
/health,/ready, and/metricsrequires the API key, sent as eitherAuthorization: Bearer <key>orx-api-key: <key>. (Authorization: ApiKey <key>is not accepted.) - Full spec. The node serves its own OpenAPI spec and Swagger UI — set
swagger.enabled: truein the config, then open/swagger-ui(raw JSON at/swagger-ui/openapi.json). That spec is the authoritative list of routes, request/response schemas, and enum values.
A few wire-format details that are easy to miss when hand-rolling requests:
- Mixed casing in one body. Request bodies are camelCase (
keyId,maxShares,messageHashHex), but each entry ofpeers[]is snake_case:node_id,index,ed25519_pubkey,x25519_pubkey. - Curve values are uppercase on the wire —
SECP256K1,ED25519,P256, … (the all-lowercase form is also accepted; the mixed-caseSecp256k1is not). peers[]is the full cohort. Its length must equalmaxSharesand it must include the node you're calling. Each peer'sx25519_pubkeycomes from that peer'sGET /node/info.
The method reference below uses the SDK's camelCase field names throughout; the SDK handles the casing and header details for you.
ClusterAgent.connect(host, port, apiKey, tls?)
static connect(host: string, port: number, apiKey: string, tls = false): ClusterAgentOpen a client against a single agent node. Use tls: true for the HTTPS interface (the only safe option in production); false runs plain HTTP and is appropriate only for local-dev.
The returned ClusterAgent is reusable across many calls; one instance per (host, port) is enough.
agent.node — health + identity
agent.node.info(): Promise<AgentNodeInfo>
agent.node.health(): Promise<AgentHealthResponse>
agent.node.ready(): Promise<AgentReadyResponse>| Method | Description |
|---|---|
info() | Static identity: nodeId, ed25519Pubkey, x25519Pubkey, license summary. Use for pinning peer keys before DKG. |
health() | Liveness probe — { status: 'ok' } when the process is running. |
ready() | Readiness probe — { status: 'ok' } once the DB schema has migrated, the intra-cluster transport is connected, and the license is verified. |
agent.sessions — ceremony orchestration
agent.sessions.createKeyShare(req: CreateKeyShareSessionRequest): Promise<Ceremony>
agent.sessions.createPresignature(req: CreatePresignatureSessionRequest): Promise<Ceremony>
agent.sessions.createSignMessage(req: CreateSignMessageSessionRequest): Promise<Ceremony>
agent.sessions.createReshare(req: CreateReshareSessionRequest): Promise<Ceremony>
agent.sessions.list(phase?: string): Promise<AgentSessionResponse[]>
agent.sessions.get(id: string): Promise<AgentSessionResponse>
agent.sessions.cancel(id: string): Promise<void>
agent.sessions.auditLogs(opts?: {...}): Promise<AgentAuditLogResponse[]>The four create* methods kick off the four ceremony kinds. Each returns a Ceremony handle:
const session = await agent.sessions.createKeyShare({ /* ... */ });
// One-shot snapshot of the initial response:
const initial = session.raw();
// Poll until phase reaches Complete / Failed / Cancelled:
const terminal: AgentSessionResponse = await session.untilTerminal();
// Cancel mid-ceremony:
await session.cancel();untilTerminal() resolves with AgentSessionResponse, the same shape returned from agent.sessions.get(id). Key fields:
| Field | Type | When set | What it carries |
|---|---|---|---|
id | string | Always | Cluster-assigned session UUID. Use this as the sessionId for any follow-up cancel / auditLogs query. |
phase | string | Always | Current phase — 'Initialising' | 'AwaitingPeerCommitments' | 'AwaitingPeerShares' | 'Reconstructing' | 'Complete' | 'Failed' | 'Expired' | 'Cancelled'. Branch on this. |
publicKeyHex | string | After 'Complete' for DKG / reshare sessions | The wallet's root public key hex. For DKG, this is the only handle you get back — keyShareId is NOT present in the DKG session response. Look up the local keyShareId separately via agent.keyShares.list() filtering by this publicKeyHex. |
keyShareId | string | After 'Complete' for presign / sign sessions (NOT DKG) | Cluster-assigned share id the presign / sign ceremony ran against. See keyId vs keyShareId below. |
partialSignatures | string[] | After 'Complete' for sign sessions | Threshold-many base64 PSIG envelopes. Combine via Scheme.finalizeSignature(...) or Utils.finalizeSignature({...}). |
error | string | null | After 'Failed' | Human-readable failure reason. |
For DKG specifically, the public key is the canonical handle: terminal.publicKeyHex is the wallet's root public key. Look up the cluster's internal keyShareId with agent.keyShares.list(), filtering by that publicKeyHex. From then on, every presign / sign / export call uses keyShareId, not keyId.
keyId vs keyShareId
These are two distinct identifiers — confusing them is the most common cluster-side integration mistake:
| Identifier | Set by | Stable across | First appears on |
|---|---|---|---|
keyId | The caller, at createKeyShare time. | The cohort. Every agent stores the same keyId for this wallet. | CreateKeyShareSessionRequest.keyId |
keyShareId | The cluster, after DKG finalises. | The local agent only — every cluster node has its own keyShareId for the same wallet. | AgentKeyShareResponse.id (returned from agent.keyShares.list()) |
Use keyId as your durable application-level identifier (the value you'd save next to a user record). Use keyShareId when calling any agent method that operates on the local share record — presign, sign, export, delete. The two values are linked by the wallet's publicKeyHex, which both expose.
| Request | Required fields |
|---|---|
CreateKeyShareSessionRequest | keyId, curve, threshold, maxShares, peers[]. Optional nizkProof, policyContext, ttlSecs. |
CreatePresignatureSessionRequest | keyShareId, peers[]. Optional sourceDkgSessionId (recommended), policyContext, ttlSecs. |
CreateSignMessageSessionRequest (one-shot) | keyShareId, peers[], messageHashHex. Optional derivationPath, partialSigExpiresInSecs. |
CreateReshareSessionRequest | keyId, curve, threshold, maxShares, myIndex, peers[] (new set), reshareOldPeers[], reshareOldThreshold. Optional reshareMyOldIndex, sourceDkgSessionId, existingPublicKeyHex. |
createSignMessage runs presigning + signing back-to-back inside the same ceremony — use it for one-shot signing with no presignature pool. To sign against a pre-minted presignature, call agent.presignature.sign(presigId, req) against each cluster node directly: every node holds its own copy of the presignature locally and produces its partial signature independently, so no multi-party ceremony is required. See Hot Wallet — cluster signing flow for the decision guidance.
agent.sessions.list(phase) filters by current phase ('Initialising', 'AwaitingPeerCommitments', 'AwaitingPeerShares', 'Reconstructing', 'Complete', 'Failed', 'Expired', 'Cancelled').
Audit logs — for compliance, forensics, and SIEM export
agent.sessions.auditLogs({ sessionId?, limit?, eventType?, failuresOnly? }) returns the complete event trail for one session (or every session within limit). Each row is an AgentAuditLogResponse carrying the session id, event type, the agent node id that emitted it, the wall-clock timestamp, and a free-form payload describing the event (DKG round transitions, partial-signature production, ceremony completion, every peer round-trip).
Use it for:
- Regulated-custody audit trails (MiCA / SOC 2 / PCI-DSS). Every signing ceremony writes a row. Export to your WORM-compliant storage on a cadence (hourly, daily) and you have a 5+ year audit trail that survives vendor changeover. Query
eventTypeto filter for'SignMessageCompleted'and similar terminal events when assembling the auditor-facing report. - Live SIEM streaming. Poll the log tail by passing
limit+ a since-timestamp filter (track the lastidyou observed). Forward each new row into Splunk / Datadog / your SIEM of choice. The agent'snodeIdplus the row's hash chain form the cryptographic evidence chain — auditors verify the log is unforged by replaying the per-event signatures against the agent's published node-info pubkey fromagent.node.info(). - Failure forensics.
failuresOnly: truefilters to ceremonies that ended inFailed/Expired/Cancelled. Pair withagent.sessions.get(id)to fetch the terminalerrorstring for each.
// Export the last 24h of audit events to a WORM bucket, signed by the agent.
const now = Date.now();
const dayMs = 24 * 60 * 60 * 1000;
const rows = await agent.sessions.auditLogs({ limit: 100_000 });
const recent = rows.filter((r) => Date.parse(r.timestamp) > now - dayMs);
await wormBucket.put(`audit/${now}.jsonl`, recent.map(JSON.stringify).join('\n'));For multi-region custody with multiple cluster nodes, query each ClusterAgent independently — every node maintains its own local audit log; the union is the cluster-wide view.
Cluster-wide reshare orchestration
agent.sessions.createReshare(...) is the cluster-side counterpart to loaded.reshare() from EmbeddedAgent. Use it when the cohort is entirely cluster-held (no device party) and you need every cluster node's share to rotate in one coordinated ceremony — typical after an agent compromise, an API-key rotation event, or a scheduled hygiene rotation.
CreateReshareSessionRequest takes keyId, curve, the new threshold / maxShares (which can match the old ones for a same-shape rotation, or differ for topology change), myIndex (this node's slot in the new cohort), and peers[] (the new set of cohort URLs + api keys). reshareOldPeers[] and reshareOldThreshold describe the existing cohort the rotation runs against; the SDK orchestrates the cross-set ceremony in one call.
The wallet's publicKeyHex is preserved by design — the same on-chain address signs after rotation. Until untilTerminal() resolves with 'Complete', every old share remains valid; on Complete, the agents atomically swap to the new shares and any subsequent presign/sign against the old shares rejects.
Fleet rotation. For a wallet platform managing tens of thousands of cluster-held wallets, the SDK provides per-wallet reshare only — orchestration (queueing, retries, per-wallet idempotency markers, agent rate-limit caps) is the integrator's job. A reasonable shape: one job per keyId, a reshare_state column tracking progress, workers loading the wallet → calling createReshare → persisting the new publicKeyHex confirmation under a per-wallet lock, and a fleet-wide concurrency cap matched to the agent's per-second presign throughput.
agent.keyShares — share lifecycle
agent.keyShares.list(limit?: number): Promise<AgentKeyShareResponse[]>
agent.keyShares.get(id: string): Promise<AgentKeyShareResponse>
agent.keyShares.delete(id: string): Promise<void>
agent.keyShares.export(id: string, opts: ExportKeyShareOptions): Promise<AgentExportKeyShareResponse>
agent.keyShares.import(req: ImportKeyShareRequest): Promise<unknown>keyShares.export
Export a key share as a sealed portable blob for migration to another agent (different cluster, different license).
{
cryptoMode: 'RSA' | 'P256';
// Required when cryptoMode === 'RSA':
publicKeyPem?: string; // PKCS#8 PEM, ≥ 2048-bit RSA pubkey
// Required when cryptoMode === 'P256':
userPublicKeyBytes?: Uint8Array; // P-256 public point (65-byte uncompressed SEC1)
}RSAmode produces an RSA-OAEP+AES-GCM sealed blob. The standard choice for inter-cluster migration.P256mode produces a P-256 ECIES sealed blob. Used for iOS Secure Enclave handoff where the recipient's key lives in the secure element and exposes only its P-256 public point.
The blob is license-bound: it can only be imported on an agent holding the SAME license cert. For license-independent recovery (cold backup, cross-tenant migration), use exportRecoveryBundle below.
keyShares.import
Import a key share via the forward-secret handshake. The keyShare field in req must be encrypted with the ephemeral server pubkey from agent.importHandshake(). The migrateKeyShare helper wraps the full export → handshake → wrap → import flow into one call — prefer that to manual orchestration.
agent.presignature — presig lifecycle
agent.presignature.list(opts?: { publicKeyHex?, limit? }): Promise<AgentPresignatureResponse[]>
agent.presignature.get(id: string): Promise<AgentPresignatureResponse>
agent.presignature.delete(id: string): Promise<void>
agent.presignature.export(id: string, keyShareId: string, publicKeyPem: string): Promise<AgentExportPresignatureResponse>
agent.presignature.import(req: ImportPresignatureRequest): Promise<unknown>
agent.presignature.sign(presigId: string, req: SignRequest): Promise<AgentSignResponse>presignatures.sign
The canonical sign path. Consumes one presignature and returns THIS node's partial signature for the given message hash:
const partial = await agent.presignature.sign(presigId, {
messageHashB64: Buffer.from(messageHash).toString('base64'),
derivationPath: [], // optional BIP-32 path
recipientPublicKeyPem: undefined, // optional RSA pubkey for end-to-end encryption
});Returns { presigId, keyShareId, publicKeyHex, partialSignatureB64, isEncrypted, expiresAt }.
partialSignatureB64is base64 of either the cleartext PSIG envelope (whenrecipientPublicKeyPemwas omitted) or an RSA-OAEP+AES-GCMmpcblobwrapping it (when supplied).- Branch on
isEncrypted. For the encrypted path, pipe throughPartialSignatures.unwrap(...)to get back to the cleartext base64. - Feed the cleartext base64 directly into
Scheme.finalizeSignature(...)after collectingthreshold-many partials — the cluster never finalizes; the client combines locally.
presignatures.export / presignatures.import
Per-presig migration between clusters. Same handshake pattern as keyShares.import; the migratePresignature helper wraps the full flow.
agent.exportRecoveryBundle(args) — cold recovery
agent.exportRecoveryBundle({
keyShareId: string;
rsaPublicKeyPem: string; // recipient's RSA pubkey
peers: Array<{
agentUrl: string;
apiKey?: string;
keyShareId: string;
}>;
}): Promise<Uint8Array[]>Asks THIS agent + every supplied peer for their per-node sealed shard, then returns the array of bundles (one entry per participating agent, this agent first). Differs from keyShares.export:
- Multi-party. Requires ≥
thresholdparticipating agents. Partial success is rejected by design — if fewer thanthresholdpeers respond, the promise rejects with"export-recovery-bundle: only N of M peers responded (need at least T)"and no partial state is returned. List ALL surviving agents inpeers[]; the cluster collects shards from whoever responds and rejects the call only when the count drops belowthreshold. - Not license-bound. The recovered private key works without a live agent or active license — designed for off-line cold recovery in disaster scenarios.
RecoveryBundle.recover(...)also runs license-free, so the air-gapped recovery device does not need to callMpc.init(...)first.
Combine the bundles via RecoveryBundle.finalize(...) then unseal with RecoveryBundle.recover(...) to get back the root private key. See Embedded Agent — Cold recovery for the full walkthrough.
agent.importHandshake()
agent.importHandshake(): Promise<AgentHandshakeResponse>Allocate a one-shot ephemeral RSA-2048 keypair on the server for a single import operation. The handshake is consumed atomically on first import and expires after 60 seconds. Used internally by migrateKeyShare / migratePresignature; callers normally don't invoke directly.
Cross-cluster migration
Moving a key share or a presignature between two clusters. The destination cluster owns the migration call — it's a method on the destination ClusterAgent that takes the sealed export blob, the caller's RSA private key, and a minimal metadata stub.
Both clusters must share the same license certificate. The sealed blob is bound to the issuer's license fingerprint; importing it on a cluster running a different license rejects with "license fingerprint mismatch". If the destination runs a different license, the only path is emergency cold recovery (raw private key extraction) followed by re-DKG on the new cluster — a destructive operation that needs board approval and rewrites the cohort.
destAgent.migrateKeyShare(exportBlob, privateKeyPem, meta)
import { ClusterAgent, Curve } from '@zafeguard/mpc-sdk';
// 1. The caller mints a one-shot RSA keypair (e.g. via Node `crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })`).
// The public key is handed to the SOURCE cluster; the private key stays on the migration runner.
const { publicKeyPem, privateKeyPem } = await mintRsaKeypair();
// 2. On the source: pull the wallet's cluster-side `keyShareId` (the caller's `keyId` is not used here).
const sourceAgent = ClusterAgent.connect('us-node-1.example', 443, US_API_KEY, true);
const sourceShares = await sourceAgent.keyShares.list();
const sourceShare = sourceShares.find(s => s.keyId === 'eu-customer-wallet-prod');
if (!sourceShare) throw new Error('wallet not found on source cluster');
// 3. Source seals the share with the caller's RSA pubkey.
const exportResp = await sourceAgent.keyShares.export(sourceShare.id, {
cryptoMode: 'RSA',
publicKeyPem,
});
// 4. Destination drives the migration in one call. The destination unseals,
// re-wraps under its own ephemeral handshake key, and imports.
const destAgent = ClusterAgent.connect('eu-node-1.example', 443, EU_API_KEY, true);
await destAgent.migrateKeyShare(
exportResp.blob, // sealed bytes from step 3
privateKeyPem, // caller's RSA private key
{ curve: sourceShare.curve, publicKeyHex: sourceShare.publicKeyHex },
);
// 5. Confirm: list shares on the destination, locate by publicKeyHex.
const destShares = await destAgent.keyShares.list();
const destShare = destShares.find(s => s.publicKeyHex === sourceShare.publicKeyHex);
// destShare.id is the destination cluster's local keyShareId — use it for
// all subsequent presign / sign / export calls against destAgent.The migrate call runs the full pipeline server-side: unwrap with the caller's RSA private key → handshake against the destination → re-wrap under the handshake key → import. The caller never sees plaintext key material in the application process; only the source export blob and the destination handshake re-wrap pass through the SDK in opaque form.
migrateKeyShare is a method on the destination ClusterAgent — the source agent is queried separately for the export blob. The call resolves once the import has settled; an exception is thrown if the handshake expires, the license fingerprints differ, the publicKeyHex in meta doesn't match the unsealed share, or the destination already holds a share with the same id.
destAgent.migratePresignature(exportBlob, privateKeyPem, meta)
Same pattern for a single presignature entry. Source: sourceAgent.presignature.export(presigId, keyShareId, publicKeyPem) returns the sealed blob; destination: destAgent.migratePresignature(blob, privateKeyPem, { keyShareId, curve, publicKeyHex }). The keyShareId in meta is the destination cluster's local key-share id (the destination must already hold the wallet's key share, e.g. via a prior migrateKeyShare).
Lifecycle around migration
- Source-side cleanup is the caller's responsibility.
migrateKeySharedoes not delete the source share. To remove it after a successful migration, callsourceAgent.keyShares.delete(sourceShare.id)against the source cluster. keyShares.delete()operates on the local agent only. Deleting on one node removes that node's local share record; it does not propagate cluster-wide. For a 3-of-3 cluster wallet, rundelete()against each surviving cohort node (typically a small loop over your peer list).- The wallet's
publicKeyHexis preserved by design. Migration moves share material — the root key never reconstructs, so every on-chain address derived from the wallet remains stable. - Presignature pools are not migrated automatically.
migrateKeySharemoves only the share. Any presignatures pre-minted on the source cluster stay there; mint a fresh pool on the destination after migration, or migrate entries individually withmigratePresignature.
RecoveryBundle.verify(bundles, rsaPublicKeyPem)
Cross-node consistency check on the array of bundles returned by exportRecoveryBundle. Does NOT decrypt — useful to validate bundles before storing them long-term.
RecoveryBundle.finalize({ shards, bundlePublicKey, tag, passphrase? })
Combines per-node shards into a single RBUN container. Optional passphrase seals the output under a passphrase (AES-256-GCM); omit for an unsealed bundle that RecoveryBundle.recover can open with just the RSA private key.
RecoveryBundle.recover({ bundle, bundlePrivateKey, rootPublicKey, tag, passphrase? })
Off-line reconstruction. Returns { curve, privateKey, publicKey, chainCode? }. Pair with KeyUtils.deriveChildPrivateKey for BIP-32 child key derivation off-line.
Error model
All ClusterAgent methods return rejected Promises with Error instances when the agent rejects the request or the network fails. Common shapes:
| Error | Cause |
|---|---|
agent unreachable: <details> | Network / DNS / connection refused. |
agent returned 401 | API key rejected. |
agent returned 403 | Policy webhook denied the operation. |
agent returned 429: rate limit exceeded | Per-API-key rate limit hit. Wait or use a different key. |
agent returned 500: <message> | Internal agent error. Check the agent's logs; report to operators. |
Ceremony failed: <reason> | A ceremony reached Phase.Failed. The reason comes from the agent. |
See also
- MPC SDK overview → — choose between
EmbeddedAgentandClusterAgent. - Integration patterns → — Embedded Agent / custody backend / hybrid.
- Deployment → — bring up an agent cluster locally or in production.
- Configuration → — every YAML knob on the cluster side.
Integration patterns
Connect your cluster to the right client surface. Embedded Agent for device + cloud agents, ClusterAgent client for server-side custody, hybrid for both.
Overview
Encrypt to a cohort, decrypt only with quorum cooperation. Three primitive families — ElGamal, Paillier, RSA — each with single-party and threshold modes.