Set Up Standby Wallet

Overview

The standby wallet is a self-custody hot wallet that serves as an intermediate buffer between your custody wallet (cold storage) and your deposit/withdraw wallet (daily operations). This wallet holds an intermediate amount of funds and requires admin approval for withdrawals.

In the custody starter architecture, this wallet:

  • Holds an intermediate amount of assets (more than deposit/withdraw, less than custody).
  • Requires admin approval for all withdrawals.
  • Only allows withdrawals to the custody wallet and deposit/withdraw wallet through whitelist.

Prerequisites

Create Standby Wallet

Generate a self-custody hot wallet using BitGo Express or the SDK. This creates the wallet and keys in one step.

Endpoint: Generate Wallet

export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ENTERPRISE="<YOUR_ENTERPRISE_ID>"
export PASSPHRASE="<YOUR_WALLET_PASSPHRASE>"
export LABEL="<YOUR_DESIRED_WALLET_NAME>"

curl -X POST \
  http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/generate \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "enterprise": "'"$ENTERPRISE"'",
    "passphrase": "'"$PASSPHRASE"'",
    "label": "'"$LABEL"'",
    "multisigType": "onchain",
    "type": "hot"
}'
const { BitGo } = require('bitgo');

const accessToken = '<YOUR_ACCESS_TOKEN>';

const bitgo = new BitGo({
  accessToken: accessToken,
  env: 'prod',
});

async function createStandbyWallet() {
  const newWallet = await bitgo.coin('<ASSET_ID>').wallets().generateWallet({
    label: 'Standby Wallet - Hot',
    passphrase: '<YOUR_WALLET_PASSPHRASE>',
    enterprise: '<YOUR_ENTERPRISE_ID>',
    multisigType: 'onchain',
    type: 'hot'
  });

  console.log('Standby Wallet Created:');
  console.log('Wallet ID:', newWallet.wallet.id());
  console.log('Receive Address:', newWallet.wallet.receiveAddress());

  // IMPORTANT: Save the backup keychain securely
  console.log('Backup Keychain (SAVE SECURELY):', newWallet.backupKeychain);
}

createStandbyWallet();

Step Result

📘

Note

This response contains critical key material. Save the backup keychain in a secure place.

{
  "wallet": {
    "id": "6849948ac0623f81f74f63dbd8351d4f",
    "users": [
      {
        "user": "62ab90e06dfda30007974f0a52a12995",
        "permissions": ["admin", "spend", "view"]
      }
    ],
    "coin": "btc",
    "label": "Standby Wallet - Hot",
    "m": 2,
    "n": 3,
    "keys": [
      "68499487e6c77351bd3bbf04281fa8bb",
      "68499487cd07fc57de18f6481baa1903",
      "6849948902be620840a384c148d19979"
    ],
    "enterprise": "62c5ae8174ac860007aff138a2d74df7",
    "approvalsRequired": 1,
    "isCold": false,
    "type": "hot",
    "multisigType": "onchain",
    "receiveAddress": {
      "address": "bc1p..."
    }
  },
  "userKeychain": {
    "id": "68499487e6c77351bd3bbf04281fa8bb",
    "pub": "xpub661MyMwAqRbcEst4tb4F36AfvoFtAy7U9viB7zapRqNnXhPknsPwNqhxpD1CqMGSGhq3hDMKQR1Br8gGxYygoR6SGic3XdJoTEzM5v9wyFy",
    "source": "user",
    "encryptedPrv": "{...}"
  },
  "backupKeychain": {
    "id": "68499487cd07fc57de18f6481baa1903",
    "pub": "xpub661MyMwAqRbcEfnfBkVRk9BB1SrYaFR884ndYpmNXnci6U2wrAQCsFiD21c49Aq7EvtK6QFzDzMwmFKVqi1bTL7kmuCEJ78bFn9Rq6NyLDV",
    "source": "backup",
    "encryptedPrv": "{...}"
  },
  "bitgoKeychain": {
    "id": "6849948902be620840a384c148d19979",
    "pub": "xpub661MyMwAqRbcGCXsEAmctkLstGa92f5ugiD3hvCL3Wyt8BqHYGCVsL2x6PgNgJeq8Aabtz92sMzq4Ezac459QkDmxowKqoL35gNJXEJDdeo",
    "source": "bitgo",
    "isBitGo": true
  }
}
📘

Note

Save the wallet ID and backup keychain from the response. You need the wallet ID to configure whitelist policies.

Configure Admin Approval Policy

Create a policy that requires admin approval for all withdrawals from the standby wallet.

Endpoint: Create Policy Rule

export ENTERPRISE_ID="<YOUR_ENTERPRISE_ID>"
export TOUCHPOINT="wallet.segregated.transfer"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export APPROVER_USER_ID="<APPROVER_USER_ID>"
export STANDBY_WALLET_ID="<STANDBY_WALLET_ID>"

curl -X POST \
  "https://app.bitgo.com/api/policy/v1/enterprises/$ENTERPRISE_ID/touchpoints/$TOUCHPOINT/rules" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "name": "Standby Wallet - Require Admin Approval",
    "adminOnly": false,
    "clauses": [
        {
            "conditions": [],
            "actions": [
                {
                    "name": "approvals.customer.enterpriseUser",
                    "parameters": {
                        "userIds": ["'"$APPROVER_USER_ID"'"],
                        "minRequired": "1",
                        "initiatorIsAllowedToApprove": false
                    }
                }
            ]
        }
    ],
    "filteringConditions": [
        {
            "name": "wallet.ids",
            "parameters": {
                "walletId": ["'"$STANDBY_WALLET_ID"'"]
            }
        }
    ]
}'

Step Result

{
  "id": "684a1b2c3d4e5f6g7h8i9j0m",
  "name": "Standby Wallet - Require Admin Approval",
  "adminOnly": false,
  "touchpoint": "wallet.segregated.transfer",
  "enterprise": "62c5ae8174ac860007aff138a2d74df7",
  "clauses": [
    {
      "conditions": [],
      "actions": [
        {
          "name": "approvals.customer.enterpriseUser",
          "parameters": {
            "userIds": ["62ab90e06dfda30007974f0a52a12995"],
            "minRequired": "1",
            "initiatorIsAllowedToApprove": false
          }
        }
      ]
    }
  ],
  "filteringConditions": [
    {
      "name": "wallet.ids",
      "parameters": {
        "walletId": ["6849948ac0623f81f74f63dbd8351d4f"]
      }
    }
  ]
}

Withdrawal Flow

Withdrawals from self-custody hot wallets can use either the simple or manual flow. The simple flow builds, signs, and sends a transaction in one call. The manual flow provides granular control with separate build, sign, and send steps.

Overview

The simple withdrawal flow for self-custody multisignature hot wallets enables you to build, sign, and send transactions, all in one call, using BitGo Express. This flow suffices for most mulsitignature use cases.

1. Build, Sign, and Send Transaction

Build and sign the transaction and send it to BitGo, all in one call.

Endpoint: Send Transaction

export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export WALLET_PASSPHRASE="<YOUR_WALLET_PASSPHRASE>"
export ADDRESS="<DESTINATION_ADDRESS>"
export AMOUNT="<AMOUNT_IN_BASE_UNITS>"

curl -X POST \
  http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/$WALLET_ID/sendcoins \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "address": "'"$ADDRESS"'",
    "amount": "'"$AMOUNT"'",
    "walletPassphrase": "'"$WALLET_PASSPHRASE"'",
    "type": "transfer",
    "txFormat": "psbt"
}'
const tx = await wallet.send({
    address: `<DESTINATION_ADDRESS>`,
    amount: `<AMOUNT>`,
    walletPassphrase: process.env.PASSWORD,
    txFormat: `psbt`,
    type: `transfer`
  });
📘

Note: If you are building transactions for a UTXO asset in quick succession, BitGo recommends reserving unspents by passing the reservation and expireTime parameters. Reserving unspents avoids errors by ensuring the UTXO are not included in subsequent builds.

Step Result

BitGo uses the data you pass to build a half-signed transaction. If your withdrawal does not require approval, BitGo applies the final signature using the BitGo key and broadcasts the transaction to the blockchain.

{
  "transfer": {
    "entries": [
      {
        "address": "2N1poiHTi5ur8hz5QBhNoy88bYzqrWYvBbV",
        "wallet": "6553e933288be490293ae748efafeaaf",
        "value": -100000,
        "valueString": "-100000"
      },
      {
        "address": "2Myx8nY8ReERqUwu9H96Lb2K4yYjs3xY8GH",
        "value": 10000,
        "valueString": "10000",
        "isChange": false,
        "isPayGo": false
      }
    ],
    "id": "6553ee12d5a49ecc9baccdcbe0563448",
    "coin": "tbtc4",
    "wallet": "6553e933288be490293ae748efafeaaf",
    "walletType": "hot",
    "txid": "e7648c85edac7f9870e511b4ef95b62b1878556791bd52ac715cb2cd4b466e6f",
    "state": "signed"
  },
  "txid": "e7648c85edac7f9870e511b4ef95b62b1878556791bd52ac715cb2cd4b466e6f",
  "status": "signed"
}

2. Approve Transaction (Optional)

📘

Note: If you configure an approval requirement for withdrawals, you cannot approve your own transactions - another admin must approve them.

Endpoint: Update Pending Approval

export APPROVAL_ID="<APPROVAL_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export OTP="<YOUR_OTP>"

curl -X PUT \
  https://app.bitgo-test.com/api/v2/pendingApprovals/$APPROVAL_ID \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "state": "approved",
    "otp": "'"$OTP"'"
  }'
const baseCoin = this.bitgoSDK.coin(initialPendingApproval.coin);
const pendingApproval = await baseCoin.pendingApprovals().get({ id: initialPendingApproval.id });
const result = await pendingApproval.approve(params);

Step Result

Once approved, BitGo rebuilds the half-signed transaction, applying the most up-to-date fees. BitGo then applies the final signature using the BitGo key and broadcasts the transaction to the blockchain.

{
  "id": "655686880765186f0b3e9e88e1bdd0f4",
  "coin": "tbtc4",
  "wallet": "6553e933288be490293ae748efafeaaf",
  "state": "approved",
  "approvalsRequired": 1,
  "resolvers": [
    {
      "user": "627ff9325a5c1b0007c05a40d15e1522",
      "date": "2023-11-16T21:33:24.644Z",
      "resolutionType": "pending"
    }
  ]
}

Next

Create Whitelists

See Also