₿ Bitcoin Integration

IBEx.Fi supports Bitcoin with two address types derived from the same master secret: P2WPKH (SegWit) and P2TR (Taproot). Transactions are built and signed client-side using PSBTs — the server never holds Bitcoin private keys.

How Bitcoin Addresses Are Derived

From the user's master bytes (PRF or sealed envelope), two Bitcoin addresses are derived via HKDF:

Type Info Label Address Format Example
P2WPKH (SegWit v0) bitcoin:global bech32 — bc1q... bc1qxy2kgdygjr...
P2TR (Taproot v1) bitcoin:taproot bech32m — bc1p... bc1pxy2kgdygjr...
// P2WPKH derivation
sk  = HKDF(SHA-256, masterBytes, "ibexfi:derivation:v1", "bitcoin:global", 32)
pub = secp256k1.getPublicKey(sk, true)         // 33 bytes compressed
hash = RIPEMD-160(SHA-256(pub))                // 20 bytes
addr = bech32("bc", 0, hash)                   // bc1q...

// Taproot derivation
sk  = HKDF(SHA-256, masterBytes, "ibexfi:derivation:v1", "bitcoin:taproot", 32)
P   = secp256k1.getPublicKey(sk).x             // 32-byte x-only pubkey
t   = taggedHash("TapTweak", P)                // BIP-341 tweak
Q   = P + t·G                                  // tweaked public key
addr = bech32m("bc", 1, Q.x)                   // bc1p...

Architecture

┌───────────────────────────────────────────────────────────────┐
│  Client                                                       │
│                                                               │
│  1. GET /v1.2/safes/bitcoin/utxos?address=bc1q...               │
│  2. GET /v1.2/safes/bitcoin/fees                                │
│  3. POST /v1.2/safes/bitcoin/send/prepare  (or BITCOIN_SEND)    │
│     ← Server selects UTXOs, estimates fees, computes change   │
│  4. Client builds PSBT from "prepared" response               │
│  5. Client signs PSBT locally                                 │
│  6. POST /v1.2/safes/bitcoin/tx/broadcast                       │
│     ← Server broadcasts raw tx → returns txid                │
└───────────────────────────────────────────────────────────────┘

API Endpoints

Endpoint Method Purpose
/v1.2/safes/bitcoin/info GET Bitcoin network info (getblockchaininfo)
/v1.2/safes/bitcoin/fees GET Fee estimation — fast / standard / slow (sat/vB)
/v1.2/safes/bitcoin/utxos GET List UTXOs for an address (scantxoutset)
/v1.2/safes/bitcoin/send/prepare POST Prepare a send — UTXO selection, fee calc, change
/v1.2/safes/bitcoin/tx/broadcast POST Broadcast a signed raw transaction (hex)

All endpoints require JWT authentication via Authorization: Bearer <token>.

Fee Estimation

GET /v1.2/safes/bitcoin/fees

Response:
{
  "feeRateSatVb": {
    "fast": 22,       // ~1 block confirmation
    "standard": 12,   // ~6 blocks
    "slow": 8         // ~12 blocks
  }
}

Preparing a Transaction

POST /v1.2/safes/bitcoin/send/prepare

Body:
{
  "from": "bc1q...",
  "to": "bc1q...",
  "amountSat": 10000,
  "sendAll": false,
  "feeProfile": "standard",
  "network": "mainnet"
}

Response:
{
  "from": "bc1q...",
  "to": "bc1q...",
  "amountSat": 10000,
  "feeSat": 1234,
  "inputsUsed": [
    { "txid": "...", "vout": 0, "value": 22345, "scriptPubKey": "0014..." }
  ],
  "outputs": [{ "address": "bc1q...", "value": 10000 }],
  "change": { "address": "bc1q...", "value": 11111 }
}
Note: When sendAll: true, the entire balance minus fees is sent and no change output is included.

How Fees Are Paid

Simple Mode (Wallet Pays Own Fees)

The sender's UTXOs fund both the transfer and the network fee. feeSat = sum(inputs) − sum(outputs).

Collaborative PSBT (Sponsored by Another Wallet)

  1. Wallet A defines business outputs (amount to recipient)
  2. Wallet B adds its own inputs to cover the fee + B's change
  3. B signs only B's inputs → returns PSBT to A
  4. A verifies outputs unchanged → signs A's inputs → finalizes → broadcasts
Neither A's nor B's private keys are exposed to the server. Use SIGHASH_ALL and let the wallet paying the business amount sign last.

BITCOIN_SEND via Safe Operations

Alternatively, use the standard POST /v1.2/safes/operations endpoint with a BITCOIN_SEND operation. This wraps the prepare flow with a WebAuthn challenge:

POST /v1.2/safes/operations

{
  "safeAddress": "0xSafe...",
  "operations": [{
    "type": "BITCOIN_SEND",
    "network": "mainnet",
    "from": "bc1q...",
    "to": "bc1q...",
    "amountSat": 10000,
    "feeProfile": "standard"
  }]
}

Response:
{
  "credentialRequestOptions": { /* WebAuthn challenge */ },
  "prepared": {
    "from": "bc1q...",
    "to": "bc1q...",
    "amountSat": 10000,
    "feeSat": 1234,
    "inputsUsed": [...],
    "outputs": [...],
    "change": {...}
  }
}

Use the prepared object to build your PSBT client-side, then broadcast via /v1.2/safes/bitcoin/tx/broadcast.

Client-Side PSBT Signing

  1. Inputs: For each inputsUsed[i], add a PSBT input with txid, vout, and witness data
  2. Outputs: Add the main to output + optional change
  3. Sign: Sign with the private key controlling from
  4. Finalize & extract hex
  5. Broadcast: POST /v1.2/safes/bitcoin/tx/broadcast with {"rawtx": "..."}

Refer to your PSBT library docs (bitcoinjs-lib, etc.) for exact input fields per type.

Key Differences vs EVM & Solana

Aspect EVM (Safe) Solana Bitcoin
Model Account-based Account-based UTXO-based
Signing location Authenticator (WebAuthn) Client-side Client-side (PSBT)
Gas / fees Paymaster (gasless) Fee payer (2-step) Wallet-paid or collaborative PSBT
Address types 0x… (one) Base58 (one) P2WPKH + Taproot (two)
Server signs? Yes (Safe UserOp) Fee payer only Never

Error Handling

Status Cause
400 Missing fields, no UTXOs, insufficient funds
401 Missing or invalid JWT
500 RPC failure, broadcast error
501 /psbt/build — reserved, not yet implemented

See Also