Set Up Custody Wallet

Overview

The custody wallet serves as your cold storage for the majority of your assets. BitGo manages all keys for custody wallets, providing the highest level of security. Withdrawals from custody wallets require video verification with a BitGo operator.

In the custody starter architecture, this wallet:

  • Holds the majority of enterprise assets.
  • Has the strictest policies (admin approvals, velocity limits).
  • Only allows withdrawals to the standby wallet through whitelist.

Prerequisites

Create Custody Wallet

Create a custody wallet using the Add Wallet endpoint. BitGo automatically creates and manages all keys for custody wallets.

Endpoint: Add Wallet

export COIN="<ASSET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ENTERPRISE="<YOUR_ENTERPRISE_ID>"
export LABEL="<YOUR_DESIRED_WALLET_NAME>"

curl -X POST \
  https://app.bitgo.com/api/v2/$COIN/wallet/add \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "enterprise": "'"$ENTERPRISE"'",
    "label": "'"$LABEL"'",
    "isCustodial": true
}'
const { BitGo } = require('bitgo');

const accessToken = '<YOUR_ACCESS_TOKEN>';

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

async function createCustodyWallet() {
  const options = {
    label: 'Custody Wallet - Cold Storage',
    isCustodial: true,
    enterprise: '<YOUR_ENTERPRISE_ID>'
  };

  const newWallet = await bitgo.coin('<ASSET_ID>').wallets().add(options);
  console.log('Custody Wallet Created:');
  console.log('Wallet ID:', newWallet.id());
  console.log('Receive Address:', newWallet.receiveAddress());
}

createCustodyWallet();

Step Result

{
  "id": "62f03dee8d13be0007894b3dbc21be17",
  "users": [
    {
      "user": "62ab90e06dfda30007974f0a52a12995",
      "permissions": ["admin", "view", "spend"]
    }
  ],
  "coin": "btc",
  "label": "Custody Wallet - Cold Storage",
  "m": 2,
  "n": 3,
  "keys": [
    "62f03dee8d13be0007894b41f63bc504",
    "62f03dee8d13be0007894b43cb0f65d0",
    "62f03dee8d13be0007894b3f8024e0e8"
  ],
  "enterprise": "62c5ae8174ac860007aff138a2d74df7",
  "approvalsRequired": 1,
  "isCold": true,
  "type": "custodial",
  "admin": {
    "policy": {
      "rules": [
        {
          "id": "Custody Wallet Transaction Custodian Approval",
          "mutabilityConstraint": "permanent",
          "type": "allTx",
          "action": {
            "type": "getCustodianApproval"
          }
        },
        {
          "id": "Custody Wallet Whitelist",
          "mutabilityConstraint": "managed",
          "type": "advancedWhitelist",
          "action": {
            "type": "deny"
          },
          "condition": {
            "entries": []
          }
        }
      ]
    }
  },
  "receiveAddress": {
    "address": "bc1q..."
  }
}
📘

Note

Save the wallet ID from the response. You need this to configure whitelist policies and to reference this wallet in subsequent steps.

Configure Recommended Policies

After creating the custody wallet, configure the following policies to maximize security.

Velocity Limit Policy

Create a velocity limit to restrict how much can be withdrawn from the custody wallet within a time window. This protects against rapid depletion of funds.

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 CUSTODY_WALLET_ID="<CUSTODY_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": "Custody Wallet - Daily Velocity Limit",
    "adminOnly": false,
    "clauses": [
        {
            "conditions": [
                {
                    "name": "transfer.velocityLimit",
                    "parameters": {
                        "operator": ">",
                        "amount": "1000000000",
                        "coin": "btc",
                        "timeWindow": "86400"
                    }
                }
            ],
            "actions": [
                {
                    "name": "approvals.customer.enterpriseUser",
                    "parameters": {
                        "userIds": ["'"$APPROVER_USER_ID"'"],
                        "minRequired": "2",
                        "initiatorIsAllowedToApprove": false
                    }
                }
            ]
        }
    ],
    "filteringConditions": [
        {
            "name": "wallet.ids",
            "parameters": {
                "walletId": ["'"$CUSTODY_WALLET_ID"'"]
            }
        }
    ]
}'

Step Result

{
  "id": "684a1b2c3d4e5f6g7h8i9j0k",
  "name": "Custody Wallet - Daily Velocity Limit",
  "adminOnly": false,
  "touchpoint": "wallet.segregated.transfer",
  "enterprise": "62c5ae8174ac860007aff138a2d74df7",
  "clauses": [
    {
      "conditions": [
        {
          "name": "transfer.velocityLimit",
          "parameters": {
            "operator": ">",
            "amount": "1000000000",
            "coin": "btc",
            "timeWindow": "86400"
          }
        }
      ],
      "actions": [
        {
          "name": "approvals.customer.enterpriseUser",
          "parameters": {
            "userIds": ["62ab90e06dfda30007974f0a52a12995"],
            "minRequired": "2",
            "initiatorIsAllowedToApprove": false
          }
        }
      ]
    }
  ],
  "filteringConditions": [
    {
      "name": "wallet.ids",
      "parameters": {
        "walletId": ["62f03dee8d13be0007894b3dbc21be17"]
      }
    }
  ]
}

Admin Approval Policy

Require multiple admin approvals for all withdrawals from the custody 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_1="<APPROVER_USER_ID_1>"
export APPROVER_USER_ID_2="<APPROVER_USER_ID_2>"
export CUSTODY_WALLET_ID="<CUSTODY_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": "Custody Wallet - Require Multiple Admin Approval",
    "adminOnly": false,
    "clauses": [
        {
            "conditions": [],
            "actions": [
                {
                    "name": "approvals.customer.enterpriseUser",
                    "parameters": {
                        "userIds": ["'"$APPROVER_USER_ID_1"'", "'"$APPROVER_USER_ID_2"'"],
                        "minRequired": "2",
                        "initiatorIsAllowedToApprove": false
                    }
                }
            ]
        }
    ],
    "filteringConditions": [
        {
            "name": "wallet.ids",
            "parameters": {
                "walletId": ["'"$CUSTODY_WALLET_ID"'"]
            }
        }
    ]
}'

Step Result

{
  "id": "684a1b2c3d4e5f6g7h8i9j0l",
  "name": "Custody Wallet - Require Multiple Admin Approval",
  "adminOnly": false,
  "touchpoint": "wallet.segregated.transfer",
  "enterprise": "62c5ae8174ac860007aff138a2d74df7",
  "clauses": [
    {
      "conditions": [],
      "actions": [
        {
          "name": "approvals.customer.enterpriseUser",
          "parameters": {
            "userIds": ["62ab90e06dfda30007974f0a52a12995", "627ff9325a5c1b0007c05a40d15e1522"],
            "minRequired": "2",
            "initiatorIsAllowedToApprove": false
          }
        }
      ]
    }
  ],
  "filteringConditions": [
    {
      "name": "wallet.ids",
      "parameters": {
        "walletId": ["62f03dee8d13be0007894b3dbc21be17"]
      }
    }
  ]
}

Withdrawal Flow

Withdrawals from custody wallets require video verification with a BitGo operator. The flow differs based on if the wallet is multisignature or MPC.

📘

Note

You can only transact from custody wallets in the production environment. BitGo doesn't sign transactions from custody wallets in testnet due to the enhanced security protocols that are required. You can still create testnet custody wallets and initiate transactions, but these transactions remain in an unsigned state.

1. Initiate Transaction

Submit transaction details to BitGo. BitGo uses the data you pass to construct an unsigned transaction that wallet admins can approve.

Endpoint: Initiate a Transaction

export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ADDRESS="<DESTINATION_ADDRESS>"
export AMOUNT="<AMOUNT_IN_BASE_UNITS>"
export VIDEO_APPROVER_1="<PUBLIC_ID_1>"
export VIDEO_APPROVER_2="<PUBLIC_ID_2>"

curl -X POST \
  https://app.bitgo.com/api/v2/$COIN/wallet/$WALLET_ID/tx/initiate \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "recipients": [
        {
          "amount": "'"$AMOUNT"'",
          "address": "'"$ADDRESS"'"
        }
      ],
    "videoApprovers": [
      "'"$VIDEO_APPROVER_1"'",
      "'"$VIDEO_APPROVER_2"'"
    ]
  }'
let params = {
  recipients: [
    {
      amount: `<AMOUNT_IN_BASE_UNITS>`,
      address: `<DESTINATION_ADDRESS>`,
      videoApprovers: [
      `<PUBLIC_ID_1>`,
      `<PUBLIC_ID_2>`
  ],
    },
  ],
};
wallet.prebuildTransaction(params).then(function (transaction) {
  // print transaction details
  console.dir(transaction);
});

Step Result

The transaction remains in a pending-approval status until all necessary wallet admins approve it.

{
  "error": "triggered all transactions policy",
  "pendingApproval": {
    "id": "655686880765186f0b3e9e88e1bdd0f4",
    "coin": "btc",
    "wallet": "6553e933288be490293ae748efafeaaf",
    "enterprise": "62c5ae8174ac860007aff138a2d74df7",
    "creator": "62ab90e06dfda30007974f0a52a12995",
    "createDate": "2023-11-16T21:15:52.703Z",
    "info": {
      "type": "transactionRequest",
      "transactionRequest": {
        "requestedAmount": "10000",
        "fee": 45242,
        "sourceWallet": "6553e933288be490293ae748efafeaaf",
        "recipients": [
          {
            "address": "2N3sBpM1RnWRMXnEVoUWnM7xtYzL756JE2Q",
            "amount": "10000"
          }
        ]
      }
    },
    "state": "pending",
    "scope": "wallet",
    "approvalsRequired": 1
  },
  "triggeredPolicy": "6553e933288be490293ae753fefc9f1a"
}

2. Approve Transaction

📘

Note

If you configure an approval requirement for withdrawals, you can't approve your own transactions - another admin must approve them.

Endpoint: Update Pending Approval

export COIN="<ASSET_ID>"
export APPROVAL_ID="<APPROVAL_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export OTP="<OTP>"

curl -X PUT \
  https://app.bitgo.com/api/v2/$COIN/pendingapprovals/$APPROVAL_ID \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "state": "approved",
    "otp": "'"$OTP"'"
}'
const baseCoin = bitgo.coin('<ASSET_ID>');
const pendingApproval = await baseCoin.pendingApprovals().get({ id: '<APPROVAL_ID>' });
const result = await pendingApproval.approve({ otp: '<OTP>' });
console.log('Approval Result:', result);

Step Result

Once approved, BitGo rebuilds the unsigned transaction, applying the most up-to-date fees.

{
  "id": "655686880765186f0b3e9e88e1bdd0f4",
  "coin": "btc",
  "wallet": "6553e933288be490293ae748efafeaaf",
  "enterprise": "62c5ae8174ac860007aff138a2d74df7",
  "creator": "62ab90e06dfda30007974f0a52a12995",
  "createDate": "2023-11-16T21:15:52.703Z",
  "info": {
    "type": "transactionRequest",
    "transactionRequest": {
      "requestedAmount": "10000",
      "fee": 45242,
      "sourceWallet": "6553e933288be490293ae748efafeaaf",
      "recipients": [
        {
          "address": "2N3sBpM1RnWRMXnEVoUWnM7xtYzL756JE2Q",
          "amount": "10000"
        }
      ]
    }
  },
  "state": "approved",
  "scope": "wallet",
  "approvalsRequired": 1,
  "resolvers": [
    {
      "user": "627ff9325a5c1b0007c05a40d15e1522",
      "date": "2023-11-16T21:33:24.644Z",
      "resolutionType": "approved"
    }
  ]
}

3. Sign and Broadcast

After approval, log in to BitGo and schedule video ID verification. Once verification completes:

  1. BitGo uploads the unsigned transaction to the BitGo Offline Vault Console (OVC).
  2. A BitGo operator uses the user key to sign the unsigned transaction in the OVC, becoming a half-signed transaction.
  3. A different BitGo operator uses the BitGo key to sign the half-signed transaction in a hardware security module (HSM), creating a fully-signed transaction.
  4. BitGo broadcasts the fully-signed transaction to the network for confirmation.

Next

Set Up Standby Wallet

See Also