BTCP Developer Guide
Non-custodial MPC wallet infrastructure. Private keys are never reconstructed.
Base URL: https://api.btcp.io
Authentication: Email OTP → JWT Bearer Token
Content-Type: application/json
Quick Start
Get from zero to a working MPC wallet in 3 API calls:
Step 1: Get an access token
# Request OTP
curl -X POST https://api.btcp.io/auth/request-otp \
-H "Content-Type: application/json" \
-d '{"email": "you@company.com"}'
# Verify OTP (check your email for the 6-digit code)
curl -X POST https://api.btcp.io/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{"email": "you@company.com", "code": "123456"}'
# Response:
# {
# "accessToken": "eyJ...",
# "refreshToken": "eyJ...",
# "walletId": null
# }
Step 2: Create a wallet
curl -X POST https://api.btcp.io/api/v1/wallets \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chain": "polygon-amoy", "network": "polygon-amoy"}'
# Response includes server's public share and Paillier key for client-side keygen
Step 3: Complete key generation
# Client generates their key share, then sends the public part:
curl -X POST https://api.btcp.io/api/v1/wallets/WALLET_ID/keygen/complete \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"client_public_share_x": "0x...",
"client_public_share_y": "0x..."
}'
# → Wallet is now active with a joint public key and address
Authentication: Email OTP
Request a one-time passcode. The API always returns 200 regardless of whether the email exists (prevents enumeration).
// Request
{ "email": "user@example.com" }
// Response (200)
{ "message": "OTP sent" }
Verify the OTP and receive JWT tokens. If the user doesn't exist, an account is created automatically.
// Request
{ "email": "user@example.com", "code": "123456" }
// Response (200)
{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"walletId": "uuid-or-null"
}
JWT Tokens
Access tokens expire in 15 minutes. Use the refresh token to get a new pair:
{ "refresh_token": "eyJhbG..." }
// → { "accessToken": "...", "refreshToken": "..." }
Get current user info. Requires Bearer token.
MFA (TOTP)
Optional TOTP-based two-factor authentication.
Create Wallet
Initiates DKG (Distributed Key Generation) Round 1. The server generates its key share and Paillier keypair.
// Request
{
"chain": "polygon-amoy", // or "bsv", "polygon"
"network": "polygon-amoy",
"recovery_enabled": false
}
// Response
{
"wallet_id": "98adc154-...",
"status": "pending_keygen",
"server_public_share_x": "0x7060...",
"server_public_share_y": "0xddb0...",
"paillier_n": "0xcc47...",
"encrypted_server_share": "0x82ed..."
}
| Chain | Value | Networks |
|---|---|---|
| Bitcoin SV | bsv | mainnet, testnet |
| Polygon | polygon | polygon |
| Polygon Amoy | polygon-amoy | polygon-amoy |
Complete Key Generation
DKG Round 2: Client sends their public key share. The server computes the joint public key and derives the wallet address.
{
"client_public_share_x": "0x1234...", // secp256k1 point
"client_public_share_y": "0xabcd..."
}
Wallet Info & Balance
MPC Signing: Init
Start the MPC signing protocol. The server checks policies, then generates its nonce share.
{
"message_hash": "abcdef1234...", // 32-byte hex (no 0x prefix)
"total_output_satoshis": 100000, // optional, for policy check
"destination_addresses": ["1A1z..."] // optional
}
MPC Signing: Complete
Client sends their partial signature. Server combines with its share to produce the final ECDSA signature. At no point does the complete private key exist.
Policy Engine
Server-enforced rules that can block or gate transactions:
| Rule | Description |
|---|---|
max_per_tx | Maximum value per transaction |
daily_limit | 24-hour rolling spend limit |
hourly_limit | 1-hour rolling spend limit |
whitelist | Only allow transfers to approved addresses |
cooldown | Minimum time between transactions |
require_otp | Require fresh OTP for high-value transactions |
time_window | Only allow signing during business hours |
require_passkey | Require WebAuthn assertion before signing |
WebAuthn / Passkey
Protect the client key share with biometrics (Touch ID, Face ID, hardware keys).
2-of-3 Recovery
Shamir secret sharing ensures users can recover funds even if the service shuts down. Three shares: user, server, encrypted backup.
API Keys
For server-to-server integration. Keys are prefixed with bm_live_ (production) or bm_test_ (sandbox).
Webhooks
Receive real-time notifications via HMAC-SHA256 signed HTTP POST requests.
{
"url": "https://yourapp.com/webhook",
"events": ["transaction.signed", "wallet.created", "security.alert"]
}
Verify webhook signatures using the X-BM-Signature header:
import hmac, hashlib
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
assert expected == request.headers["X-BM-Signature"]
TypeScript SDK
npm install @bsv-mpc/sdk
import { MPCWalletClient } from '@bsv-mpc/sdk';
const client = new MPCWalletClient({
baseUrl: 'https://api.btcp.io',
});
// Authenticate
await client.auth.requestOTP('user@example.com');
await client.auth.verifyOTP('user@example.com', '123456');
// Create wallet
const wallet = await client.wallet.create({ chain: 'polygon-amoy' });
// wagmi integration (drop-in Privy replacement)
import { MPCWalletProvider } from '@bsv-mpc/sdk/wagmi';
<MPCWalletProvider baseUrl="https://api.btcp.io">
<WagmiProvider config={config}>
<App />
</WagmiProvider>
</MPCWalletProvider>
Error Handling
Errors follow RFC 7807 format:
{
"type": "about:blank",
"title": "Policy violation",
"status": 403,
"detail": "Transaction exceeds daily limit of 1000000 satoshis",
"instance": "https://api.btcp.io/api/v1/wallets/.../sign/init"
}
| Status | Meaning |
|---|---|
401 | Invalid or expired token |
403 | Policy violation / passkey required |
404 | Resource not found |
428 | OTP required (high-value transaction) |
429 | Rate limit exceeded |