Embedded Wallet Solution

Dynamic Configuration

Runtime configuration patterns for embedded wallets — how your application layer resolves per-user, per-tier, per-region signer shape and passes it to the SDK constructor without rewriting the app for each new combination.

Dynamic Configuration

A production embedded wallet is not one wallet shape. It is a parameterised wallet shape that takes different concrete forms per user tier, region, risk profile, and feature flag. The whole point of building on a platform is that these dimensions are configuration, not engineering work.

Where dynamic configuration lives matters. The SDK is constructor-driven — your application code decides which cohort, threshold, curve, storage, and presignature-pool settings to pass to new EmbeddedAgent(...). The workflow canvas is policy-driven — the policy stages around signing (rate limits, denylists, time locks, gas sponsorship) live in workflows you edit on the dashboard. Together they cover the moving parts.

This page covers the patterns that let one codebase serve every user tier, region, and risk profile.


What is runtime-configurable

ParameterSurfaceTypical change frequency
Cohort agent setSDK constructor — agent: Array<JsAgentConnection>Tier promotions, regional rollouts
Threshold countSDK constructor — thresholdTier promotions, risk classifications
CurveSDK constructor — curveAdding chain family support (build a second signer instance)
Storage backendSDK constructor — storagePlatform variation (iOS / Android / web / Node)
Bridge transportSDK constructor — bridgeCustom transport requirements
Presignature-pool auto-refillSDK constructor — presignaturePool: { autoRefillWhen, autoRefillTo }Mobile UX tuning, battery budget
Recovery clauseSDK create() argument — recovery: JsRecoveryConfigUser-initiated change, policy update
Pool depth at any momentRuntime — loaded.presignature.mint({ count })Opportunistic
Policy stages around signingWorkflow canvas — components on the workflow that wraps signCompliance changes, experiments
Workflow routingWorkspace workflow registryProduct experiments, regional rollouts

The cryptographically-meaningful changes (threshold, cohort) are committed at create and rotated in place only via loaded.reshare({ passphrase }) — which preserves the same cohort shape. Cohort-shape changes (replace a party, expand threshold) require a fresh wallet via a new create.

The operationally-meaningful changes (pool depth, workflow routing, policy stage parameters) take effect immediately on the next relevant call.


The configuration resolver pattern

Centralise per-user shape resolution in one function. Your application uses it at every signer construction:

import { EmbeddedAgent, Curve } from '@zafeguard/mpc-sdk';

interface SignerShape {
  agents: Array<{ baseUrl: string; apiKey: string }>;
  threshold: number;
  curve: Curve;
  presignaturePool?: { autoRefillWhen: number; autoRefillTo: number };
}

async function resolveSignerShape(user: User): Promise<SignerShape> {
  // Your business logic decides which agents, threshold, and pool policy
  // apply to this user. Could read from a feature-flag service, a tier
  // database, an A/B-test cohort assignment, anything.
  if (user.tier === 'starter') {
    return {
      agents: [{ baseUrl: BUILTIN_NODE_1, apiKey: process.env.ZG_BUILTIN_1! }],
      threshold: 2,
      curve: Curve.Secp256k1,
      presignaturePool: { autoRefillWhen: 3, autoRefillTo: 10 },
    };
  }
  if (user.tier === 'premium') {
    return {
      agents: [
        { baseUrl: BUILTIN_NODE_1, apiKey: process.env.ZG_BUILTIN_1! },
        { baseUrl: BUILTIN_NODE_2, apiKey: process.env.ZG_BUILTIN_2! },
      ],
      threshold: 2,
      curve: Curve.Secp256k1,
      presignaturePool: { autoRefillWhen: 10, autoRefillTo: 50 },
    };
  }
  if (user.tier === 'enterprise') {
    return {
      agents: [
        { baseUrl: BUILTIN_NODE_1,         apiKey: process.env.ZG_BUILTIN_1! },
        { baseUrl: BUILTIN_NODE_2,         apiKey: process.env.ZG_BUILTIN_2! },
        { baseUrl: user.selfHostedAgentUrl, apiKey: user.selfHostedApiKey },
      ],
      threshold: 3,
      curve: Curve.Secp256k1,
      presignaturePool: { autoRefillWhen: 20, autoRefillTo: 100 },
    };
  }
  throw new Error('Unrecognised tier');
}

async function buildSignerForUser(user: User): Promise<EmbeddedAgent> {
  const shape = await resolveSignerShape(user);
  return new EmbeddedAgent(shape);
}

Your app code is identical for every tier. The user's signer shape is data — decided by your resolver, sourced from wherever your feature-flag / tier / experiment infrastructure lives.


Region-based topology

For users with data-residency requirements, the resolver returns regionally appropriate agents:

async function resolveSignerShape(user: User): Promise<SignerShape> {
  const region = user.dataResidencyRegion;  // 'us' | 'eu' | 'ap'

  const agents: Record<string, SignerShape['agent']> = {
    us: [
      { baseUrl: NODES.us_east, apiKey: process.env.ZG_US_EAST! },
      { baseUrl: NODES.us_west, apiKey: process.env.ZG_US_WEST! },
    ],
    eu: [
      { baseUrl: NODES.eu_west,    apiKey: process.env.ZG_EU_WEST! },
      { baseUrl: NODES.eu_central, apiKey: process.env.ZG_EU_CENTRAL! },
    ],
    ap: [
      { baseUrl: NODES.ap_south,     apiKey: process.env.ZG_AP_SOUTH! },
      { baseUrl: NODES.ap_southeast, apiKey: process.env.ZG_AP_SOUTHEAST! },
    ],
  };

  return {
    agents: agents[region],
    threshold: 2,
    curve: Curve.Secp256k1,
  };
}

The user never knows the topology. The app never knows the topology beyond the resolver. Moving a region's infrastructure is a resolver change.

For existing signers that need to move regions (re-key under new agents), see "Cohort-shape changes" below.


Risk-based topology

Risk signals — fraud flags, high-value account, regulatory flag on a specific user — can drive a new user's signer to a higher-threshold, stricter-pool shape:

async function resolveSignerShape(user: User): Promise<SignerShape> {
  if (user.riskLevel === 'high') {
    return {
      agents: [
        { baseUrl: NODES.eu_west,    apiKey: process.env.ZG_EU_WEST! },
        { baseUrl: NODES.eu_central, apiKey: process.env.ZG_EU_CENTRAL! },
        { baseUrl: NODES.eu_north,   apiKey: process.env.ZG_EU_NORTH! },
      ],
      threshold: 3,                            // require all three (instead of any two)
      curve: Curve.Secp256k1,
      presignaturePool: { autoRefillWhen: 0, autoRefillTo: 5 },   // force every sign through inline policy
    };
  }
  // ... other tiers
}

For existing signers whose risk profile changes after create, the SDK does not support adjusting the threshold or cohort in place. The application-layer mitigations available without a fresh DKG:

  • Policy components. Add a stricter policy stage to the workflow that wraps sign. The wallet is unchanged; the wrapping changes.
  • Pool depletion. Reduce the auto-refill target to 0 (next mint completes the existing target; subsequent mints respect the new lower target) so every signature goes through inline policy.
  • Recovery rotation. loaded.rotateRecoveryCredential({ oldRecovery, newRecovery }) with a longer / stricter credential changes the cost-of-bundle-access without touching the share material.

Multi-account derivation for one root key

For products where one MPC root key derives addresses for many users — the embedded-wallet-per-end-user pattern from the LINE Mini-App case study — derivation happens at the workflow level using COMPUTE_PUBLIC_KEY + COMPUTE_EVM_ADDRESS:

import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';

const workspace = new WorkspaceClient({ apiKey: process.env.ZAFEGUARD_API_KEY! });

async function deriveUserEvmAddress(addressIndex: number): Promise<string> {
  const { derivationPath } = await workspace
    .call(ComponentModule.GET_EVM_DERIVATION_PATH, {
      accountIndex: 0,
      changeIndex: 0,
      addressIndex,
    })
    .promise();

  const { publicKey } = await workspace
    .call(ComponentModule.COMPUTE_PUBLIC_KEY, {
      keyId: process.env.ZAFEGUARD_ROOT_KEY_ID!,
      derivationPath,
    })
    .promise();

  const { address } = await workspace
    .call(ComponentModule.COMPUTE_EVM_ADDRESS, { publicKey })
    .promise();

  return address;
}

The root key participates in one DKG ceremony; every end user gets a deterministic address derived from it. Adding a new user is a derivation operation, not a new ceremony — a million users can be provisioned from one root key. The signing for any end user goes through a workflow that signs at the corresponding derivation path.

This pattern uses ClusterAgent cluster-side (a single root MPC key managed by the cluster) rather than EmbeddedAgent, because the root key is platform-managed and per-user shares are derivation paths, not separate DKG runs.


Cohort-shape changes (no in-place reshape)

The SDK's loaded.reshare({ passphrase }) rotates share material within the same cohort shape — same parties, same threshold. Real cohort-shape changes (add a region, replace a party, change the threshold) require provisioning a new wallet and migrating funds.

The typical migration shape:

  1. Resolve the new shape via your resolver.
  2. create() a new signer at a new signerId.
  3. Build a transfer workflow that drains the old signer's balances to the new signer's address (gated by your normal multi-party / policy approval flow).
  4. Mark the old signer retired in your application.

For an embedded wallet, this is a user-visible event — "we're upgrading your wallet for the new region; you'll see your address change." Many products avoid it by committing to a generous-enough cohort shape at the original ceremony.


Policy on the workflow canvas

The configuration patterns above cover the signing primitive. The policy around signing lives in workflows you compose on the dashboard. A typical send-payment workflow:

Trigger (signerId, to, amount, chain)
        |
        v
   Policy check         (velocity limits, denylists, geo)
        |
        v
   Gas sponsorship      (estimate, sponsor pays)
        |
        v
   Sign                 (loaded.sign — by your app, or MPC component if server-side)
        |
        v
   Submit               (broadcast on chain)
        |
        v
   Audit                (workflow execution log)

The workspace tracks workflow versions; older runs reference the version of the workflow that ran at the time. Editing a policy is a dashboard change; the workflow ID your app triggers stays the same and the next run uses the new definition.

→ See Workflow concepts for how workflows are composed and triggered.


Where configuration lives in practice

SettingWhere it livesWho edits it
Resolver logicYour application codeProduct / engineering team
Per-user shape dataYour database + feature-flag systemOperations team
Workflow definitionsWorkspace dashboardProduct / platform teams
Cohort node hostnames + API keysYour secret managementPlatform / SRE team
Default RPC endpoints (for chain components)Workspace configPlatform team
Audit emission targetWorkspace observability configPlatform team

The pattern that emerges: SDK shape lives in your application's resolver function; policy and gas / broadcast wrappers live on the workflow canvas. Both are runtime-configurable; both are dashboard-editable for the canvas side, code-editable for the resolver side.


Next

On this page