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
| Parameter | Surface | Typical change frequency |
|---|---|---|
| Cohort agent set | SDK constructor — agent: Array<JsAgentConnection> | Tier promotions, regional rollouts |
| Threshold count | SDK constructor — threshold | Tier promotions, risk classifications |
| Curve | SDK constructor — curve | Adding chain family support (build a second signer instance) |
| Storage backend | SDK constructor — storage | Platform variation (iOS / Android / web / Node) |
| Bridge transport | SDK constructor — bridge | Custom transport requirements |
| Presignature-pool auto-refill | SDK constructor — presignaturePool: { autoRefillWhen, autoRefillTo } | Mobile UX tuning, battery budget |
| Recovery clause | SDK create() argument — recovery: JsRecoveryConfig | User-initiated change, policy update |
| Pool depth at any moment | Runtime — loaded.presignature.mint({ count }) | Opportunistic |
| Policy stages around signing | Workflow canvas — components on the workflow that wraps sign | Compliance changes, experiments |
| Workflow routing | Workspace workflow registry | Product 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:
- Resolve the new shape via your resolver.
create()a new signer at a newsignerId.- 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).
- 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
| Setting | Where it lives | Who edits it |
|---|---|---|
| Resolver logic | Your application code | Product / engineering team |
| Per-user shape data | Your database + feature-flag system | Operations team |
| Workflow definitions | Workspace dashboard | Product / platform teams |
| Cohort node hostnames + API keys | Your secret management | Platform / SRE team |
| Default RPC endpoints (for chain components) | Workspace config | Platform team |
| Audit emission target | Workspace observability config | Platform 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
- Embedded Agent reference — every constructor field, every option, every method.
- Smart Account Flow — end-to-end workflow-driven multi-user wallet using the cluster-side root-key + derivation pattern (the canonical LINE LIFF / Telegram / Discord / in-game shape).
- Custody Whitelabel — Dynamic Configuration — the institutional counterpart for multi-tenant custody-as-a-service.
Smart Account Flow
Build an embedded smart-account wallet where every end user gets a deterministic Gnosis Safe address derived from one MPC root key, lazy-deploys on first use, and pays no gas — a workflow-driven gas sponsor covers every transaction. The pattern travels from chat-app mini-apps (LINE / Telegram / Discord) to email-login dApps to in-game wallets.
Overview
Architectural overview for a regulated custody whitelabel — hot wallet for 24/7 programmatic signing, cold wallet for air-gapped multi-party approvals, both built on Zafeguard MPC SDK and the workflow canvas.