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

All API responses use camelCase for auth endpoints and snake_case for wallet/signing endpoints. We're standardizing to camelCase in v2.

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

POST /auth/request-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" }
POST /auth/verify-otp

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:

POST /auth/refresh
{ "refresh_token": "eyJhbG..." }
// → { "accessToken": "...", "refreshToken": "..." }
GET /auth/me

Get current user info. Requires Bearer token.

MFA (TOTP)

Optional TOTP-based two-factor authentication.

POST /auth/mfa/setup — Returns secret + QR URI
POST /auth/mfa/verify — Confirm setup with TOTP code
POST /auth/mfa/disable — Disable MFA (requires TOTP code)

Create Wallet

POST /api/v1/wallets

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..."
}
ChainValueNetworks
Bitcoin SVbsvmainnet, testnet
Polygonpolygonpolygon
Polygon Amoypolygon-amoypolygon-amoy

Complete Key Generation

POST /api/v1/wallets/{wallet_id}/keygen/complete

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..."
}
The client must keep their private share secret. Store it encrypted (see WebAuthn/Passkey section).

Wallet Info & Balance

GET /api/v1/wallets/{wallet_id}
GET /api/v1/wallets/{wallet_id}/balance
GET /api/v1/wallets/{wallet_id}/address

MPC Signing: Init

POST /api/v1/wallets/{wallet_id}/sign/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
}
Policy engine runs at this step. If a policy blocks the transaction, you'll get 403 (PolicyViolation) or 428 (OTP required).

MPC Signing: Complete

POST /api/v1/wallets/{wallet_id}/sign/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:

RuleDescription
max_per_txMaximum value per transaction
daily_limit24-hour rolling spend limit
hourly_limit1-hour rolling spend limit
whitelistOnly allow transfers to approved addresses
cooldownMinimum time between transactions
require_otpRequire fresh OTP for high-value transactions
time_windowOnly allow signing during business hours
require_passkeyRequire WebAuthn assertion before signing
POST /api/v1/wallets/{wallet_id}/policies
GET /api/v1/wallets/{wallet_id}/policies
PUT /api/v1/wallets/{wallet_id}/policies/{policy_id}
DELETE /api/v1/wallets/{wallet_id}/policies/{policy_id}

WebAuthn / Passkey

Protect the client key share with biometrics (Touch ID, Face ID, hardware keys).

POST /api/v1/webauthn/register/begin
POST /api/v1/webauthn/register/complete
POST /api/v1/webauthn/authenticate/begin
POST /api/v1/webauthn/authenticate/complete

2-of-3 Recovery

Shamir secret sharing ensures users can recover funds even if the service shuts down. Three shares: user, server, encrypted backup.

POST /api/v1/wallets/{wallet_id}/recovery/export — Generate recovery shares
POST /api/v1/wallets/{wallet_id}/recovery/verify — Test recovery
POST /api/v1/wallets/{wallet_id}/recovery/initiate — Start recovery

API Keys

For server-to-server integration. Keys are prefixed with bm_live_ (production) or bm_test_ (sandbox).

POST /api/v1/api-keys
GET /api/v1/api-keys
DELETE /api/v1/api-keys/{key_id}

Webhooks

Receive real-time notifications via HMAC-SHA256 signed HTTP POST requests.

POST /api/v1/webhooks
{
  "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"
}
StatusMeaning
401Invalid or expired token
403Policy violation / passkey required
404Resource not found
428OTP required (high-value transaction)
429Rate limit exceeded