The XCP Wallet browser extension injects a provider at window.xcpwallet on all HTTPS pages. Websites use this provider to connect wallets, sign messages, compose transactions, and broadcast to the Bitcoin network.
The provider is injected at document_start — before page scripts run. Two detection methods:
// Synchronous (provider already injected)
if (window.xcpwallet) {
// ready
}
// Asynchronous (wait for injection)
window.addEventListener('xcp-wallet#initialized', () => {
// window.xcpwallet is now available
});
// Request re-announcement (for SPAs that mount after injection)
window.dispatchEvent(new Event('xcp-wallet#discover'));interface XcpProvider {
request(args: { method: string; params?: unknown[] }): Promise<unknown>
on(event: string, handler: (...args: any[]) => void): void
removeListener(event: string, handler: (...args: any[]) => void): void
}Connect to the wallet. Opens a popup for user approval on first connection.
Returns a connection proof — a BIP-322 signature proving the user controls the address. The proof message is generated by the extension (not the website) and includes the requesting origin, a random nonce, and a timestamp.
const result = await xcpwallet.request({ method: 'xcp_requestAccounts' });
// {
// accounts: ['bc1q...'],
// proof: {
// address: 'bc1q...',
// message: 'xcp-wallet\norigin:https://example.com\nnonce:a1b2c3d4\nissued:1711130400',
// signature: '<BIP-322 signature>',
// verification: {
// method: 'BIP-322',
// format: 'p2wpkh' // address format used for signing
// }
// }
// }Proof verification (server-side):
- Parse the
message— verifyoriginmatches your domain andissuedis recent (< 5 minutes) - Verify the BIP-322 signature against the
addressusing theverification.formathint - Optionally store the
nonceto prevent replay
The proof is auto-signed during connection — no additional user prompt beyond the connect approval.
Get currently connected accounts. No popup — returns empty array if not connected or wallet is locked.
const accounts = await xcpwallet.request({ method: 'xcp_accounts' });
// ['bc1q...'] or []Disconnect the current site.
await xcpwallet.request({ method: 'xcp_disconnect' });
// trueAll signing methods require an active connection and open a popup for user approval.
Sign a message with the active address using BIP-322.
const result = await xcpwallet.request({
method: 'xcp_signMessage',
params: ['Hello, world!']
});
// { signature: '<base64 BIP-322 signature>' }An optional second parameter can specify the address (must match the active address):
params: ['Hello, world!', 'bc1q...']Sign a raw transaction hex.
const result = await xcpwallet.request({
method: 'xcp_signTransaction',
params: [{ hex: '0200000001...' }]
});
// { hex: '<signed transaction hex>' }
// Also accepts a plain string:
params: ['0200000001...']Sign a PSBT (Partially Signed Bitcoin Transaction).
const result = await xcpwallet.request({
method: 'xcp_signPsbt',
params: [{
hex: '<PSBT hex>',
signInputs: { 'bc1q...': [0, 1] }, // optional: which inputs to sign
sighashTypes: [0x01] // optional: sighash types
}]
});
// { hex: '<signed PSBT hex>' }Broadcast a signed transaction to the Bitcoin network. Includes replay protection — the same transaction cannot be broadcast twice.
const result = await xcpwallet.request({
method: 'xcp_broadcastTransaction',
params: ['<signed transaction hex>']
});
// { txid: '<64-char transaction hash>' }Get BTC and token balances for the connected address.
const result = await xcpwallet.request({ method: 'xcp_getBalances' });
// { address: 'bc1q...', btc: { ... }, xcp: { ... }, tokens: [...] }await xcpwallet.request({ method: 'xcp_chainId' });
// '0x0' (Bitcoin mainnet)await xcpwallet.request({ method: 'xcp_getNetwork' });
// 'mainnet'// Account changed (address switch or connect/disconnect)
xcpwallet.on('accountsChanged', (accounts) => {
// accounts: string[] — empty on disconnect
});
// Wallet disconnected this site
xcpwallet.on('disconnect', () => {
// Connection revoked
});An SDK is available for React/Next.js applications at lib/wallet/sdk. Copy the sdk/ folder into your project:
lib/wallet/
├── sdk/
│ ├── constants.ts — validation patterns, error codes
│ ├── detect.ts — provider detection with race condition handling
│ ├── errors.ts — user-friendly error messages
│ ├── index.ts — public exports
│ ├── provider.ts — typed XcpWallet wrapper class
│ ├── types.ts — TypeScript interfaces
│ └── verify.ts — connection proof validation
├── wallet-context.tsx — React context provider (useWallet hook)
└── useCompose.ts — compose → sign → broadcast pipeline
import { WalletProvider, useWallet } from '@/lib/wallet/wallet-context';
// Wrap your app
<WalletProvider>{children}</WalletProvider>
// In components
const { status, address, connectionProof, connect, disconnect } = useWallet();
// status: 'not_detected' | 'disconnected' | 'connected'import { detectProvider, XcpWallet } from '@/lib/wallet/sdk';
const provider = await detectProvider();
const wallet = new XcpWallet(provider);
const { accounts, proof } = await wallet.connect();import { validateProof } from '@/lib/wallet/sdk';
// Client-side (structural checks only)
const result = await validateProof(proof, 'https://example.com', address);
// Server-side (with cryptographic verification)
const result = await validateProof(proof, origin, address, {
verifySignature: async (message, signature, addr) => {
// Use your BIP-322 verification library
return await verifyBIP322(message, signature, addr);
}
});- Origin validation: The content script provides the origin — page JavaScript cannot spoof it
- Connection proof: BIP-322 signature proving address ownership, message format controlled by extension
- Rate limiting: Connection, transaction, and API requests are rate-limited per origin
- Replay protection: Broadcast transactions are tracked to prevent double-submission
- Parameter validation: All inputs are type-checked and size-limited (max 1MB)
- CSP analysis: Sites without Content Security Policy generate console warnings