Overview
The high-level facade in @zafeguard/mpc-sdk for the warm-cold wallet shape — one device share, N cloud-agent shares, threshold signing across the cohort, with an optional presignature pool that batches the heavier round.
Overview
EmbeddedAgent is the high-level facade in @zafeguard/mpc-sdk for the warm-cold wallet shape: the user's device holds one MPC share, one or more Zafeguard cloud agents hold the rest, and signing requires the device plus a configurable threshold of agents to cooperate. No single party — device alone, agent alone, or the network alone — can produce a signature.
Cryptographic construction: Pedersen-style DKG (1991) with Feldman commitments, Gennaro-Goldfeder GG18/GG20 threshold ECDSA for the EC-curve signing flows, RFC 8032 EdDSA for Ed25519 / Ed448, BIP-340 for Bitcoin Taproot Schnorr, and BIP-32 for hierarchical derivation. See Cryptographic foundations for the full reference map.
Call await Mpc.init(licenseCert) once at process start, before constructing the first EmbeddedAgent. The class exposes finalizeSignature / verifySignature — local-crypto methods that re-validate the license per call. Without Mpc.init(...) they throw at first use. See License requirement.
The five-method lifecycle covers every state a signer goes through:
import { EmbeddedAgent, Curve, RecoveryKind } from '@zafeguard/mpc-sdk';
const signer = new EmbeddedAgent({
agents: [{ baseUrl: 'https://node-1.example', apiKey: '...' }],
threshold: 2,
curve: Curve.Secp256k1,
});
const loaded = await signer.create({ keyId, recovery: { kind: RecoveryKind.Noop }, passphrase });
const blob = loaded.exportedBlob!; // sealed during create — persist via your HSM-backed storage
const partials = await loaded.sign({ messageHash }); // cohort partials
const sig = await loaded.finalizeSignature({ // combine locally
messageHash: partials.messageHash, partials: partials.partials, derivationPath: partials.derivationPath,
});
const restored = await signer.load({ keyId, blob, passphrase });
const reshared = await loaded.reshare({ passphrase }); // reshared.exportedBlob sealed inlineThe cohort size is always 1 + agent.length. A single-agent cohort is the simplest 2-of-2 wallet; adding more agents to the array adds redundancy and quorum flexibility. Every agent participates as a distinct DKG party — they're equal participants, not a primary-plus-fallback split.
Vocabulary in one table
These four terms appear throughout the SDK and the docs. They mean different things — keep them straight:
| Term | What it means | How it's set |
|---|---|---|
| Cohort | The fixed set of parties that hold key shares for a single wallet. Device counts as one party; every entry in agents[] counts as one. | Decided at create() (= DKG). Never changes for the lifetime of the wallet — even reshare keeps the same membership. |
| Participants | Total count of cohort parties. Equals 1 + agents.length. | Implied by the cohort. Surfaced on loaded.participants. |
| Threshold | How many cohort parties must cooperate to produce a signature. The t in t-of-n. | Constructor argument threshold. Range 2..=participants. Surfaced on loaded.threshold. |
| Quorum | A concrete set of threshold-many parties that actually participate in one signing call. Synonymous with "the signers on this particular sign() round". | Picked at sign time — for a 2-of-3 wallet, the device + either agent forms a quorum. |
So a 2-of-3 wallet has cohort = 3, participants = 3, threshold = 2, and each sign() call uses one quorum of 2.
Mental model
Device Cloud agents (1..N)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ share₀ │ │ share₁ │ │ share₂ │ ...
└─────┬────┘ └──────┬───┘ └────┬─────┘
│ │ │
└────── threshold-many partial sigs ─────┘
│
▼
Combined signature
(verifies against the wallet's
DKG-produced public key)
-
DKG runs once at
create(). Every cohort party generates its own share locally; no party ever sees the full private key. -
Sign is one logical operation that runs in two phases — a cohort round-trip to gather
threshold-many partial signatures, then a local combine that assembles the final(r, s, v). The SDK gives you three entry points for the same operation depending on where the two phases need to run:loaded.signMessage({ messageHash })— the everyday path. Runs both phases and returns the assembled signature directly. Use this when the wallet signs and finalises on the same host.loaded.sign({ messageHash })thenloaded.finalizeSignature({ partials })— runs the same phases as separate calls. Use only when you need to inspect partials between calls.loaded.sign(...)thenUtils.finalizeSignature({ partialSignatures, publicKey })— used when the host that runssign()is not the host that finalises. Shippartialsto the other host and callUtils.finalizeSignaturethere. (Scheme.finalizeSignatureis the low-level variant when you only have raw bytes, not a handle.)
Each
sign()(orsignMessage()) call burns a cohort interaction — it is not idempotent. -
Reshare rotates the cryptographic material across the cohort while preserving the wallet's public key (and therefore every on-chain address derived from it).
-
Restart vs. recover —
signer.load({ blob, passphrase })is the routine path for "same device, new process" (an app restart or device reboot).signer.load({ recoveryBundle, recovery })is the disaster path for "new device, original device gone" (a phone you no longer have access to) — it consumes the off-device recovery bundle and the matching recovery credential, not the device-local blob.
What's in this section
- Quick start → — a working signer in under 30 lines: create → sign → verify → export → restore.
- Topology → — choose your cohort: single-agent for the simplest 2-of-2 wallet, multi-agent for redundancy, threshold for the signing quorum.
- Presignature pool → — pre-mint a batch to cut per-sign cohort round-trips from two to one, opt-in auto-refill, and pool export/import for cold-storage handoff.
- Recovery → — share rotation (
reshare()), sealed-bundle backup (theRecoveryKindplugins), and the emergency recovery path that reconstructs the raw root private key off-cluster from a quorum of cohort shards — your escape hatch when the MPC infrastructure becomes unreachable, you need to migrate off MPC entirely, or a regulator demands a single-key extraction.
When NOT to use EmbeddedAgent
EmbeddedAgent assumes your app holds one of the MPC shares and talks to Zafeguard agents over HTTPS. It's the right facade for:
- Consumer wallets (mobile / browser apps) that want self-custody with one agent as a 2FA-like co-signer.
- Institutional integrations that run their own MPC nodes (the agent topology lets you point the array at your own cluster).
If your product wants Zafeguard to hold and manage the keys server-side (and your code just triggers signing as a workflow component), use the @zafeguard/caller-sdk instead — that's the higher-level workflow API; the MPC ceremony machinery is hidden inside Zafeguard's components.
If you need raw access to a single MPC cluster node (no embedded device party — pure server-side custody) reach for ClusterAgent, the lower-level client in the same package.
MPC SDK
Zafeguard MPC SDK (@zafeguard/mpc-sdk) — run threshold-MPC key generation, signing, and recovery client-side. Two entry points: EmbeddedAgent for the warm-cold wallet shape and ClusterAgent for raw cluster access.
Quick start
A minimal end-to-end Embedded Agent integration in under 30 lines — create, sign, verify, export, restore.