Custody Starter Architecture
Create Whitelists
Overview
Whitelist policies restrict which addresses a wallet can send funds to. In the custody starter architecture, you create bidirectional whitelists between wallets so funds can only flow between designated wallets.
This guide covers creating whitelists for:
- The custody wallet to allow withdrawals only to the standby wallet.
- The standby wallet to allow withdrawals to the custody wallet and the deposit/withdraw wallet.
Custody-Standby Whitelist
Create bidirectional whitelists between the custody wallet and standby wallet so funds can only flow between these two wallets.
Prerequisites
- Get Started
- Set Up Custody Wallet
- Set Up Standby Wallet
- Custody Wallet ID and receive address.
- Standby Wallet ID and receive address.
Cookbooks
Need just the steps? Expand a cookbook below to get started:
Create Whitelists: Custody ↔ StandbyOpen Cookbook Create Whitelists: Standby ↔ Deposit/WithdrawOpen CookbookStep 1: Create Whitelist on Custody Wallet
Add the standby wallet address to the custody wallet whitelist. This ensures funds from the custody wallet can only go to the standby wallet.
Endpoint: Add Wallet-Policy Rule
export COIN="<ASSET_ID>"
export CUSTODY_WALLET_ID="<CUSTODY_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export STANDBY_WALLET_ADDRESS="<STANDBY_WALLET_ADDRESS>"
curl -X POST \
"https://app.bitgo-test.com/api/v2/$COIN/wallet/$CUSTODY_WALLET_ID/policy/rule" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"coin": "'"$COIN"'",
"id": "Custody to Standby Whitelist",
"type": "advancedWhitelist",
"condition": {
"add": {
"type": "address",
"item": "'"$STANDBY_WALLET_ADDRESS"'",
"metaData": {
"label": "Standby Wallet"
}
}
},
"action": {
"type": "deny"
}
}'
const BitGoJS = require('bitgo');
const bitgo = new BitGoJS.BitGo({ env: 'prod' });
const accessToken = '<YOUR_ACCESS_TOKEN>';
const coin = '<ASSET_ID>';
const custodyWalletId = '<CUSTODY_WALLET_ID>';
const standbyWalletAddress = '<STANDBY_WALLET_ADDRESS>';
async function createCustodyWhitelist() {
bitgo.authenticateWithAccessToken({ accessToken });
const wallet = await bitgo.coin(coin).wallets().get({ id: custodyWalletId });
console.log(`Setting whitelist policy on custody wallet ${wallet.label()}`);
const policy = {
action: {
type: 'deny'
},
condition: {
add: {
item: standbyWalletAddress,
type: 'address',
metaData: {
label: 'Standby Wallet'
}
}
},
id: 'Custody to Standby Whitelist',
type: 'advancedWhitelist'
};
const result = await wallet.createPolicyRule(policy);
console.log('Whitelist created:', result);
}
createCustodyWhitelist();
Step Result
The whitelist policy is now active on the custody wallet. BitGo automatically denies any withdrawal attempt to an address not on the whitelist.
{
"id": "66e9f4bb549f6c3957f8977fe3ed6ab4",
"coin": "btc",
"label": "Custody Wallet - Cold Storage",
"admin": {
"policy": {
"rules": [
{
"id": "Custody to Standby Whitelist",
"lockDate": "2025-09-18T22:05:21.219Z",
"coin": "btc",
"type": "advancedWhitelist",
"action": { "type": "deny", "userIds": [] },
"condition": {
"entries": [
{
"item": "bc1p...",
"type": "address",
"metaData": {
"label": "Standby Wallet"
}
}
]
}
}
]
}
}
}
Note
For self-custody wallets, new whitelist policies lock 48 hours after creation and can only be unlocked by BitGo support. Custody wallets have different lock periods managed by BitGo.
Step 2: Create Whitelist on Standby Wallet (to Custody)
Add the custody wallet address to the standby wallet whitelist. This allows funds to flow back from the standby wallet to the custody wallet.
Endpoint: Add Wallet-Policy Rule
export COIN="<ASSET_ID>"
export STANDBY_WALLET_ID="<STANDBY_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export CUSTODY_WALLET_ADDRESS="<CUSTODY_WALLET_ADDRESS>"
curl -X POST \
"https://app.bitgo-test.com/api/v2/$COIN/wallet/$STANDBY_WALLET_ID/policy/rule" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"coin": "'"$COIN"'",
"id": "Standby to Custody Whitelist",
"type": "advancedWhitelist",
"condition": {
"add": {
"type": "address",
"item": "'"$CUSTODY_WALLET_ADDRESS"'",
"metaData": {
"label": "Custody Wallet"
}
}
},
"action": {
"type": "deny"
}
}'
const BitGoJS = require('bitgo');
const bitgo = new BitGoJS.BitGo({ env: 'prod' });
const accessToken = '<YOUR_ACCESS_TOKEN>';
const coin = '<ASSET_ID>';
const standbyWalletId = '<STANDBY_WALLET_ID>';
const custodyWalletAddress = '<CUSTODY_WALLET_ADDRESS>';
async function createStandbyToCustodyWhitelist() {
bitgo.authenticateWithAccessToken({ accessToken });
const wallet = await bitgo.coin(coin).wallets().get({ id: standbyWalletId });
console.log(`Setting whitelist policy on standby wallet ${wallet.label()}`);
const policy = {
action: {
type: 'deny'
},
condition: {
add: {
item: custodyWalletAddress,
type: 'address',
metaData: {
label: 'Custody Wallet'
}
}
},
id: 'Standby to Custody Whitelist',
type: 'advancedWhitelist'
};
const result = await wallet.createPolicyRule(policy);
console.log('Whitelist created:', result);
}
createStandbyToCustodyWhitelist();
Step Result
{
"id": "6849948ac0623f81f74f63dbd8351d4f",
"coin": "btc",
"label": "Standby Wallet - Hot",
"admin": {
"policy": {
"rules": [
{
"id": "Standby to Custody Whitelist",
"lockDate": "2025-09-18T22:05:21.219Z",
"coin": "btc",
"type": "advancedWhitelist",
"action": { "type": "deny", "userIds": [] },
"condition": {
"entries": [
{
"item": "bc1q...",
"type": "address",
"metaData": {
"label": "Custody Wallet"
}
}
]
}
}
]
}
}
}
Note
You add the deposit/withdraw wallet address to the standby wallet whitelist in the Standby-Deposit tab.
Standby-Deposit Whitelist
Update the standby wallet whitelist to include the deposit/withdraw wallet address. This enables funds to flow between the standby wallet and the deposit/withdraw wallet.
After completing this section, the standby wallet can send funds to:
- The custody wallet (configured in the Custody-Standby tab).
- The deposit/withdraw wallet (configured in this section).
Prerequisites
- Get Started
- Set Up Custody Wallet
- Set Up Standby Wallet
- Custody-Standby Whitelist (see Custody-Standby tab).
- Set Up Deposit/Withdraw Wallet
- Standby Wallet ID.
- Deposit/Withdraw Wallet receive address.
Update Standby Wallet Whitelist
Add the deposit/withdraw wallet address to the standby wallet existing whitelist. This is an update to the whitelist policy you created earlier.
Endpoint: Update Wallet-Policy Rule
export COIN="<ASSET_ID>"
export STANDBY_WALLET_ID="<STANDBY_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export WHITELIST_RULE_ID="Standby to Custody Whitelist"
export DEPOSIT_WITHDRAW_ADDRESS="<DEPOSIT_WITHDRAW_WALLET_ADDRESS>"
curl -X PUT \
"https://app.bitgo-test.com/api/v2/$COIN/wallet/$STANDBY_WALLET_ID/policy/rule" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"id": "'"$WHITELIST_RULE_ID"'",
"type": "advancedWhitelist",
"condition": {
"add": {
"type": "address",
"item": "'"$DEPOSIT_WITHDRAW_ADDRESS"'",
"metaData": {
"label": "Deposit-Withdraw Wallet"
}
}
},
"action": {
"type": "deny"
}
}'
const BitGoJS = require('bitgo');
const bitgo = new BitGoJS.BitGo({ env: 'prod' });
const accessToken = '<YOUR_ACCESS_TOKEN>';
const coin = '<ASSET_ID>';
const standbyWalletId = '<STANDBY_WALLET_ID>';
const depositWithdrawAddress = '<DEPOSIT_WITHDRAW_WALLET_ADDRESS>';
async function updateStandbyWhitelist() {
bitgo.authenticateWithAccessToken({ accessToken });
const wallet = await bitgo.coin(coin).wallets().get({ id: standbyWalletId });
console.log(`Updating whitelist policy on standby wallet ${wallet.label()}`);
const policy = {
action: {
type: 'deny'
},
condition: {
add: {
item: depositWithdrawAddress,
type: 'address',
metaData: {
label: 'Deposit-Withdraw Wallet'
}
}
},
id: 'Standby to Custody Whitelist', // Use existing rule ID to update
type: 'advancedWhitelist'
};
const result = await wallet.setPolicyRule(policy);
console.log('Whitelist updated:', result);
}
updateStandbyWhitelist();
Step Result
The standby wallet whitelist now includes both the custody wallet and the deposit/withdraw wallet addresses.
{
"id": "6849948ac0623f81f74f63dbd8351d4f",
"coin": "btc",
"label": "Standby Wallet - Hot",
"admin": {
"policy": {
"rules": [
{
"id": "Standby to Custody Whitelist",
"lockDate": "2025-09-18T22:05:21.219Z",
"coin": "btc",
"type": "advancedWhitelist",
"action": { "type": "deny", "userIds": [] },
"condition": {
"entries": [
{
"item": "bc1q...",
"type": "address",
"metaData": {
"label": "Custody Wallet"
}
},
{
"item": "bc1p...",
"type": "address",
"metaData": {
"label": "Deposit-Withdraw Wallet"
}
}
]
}
}
]
}
}
}
Note
For self-custody wallets, whitelist updates are subject to the same 48-hour lock period as new whitelists.
Verify Whitelists
Use the Get Wallet endpoint to verify whitelist policies.
Endpoint: Get Wallet by ID
export COIN="<ASSET_ID>"
export WALLET_ID="<WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
curl -X GET \
"https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN"
const wallet = await bitgo.coin('<ASSET_ID>').wallets().get({ id: '<WALLET_ID>' });
console.log('Wallet policies:', wallet.admin().policy);
Step Result
Check the admin.policy.rules array in the response to confirm the whitelist entries.
{
"id": "6849948ac0623f81f74f63dbd8351d4f",
"coin": "btc",
"label": "Standby Wallet - Hot",
"admin": {
"policy": {
"rules": [
{
"id": "Standby to Custody Whitelist",
"type": "advancedWhitelist",
"condition": {
"entries": [
{
"item": "bc1q...",
"type": "address",
"metaData": { "label": "Custody Wallet" }
},
{
"item": "bc1p...",
"type": "address",
"metaData": { "label": "Deposit-Withdraw Wallet" }
}
]
}
}
]
}
}
}
Whitelist Behavior
When you configure a whitelist policy with action.type: "deny":
- Transactions to addresses on the whitelist proceed normally (subject to other policies).
- Transactions to addresses not on the whitelist are automatically denied.
You can also configure whitelists with action.type: "getApproval" to require admin approval for non-whitelisted addresses instead of denying them outright.
Verify Complete Architecture
At this point, your custody starter architecture is fully configured. Verify the whitelist configuration:
| Wallet | Can Send To |
|---|---|
| Custody Wallet | Standby Wallet only |
| Standby Wallet | Custody Wallet, Deposit/Withdraw Wallet |
| Deposit/Withdraw Wallet | Any address (no whitelist) |
Fund Flow Summary
With the complete architecture in place, funds flow as follows:
┌─────────────────────────────────────────────────────────────┐
│ FUND FLOW DIAGRAM │
├─────────────────────────────────────────────────────────────┤
│ │
│ INBOUND (Deposits): │
│ External ──────────────────────► Deposit/Withdraw Wallet │
│ │
│ INTERNAL (Accumulation): │
│ Deposit/Withdraw ─────────────► Standby Wallet │
│ Standby Wallet ───────────────► Custody Wallet │
│ │
│ INTERNAL (Replenishment): │
│ Custody Wallet ───────────────► Standby Wallet │
│ Standby Wallet ───────────────► Deposit/Withdraw Wallet │
│ │
│ OUTBOUND (Withdrawals): │
│ Deposit/Withdraw Wallet ──────► External (Customers) │
│ │
└─────────────────────────────────────────────────────────────┘