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..."
}
}
NoteSave 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.
NoteYou 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
NoteIf 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:
- BitGo uploads the unsigned transaction to the BitGo Offline Vault Console (OVC).
- A BitGo operator uses the user key to sign the unsigned transaction in the OVC, becoming a half-signed transaction.
- 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.
- BitGo broadcasts the fully-signed transaction to the network for confirmation.
Next
See Also
Updated 1 day ago