Export the Private Key
Retrieve the fully reconstructed private key for local signing or wallet migration
Export the Private Key
Retrieve the fully reconstructed private key from the MPC system. The SDK fetches the backend's key share through an encrypted ECDH channel, combines it with the local client share, and returns the raw private key. This enables wallet migration, offline signing, and self-custody recovery.
Prerequisites
@nerochain/mpc-sdkinstalled- An authenticated user with a generated wallet
When to Use This
| Use Case | Description |
|---|---|
| Wallet migration | Import the key into MetaMask, Rabby, or another standard wallet |
| Offline signing | Sign transactions without the MPC backend |
| Self-custody backup | Store the raw key in a secure, offline location |
| Recovery | Reconstruct wallet access if the MPC backend is unavailable |
Security Warnings
The exported private key controls full access to the wallet funds.
- The key exists in browser memory during export — clear it as soon as you're done
- Anyone with the key can transfer all assets without MPC authorization
- The key is transmitted via an ephemeral ECDH-encrypted channel and never stored in plaintext on the backend
- Export is rate-limited: 5 calls per hour by default
Step 1 -- Call sdk.getKeyMaterial()
Use the useNeroMpcAuth hook to access the SDK instance, then call getKeyMaterial():
import { useNeroMpcAuth } from "@nerochain/mpc-sdk/react";
function ExportKey() {
const { sdk, isAuthenticated } = useNeroMpcAuth();
const handleExport = async () => {
if (!sdk || !isAuthenticated) return;
const result = await sdk.getKeyMaterial();
console.log("Private key:", result.privateKey);
console.log("Wallet address:", result.walletAddress);
console.log("Protocol:", result.protocol);
};
return (
<button onClick={handleExport} disabled={!isAuthenticated}>
Export Private Key
</button>
);
}The returned object contains:
| Field | Type | Description |
|---|---|---|
privateKey | string | Hex-encoded private key (with 0x prefix) |
walletAddress | string | The EOA address derived from this key |
protocol | string | The MPC protocol used ("dkls" or "pedersen") |
Step 2 -- Display with Reveal/Hide Toggle
Keep the private key hidden by default and let the user reveal it on demand. Include a copy button for convenience.
import { useState } from "react";
import { useNeroMpcAuth } from "@nerochain/mpc-sdk/react";
export default function PrivateKeyExport() {
const { sdk, isAuthenticated } = useNeroMpcAuth();
const [privateKey, setPrivateKey] = useState<string | null>(null);
const [walletAddress, setWalletAddress] = useState<string | null>(null);
const [protocol, setProtocol] = useState<string | null>(null);
const [revealed, setRevealed] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleExport = async () => {
if (!sdk || !isAuthenticated) return;
setIsLoading(true);
setError(null);
setRevealed(false);
try {
const result = await sdk.getKeyMaterial();
setPrivateKey(result.privateKey);
setWalletAddress(result.walletAddress);
setProtocol(result.protocol);
} catch (err) {
if (err instanceof Error && err.message.includes("rate")) {
setError("Rate limit reached. Try again in a few minutes.");
} else {
setError(err instanceof Error ? err.message : String(err));
}
} finally {
setIsLoading(false);
}
};
const handleCopy = () => {
if (privateKey) {
navigator.clipboard.writeText(privateKey);
}
};
return (
<div className="mx-auto max-w-lg">
<h2>Export Private Key</h2>
<p className="mb-4 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3 text-sm">
<strong>Warning:</strong> The private key controls your wallet funds.
Do not share it or paste it into untrusted websites.
</p>
<button onClick={handleExport} disabled={isLoading || !isAuthenticated}>
{isLoading ? "Retrieving..." : "Get Private Key"}
</button>
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
{privateKey && (
<div className="mt-4">
<label>Private Key</label>
<div className="flex items-center gap-2">
<code className="flex-1 break-all rounded-lg bg-fd-secondary p-2 text-xs">
{revealed ? privateKey : "\u2022".repeat(64)}
</code>
<button onClick={() => setRevealed(!revealed)}>
{revealed ? "Hide" : "Reveal"}
</button>
<button onClick={handleCopy}>Copy</button>
</div>
{walletAddress && <p>Address: <code>{walletAddress}</code></p>}
{protocol && <p>Protocol: <code>{protocol}</code></p>}
</div>
)}
</div>
);
}What the User Sees
- A warning banner about private key security
- A "Get Private Key" button
- After clicking, the key appears masked as
••••••••••••(64 dots) - "Reveal" shows the actual hex key; "Hide" masks it again
- "Copy" puts the key on the clipboard for pasting into MetaMask
Step 3 -- Import Into MetaMask
Once the user has the private key, they can import it into any standard wallet:
- Open MetaMask and click the account icon
- Select "Import Account"
- Choose "Private Key" as the type
- Paste the hex key (with or without the
0xprefix) - Click "Import"
The imported account will have the same EOA address shown during export.
Rate Limiting
Key export is rate-limited to prevent abuse:
| Limit | Default |
|---|---|
| Calls per hour | 5 |
| Configurable | Yes, per application in the dashboard |
When the rate limit is exceeded, the SDK throws a RateLimitError. Handle it gracefully:
try {
const result = await sdk.getKeyMaterial();
} catch (err) {
if (err.code === "RATE_LIMIT_EXCEEDED") {
// Show a message to try again later
}
}How It Works
- The client generates an ephemeral ECDH key pair
- The client sends the public half to the backend via an authenticated API call
- The backend encrypts its key share with the shared ECDH secret and returns it
- The client decrypts the backend share
- The client combines both shares to reconstruct the full private key
- The plaintext private key exists only in browser memory -- the backend never stores or logs it
Error Handling
| Error | Cause | Fix |
|---|---|---|
NOT_AUTHENTICATED | No active session | Log in first |
RATE_LIMIT_EXCEEDED | Too many export calls | Wait and retry after the cooldown period |
RECOVERY_REQUIRED | Local key share is missing | Run the recovery flow before exporting |
NETWORK_ERROR | Backend unreachable | Check connectivity and retry |