name: mpckit description: Build with the MPCKit SDKs (@mpckit/sdk for TypeScript, @mpckit/react for React + TanStack Query, and the mpckit crate for Rust). Use when integrating MPCKit hosted MPC signing, onboarding dWallets, producing signatures across Bitcoin / Ethereum / Solana / Sui, or wiring billing and introspection from any of those three languages. Triggers on tasks involving @mpckit/sdk, @mpckit/react, the mpckit Rust crate, the MPCKit class, MPCKit::builder, useMPCKit, useOnboard, useSign, useDWallets, the OnboardArgs / SignArgs types, MPCKit API keys, or the api.mpckit.xyz / api.testnet.mpckit.xyz endpoints. Use when this capability is needed. metadata: author: Iamknownasfesal
MPCKit
Build cross-chain signing applications against the MPCKit hosted MPC API. One backend handles every chain that ECDSA secp256k1 / Taproot, Ed25519, ECDSA P-256, or Schnorrkel covers. Three SDKs are first-party.
When to use
- Calling
api.mpckit.xyzorapi.testnet.mpckit.xyzfrom any language with a published SDK. - Onboarding a zero-trust dWallet end-to-end (DKG + accept-share) from a 32-byte seed.
- Signing a message with
api.sign(...)(TypeScript / Rust) oruseSign()(React). - Reading
health,network,protocol-parameters, billing, or dWallet state. - Picking the right SDK for a given app shape (Node service, browser, native binary, React UI).
When NOT to use
- Self-hosting the backend. That lives in
apps/backend; consultapps/docs/content/docs/self-hosting/**. - Direct upstream
@ika.xyz/sdkwork. MPCKit re-implements the public surface so consumers never take a transitive@ika.xyz/sdkdependency. If the task is about Sui PTBs, Ika coordinator objects, orIkaTransaction, use theika-sdkskill instead, not this one. - Bitcoin / Solana / Ethereum transaction assembly. MPCKit produces the raw signature only; broadcasting it to the destination chain is the consumer's responsibility.
- Move contract authoring against
mpckitcore. That is a Move package underpackages/mpckitcore_move; use theika-moveskill.
Pick by language
| App shape | SDK | Entry point |
|---|---|---|
| Node service, edge function, CLI, tests | @mpckit/sdk |
new MPCKit({ apiKey, network }) |
| Browser SPA (Vite, Next, Remix) | @mpckit/sdk + WebWorkerCryptoEngine OR @mpckit/react |
MPCKitProvider |
| React app with TanStack Query already in tree | @mpckit/react |
useMPCKit, useOnboard, useSign, ... |
| Rust binary, Tauri backend, async service | mpckit crate |
MPCKit::builder().api_key(...).network(...).build() |
The Rust crate ships with default = [] features (HTTP only). Add features = ["crypto"] to opt into the high-level onboard / sign ceremonies. Without the crypto feature the crate exposes only sign_prepare + sign_submit and expects the caller to drive the WASM-equivalent centralized signature themselves.
Hosts and API keys
| Network | Base URL | API key prefix |
|---|---|---|
| Testnet | https://api.testnet.mpckit.xyz |
mpckit_test_… |
| Mainnet | https://api.mpckit.xyz |
mpckit_live_… |
API keys are scoped per network. The base URL is auto-derived from the Network enum value if you do not pass baseUrl / .base_url(...); override only for self-hosted backends or local development.
Sign-up at https://app.mpckit.xyz to issue keys.
Install
| SDK | Install |
|---|---|
@mpckit/sdk |
bun add @mpckit/sdk or pnpm add @mpckit/sdk |
@mpckit/react |
bun add @mpckit/react @tanstack/react-query (peer dep). Pulls in @mpckit/sdk transitively |
mpckit (Rust) |
cargo add mpckit then --features crypto for the high-level ceremonies |
At a glance
import { MPCKit, Curve, Hash, SignatureAlgorithm } from "@mpckit/sdk";
import { randomBytes } from "node:crypto";
const api = new MPCKit({
apiKey: process.env.MPCKIT_API_KEY!,
network: "testnet",
});
const seed = randomBytes(32);
const { dwallet, userSecretKeyShareHex } = await api.onboard({
seed,
curve: Curve.SECP256K1,
});
const { signature } = await api.sign({
seed,
dwalletId: dwallet.id,
curve: Curve.SECP256K1,
signatureAlgorithm: SignatureAlgorithm.ECDSASecp256k1,
hashScheme: Hash.SHA256,
message: new TextEncoder().encode("hello mpckit"),
userSecretKeyShareHex,
});
The seed is the only thing you must safeguard between onboard and sign. Anything that derives 32 bytes deterministically works: a passkey PRF output, an env-stored secret, a KMS-released key. The userSecretKeyShareHex returned from onboard() is the encrypted user share; persist it next to the dWallet record so future signs can recover it from your DB.
Crypto constants
Curves, signature algorithms, and hash schemes are string enums in TypeScript and numeric enums in Rust. Valid combinations:
| Chain | Curve | SignatureAlgorithm | Hash |
|---|---|---|---|
| Ethereum | SECP256K1 | ECDSASecp256k1 | KECCAK256 |
| Bitcoin Taproot | SECP256K1 | Taproot | SHA256 |
| Bitcoin legacy | SECP256K1 | ECDSASecp256k1 | DoubleSHA256 |
| Solana | ED25519 | EdDSA | SHA512 |
| WebAuthn / P-256 | SECP256R1 | ECDSASecp256r1 | SHA256 |
| Polkadot / Substrate | RISTRETTO | SchnorrkelSubstrate | Merlin |
Passing an invalid (curve, signatureAlgorithm, hashScheme) triple is rejected by the backend before any crypto runs; the error surfaces as MPCKitError with a clear message, not a thrown WASM panic.
References (load on demand)
| file | when |
|---|---|
references/typescript.md |
@mpckit/sdk full surface, options, billing, introspection, raw HTTP escape hatch |
references/react.md |
MPCKitProvider, all 10 hooks, worker setup, SSR notes |
references/rust.md |
MPCKit::builder(), async traits, default vs crypto feature, two-phase sign |
references/flows.md |
Onboard + sign sequenced for each language; passkey-PRF seed pattern; idempotency |
references/errors.md |
MPCKitError taxonomy, retry policy, insufficient-credits handling |
Quick decision flow
- What language are you in? Pick the SDK from the table above.
- Do you have an API key? If not, sign up at
app.mpckit.xyzand grab one for the right network. - Server-only or browser? Server / Node: default
InlineCryptoEngineis fine. Browser:WebWorkerCryptoEngine(TS) oruseWorker: trueonMPCKitProvider(React). Otherwise the WASM-heavy DKG blocks the main thread for several seconds. - Need a stable signing identity across reloads? Persist the
seedsomewhere durable (passkey PRF, OS keychain, KMS).onboard()returnsuserSecretKeyShareHexanduserPublicOutputHex; persist both alongsidedwallet.id. - Onboard once, sign many. Onboard produces a dWallet. Each subsequent
sign()is one HTTP roundtrip plus the centralized-signature math. There is no per-request presign management for the consumer; the backend warms a presign pool.
Common mistakes
| mistake | what to do instead |
|---|---|
Forgetting to persist userSecretKeyShareHex after onboard |
It is the only share-side artifact; without it future signs cannot recover the encrypted user share. The backend never sees it decrypted. |
| Inlining a WASM-heavy crypto engine on the browser main thread | Use WebWorkerCryptoEngine (TS) or useWorker: true (React). DKG takes 5 to 15 seconds; UI freezes are unacceptable. |
| Sending a private key directly to MPCKit | There is no such endpoint. The flow is DKG-based: the user share is generated on the client during onboard(), never extracted. To import an existing key, see references/flows.md. |
Constructing many MPCKit instances per request |
The instance caches protocol public parameters per curve, so re-creating defeats the cache (and the LRU on the backend warms more slowly). Construct one per process for server use; one per React tree via MPCKitProvider. |
| Mixing testnet and mainnet API keys | Keys are scoped per network. Crossing the streams returns a MPCKitError with code auth.network_mismatch. |
Treating the crypto Rust feature as default |
It is opt-in. default = [] ships HTTP only. Add features = ["crypto"] (or mpckit = { version = "...", features = ["crypto"] }) when you want api.onboard(...) / api.sign(...). |
| Sending the raw signature on-chain without per-chain encoding | MPCKit returns 64-byte Schnorr / 65-byte ECDSA bytes. The destination chain (Solana ed25519, Ethereum 65-byte rsv, Bitcoin DER) imposes its own wrapping. |
Build verification
After integrating, check that:
health()/health()-equivalent returns{ ok: true }against the right base URL.networkInfo()reports the network you expected.balance()is non-zero (deposit if needed; seereferences/flows.md).- A small SECP256K1 + ECDSASecp256k1 + SHA256 sign round-trips, returning 65 bytes and a non-null
signature.
If sign() returns but signature is zero-length, the dWallet was not yet Active; that means onboard() did not wait for acceptUserShare. Re-run onboard or call getDwallet() until state.kind === "Active" before signing.
Source: Iamknownasfesal/mpckit — distributed by TomeVault.