Embedded Agent

Quick start

A minimal end-to-end Embedded Agent integration in under 30 lines — create, sign, verify, export, restore.

Quick start

A working 2-of-2 wallet (device + one cloud agent) in one TypeScript file. Drop it into a script, set the env vars, run it. Every step from DKG through signature verification is real — no mocks.

Call await Mpc.init(licenseCert) once at process start, before constructing any handle. EmbeddedAgent and ClusterAgent both expose finalizeSignature / verifySignature — local-crypto methods that re-validate the license on every invocation. Skipping Mpc.init(...) lets signer.create() succeed but throws as soon as you try to combine or verify a signature.

import { Utils } from '@zafeguard/mpc-sdk';
import {
  Curve,
  EmbeddedAgent,
  Mpc,
  RecoveryKind,
  Utils,
} from "@zafeguard/mpc-sdk";

const NODE_URL = process.env.MPC_NODE_URL ?? 'https://your-agent.example';
const API_KEY  = process.env.MPC_NODE_API_KEY ?? 'your-api-key';
const LICENSE  = process.env.ZAFEGUARD_LICENSE_CERT!;

async function main() {
  // 0) Activate the SDK license once per process. Required before
  //    any handle that uses finalizeSignature / verifySignature
  //    (i.e. EmbeddedAgent and ClusterAgent).
  await Mpc.init(LICENSE);

  // 1) Construct — no network call, just validates config.
  const signer = new EmbeddedAgent({
    agents: [{ baseUrl: NODE_URL, apiKey: API_KEY }],
    threshold: 2,
    curve: Curve.Secp256k1,
  });

  // 2) Create — runs DKG with the agent. Omit `keyId` and the SDK
  //    auto-generates a UUID; surface it via `loaded.keyId` and
  //    persist it alongside the blob so `load()` can find the right
  //    record later. The `passphrase` seals the post-DKG state into
  //    `loaded.exportedBlob`, ready to persist in one round-trip.
  const loaded = await signer.create({
    recovery: { kind: RecoveryKind.Noop },
    passphrase: 'cold-backup-passphrase',     // derive from HSM in production
  });
  console.log('keyId:', loaded.keyId);
  console.log('publicKey:', loaded.publicKey.toString('hex'));
  console.log('exportedBlob:', loaded.exportedBlob?.length, 'bytes ready for persistence');

  // 3) Sign — one call. `signMessage` runs `sign()` (cohort round-trip
  //    for partials) and `finalizeSignature()` (local combine) for
  //    you, returning the assembled signature directly. Use the split
  //    `loaded.sign(...)` + `Utils.finalizeSignature(...)` path when
  //    the two halves run on different hosts.
  const messageHash = Utils.sha256(new TextEncoder().encode('hello embedded agent'));
  const sig = await loaded.signMessage({ messageHash });

  // 4) Verify locally — no agent involvement.
  const ok = loaded.verifySignature({
    messageHash,
    signature: sig.compactRecovery,
  });
  if (!ok) throw new Error('signature did not verify');
  console.log('signature verified');

  // 5) Persist — `create()` already produced the sealed blob via
  //    `loaded.exportedBlob`. Persist anywhere (file, OS keychain,
  //    encrypted DB). The agent never sees this blob.
  //    `loaded.export(passphrase)` is available too — call it later
  //    to re-seal under a different passphrase, or to snapshot after
  //    minting presigs.
  const blob = loaded.exportedBlob!;

  // 6) Restore — same keyId, same passphrase, fresh process.
  //    Reloaded handle holds the same publicKey byte-for-byte.
  const signer2 = new EmbeddedAgent({
    agents: [{ baseUrl: NODE_URL, apiKey: API_KEY }],
    threshold: 2,
    curve: Curve.Secp256k1,
  });
  const restored = await signer2.load({
    keyId: loaded.keyId,
    blob,
    passphrase: 'cold-backup-passphrase',
  });
  console.log(
    'publicKey matches across handles:',
    restored.publicKey.equals(loaded.publicKey),
  );
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Run it:

MPC_NODE_URL=https://your-agent.example \
MPC_NODE_API_KEY=your-api-key \
ZAFEGUARD_LICENSE_CERT=<your-license-cert> \
npx ts-node embedded-agent-quickstart.ts

You should see three lines: the public key hex, "signature verified", and publicKey matches across handles: true.

loaded.exportedBlob is a Buffer — opaque, sealed ciphertext. Persisting it is your job. The SDK never touches the filesystem, the OS keychain, or cloud storage; it hands you the bytes and you decide where they go. In production, persist to your platform's secure store:

  • iOS — Keychain, optionally hardware-sealed via P256Plugin + HardwareWrappedStorage (see Storage).
  • Android — Keystore via the same HardwareWrappedStorage shape.
  • Node desktop / server — EncryptedFileStorage (the bundled passphrase-sealed file backend).
  • Cross-platform / tests — InMemoryKeyStorage.

Losing the blob without a configured recovery clause means the wallet is unrecoverable, so wire storage early.


What this snippet doesn't cover

The five-method lifecycle above is the minimum integration. The other pages in this section build on it:

  • More than one agent (institutional redundancy, multi-region wallets): pass more entries to agent[] and pick a threshold other than 2. See Topology.
  • Lower per-sign latency (pre-mint presignatures so each sign() runs a single cohort round-trip instead of two): mint a batch with mintPresignatures and let subsequent signs drain it. See Presignature pool.
  • Cold backup beyond the passphrase-encrypted blob above (Shamir-split across N contacts, custodian-sealed RSA, paper recovery): swap RecoveryKind.Noop for Password / Social / Custodial. See Recovery.
  • Share rotation (rotate the cryptographic material on a defined cadence without changing the on-chain address): loaded.reshare({ passphrase }). See Recovery.

On this page