NERO
Guides

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-sdk installed
  • An authenticated user with a generated wallet

When to Use This

Use CaseDescription
Wallet migrationImport the key into MetaMask, Rabby, or another standard wallet
Offline signingSign transactions without the MPC backend
Self-custody backupStore the raw key in a secure, offline location
RecoveryReconstruct 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:

FieldTypeDescription
privateKeystringHex-encoded private key (with 0x prefix)
walletAddressstringThe EOA address derived from this key
protocolstringThe 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

  1. A warning banner about private key security
  2. A "Get Private Key" button
  3. After clicking, the key appears masked as •••••••••••• (64 dots)
  4. "Reveal" shows the actual hex key; "Hide" masks it again
  5. "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:

  1. Open MetaMask and click the account icon
  2. Select "Import Account"
  3. Choose "Private Key" as the type
  4. Paste the hex key (with or without the 0x prefix)
  5. Click "Import"

The imported account will have the same EOA address shown during export.

Rate Limiting

Key export is rate-limited to prevent abuse:

LimitDefault
Calls per hour5
ConfigurableYes, 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

  1. The client generates an ephemeral ECDH key pair
  2. The client sends the public half to the backend via an authenticated API call
  3. The backend encrypts its key share with the shared ECDH secret and returns it
  4. The client decrypts the backend share
  5. The client combines both shares to reconstruct the full private key
  6. The plaintext private key exists only in browser memory -- the backend never stores or logs it

Error Handling

ErrorCauseFix
NOT_AUTHENTICATEDNo active sessionLog in first
RATE_LIMIT_EXCEEDEDToo many export callsWait and retry after the cooldown period
RECOVERY_REQUIREDLocal key share is missingRun the recovery flow before exporting
NETWORK_ERRORBackend unreachableCheck connectivity and retry

On this page