Custody Starter Architecture
Set Up Standby Wallet
Overview
The standby wallet is a self-custody 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
Cookbooks
Need just the steps? Expand a cookbook below to get started:
Set Up Standby Wallet (MPC)Open Cookbook Set Up Standby Wallet (Multisig)Open CookbookCreate Standby Wallet
Generate a self-custody 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
}
}
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?includeKeychains=true \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"enterprise": "'"$ENTERPRISE"'",
"passphrase": "'"$PASSPHRASE"'",
"label": "'"$LABEL"'",
"type": "hot",
"multisigType": "tss",
"walletVersion": 5
}'
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: 'tss',
type: 'hot',
walletVersion: 5
});
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": "665e33f768f9b4b1cc2ca87a6b87cdfb",
"users": [
{
"user": "660d7c9fa6fbf0d00b415146cf70405a",
"permissions": ["admin", "spend", "view"]
}
],
"coin": "eth",
"label": "Standby Wallet - Hot",
"m": 2,
"n": 3,
"keys": [
"665e33f6fcf87cac817eb3b2ed964042",
"665e33f6e7331ba062e62c48c6ccaf98",
"665e33f6fcf87cac817eb3b01c6b4c36"
],
"enterprise": "66325011d4b1ff58feae3b311d669aae",
"approvalsRequired": 1,
"isCold": false,
"type": "hot",
"multisigType": "tss",
"receiveAddress": {
"address": "0x1a52b70e708f4aec9d92260aa035b3c71e51df84"
}
},
"userKeychain": {
"id": "665e33f6fcf87cac817eb3b2ed964042",
"source": "user",
"type": "tss",
"encryptedPrv": "{...}"
},
"backupKeychain": {
"id": "665e33f6e7331ba062e62c48c6ccaf98",
"source": "backup",
"type": "tss",
"encryptedPrv": "{...}"
},
"bitgoKeychain": {
"id": "665e33f6fcf87cac817eb3b01c6b4c36",
"source": "bitgo",
"type": "tss",
"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 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 wallets enables you to build, sign, and send transactions, all in one call, using BitGo Express. This flow suffices for most multisignature 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"
}
]
}
Overview
The manual withdrawal flow for self-custody multisignature wallets provides granular control by separating the build, sign, and send steps. Use this flow when you need more control over the transaction process.
1. Build Transaction
Build the transaction by specifying the transaction details and sending them to BitGo.
Endpoint: Build 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>"
curl -X POST \
https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/build \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"recipients": [
{
"amount": "'"$AMOUNT"'",
"address": "'"$ADDRESS"'"
}
]
}'
let params = {
recipients: [
{
amount: `<AMOUNT>`,
address: `<DESTINATION_ADDRESS>`,
},
],
};
wallet.prebuildTransaction(params).then(function (transaction) {
console.log(JSON.stringify(transaction, null, 2));
});
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 an unsigned transaction.
{
"txHex": "01000000010e4d3af014f9efe311062965d561b67f78a1759e7016605cd506ddd7041762d50000000000ffffffff02102700000000000017a9145a581567fd2a630e61e34a696ab3bb887972886d87ad0f010000000000225120850d0ab466d15cb1565dd528d4d9709f3e46f41d41fe6d94aa01378e6269839900000000",
"txInfo": {
"nP2SHInputs": 0,
"nSegwitInputs": 1,
"nOutputs": 2,
"unspents": [
{
"chain": 10,
"index": 5,
"id": "d5621704d7dd06d55c6016709e75a1787fb661d565290611e3eff914f03a4d0e:0",
"address": "2N5bzKehiq63uqT9hKDVaPWm1LmrPbHDz2S",
"value": 100000,
"valueString": "100000"
}
]
},
"feeInfo": {
"size": 226,
"fee": 20451,
"feeRate": 90491,
"feeString": "20451"
},
"coin": "tbtc4"
}
2. Sign Transaction
Using the txHex and txInfo returned in the prior call, sign the transaction using your private key.
Endpoint: Sign Transaction
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export WALLET="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export PRV="<YOUR_PRIVATE_KEY>"
export USER_PUB="<YOUR_USER_PUBLIC_KEY>"
export BACKUP_PUB="<YOUR_BACKUP_PUBLIC_KEY>"
export BITGO_PUB="<BITGO_PUBLIC_KEY>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/$COIN/signtx \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"prv": "'"$PRV"'",
"pubs": [
"'"$USER_PUB"'",
"'"$BACKUP_PUB"'",
"'"$BITGO_PUB"'"
],
"txPrebuild": {
"wallet": "'"$WALLET"'",
"txHex": "<TX_HEX_FROM_BUILD_STEP>",
"txInfo": "<TX_INFO_FROM_BUILD_STEP>"
}
}'
let params = {
txPrebuild: {
txHex: "<TX_HEX_FROM_BUILD_STEP>",
txInfo: "<TX_INFO_FROM_BUILD_STEP>",
},
prv: `<YOUR_PRIVATE_KEY>`,
};
wallet.signTransaction(params).then(function (transaction) {
console.dir(transaction);
});
Step Result
BitGo returns a txHex for the half-signed transaction.
{
"txHex": "010000000001010e4d3af014f9efe311062965d561b67f78a1759e7016605cd506ddd7041762d50000000023220020510ded26d712922bbb61bc68ef6766f836a03527820cbdc8b1551914eb467dafffffffff02102700000000000017a9145a581567fd2a630e61e34a696ab3bb887972886d87ad0f010000000000225120850d0ab466d15cb1565dd528d4d9709f3e46f41d41fe6d94aa01378e626983990500483045022100db45a8d94ee2144f7e29baa855d94a2bf0120707a7ab2fc93734ed94af972c460220558d00b91275aafc7805dfaaf9ab796adbe9f4e66dc467f7eb93b790671a323201000069522103c10ac628c880629ed0fd2a0563a898f4882baca45e15668a4d3064cf1ea379882103e56f84be4460080618ef869bb7b07096880760f748ba23efab533c2359f923bd21020ae81372264b5eac5c9dc7fe0b9a32bad00771c3a2c71f6e2c971823c3182ed653ae00000000"
}
3. Send Transaction
Using the txHex returned in the prior step, send the half-signed transaction to BitGo.
Endpoint: Send Half-Signed Transaction
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
curl -X POST \
https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/send \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"halfSigned": {
"txHex": "<HALF_SIGNED_TX_HEX>"
}
}'
let params = {
txHex: '<HALF_SIGNED_TX_HEX>',
otp: '0000000',
};
wallet.submitTransaction(params).then(function (transaction) {
console.dir(transaction);
});
Step Result
If your withdrawal does not require approval, BitGo applies the final signature using the BitGo key and broadcasts the transaction to the blockchain. If you create policy rules to require approvals on withdrawals, the transaction remains in a pending-approval status until a wallet admin approves it.
{
"transfer": {
"entries": [
{
"address": "2NAVwU9KtPyE4h3RyHkhbLgQQvrc4EuSXD3",
"wallet": "654ec786c07fe8dc0dcfe03916ec5bb0",
"value": -100000,
"valueString": "-100000"
},
{
"address": "2Mw241n14gJx2PcK2kijidZ2BPGeJBdwp1A",
"value": 10000,
"valueString": "10000",
"isChange": false,
"isPayGo": false
}
],
"id": "65528cde229f765c57a8f4d1eb762908",
"coin": "tbtc4",
"wallet": "654ec786c07fe8dc0dcfe03916ec5bb0",
"walletType": "hot",
"txid": "c68916ddc1672ec62c474ef0839ec479ad9b2dabc2249177ce6be6247f81dfe7",
"state": "signed"
},
"txid": "c68916ddc1672ec62c474ef0839ec479ad9b2dabc2249177ce6be6247f81dfe7",
"status": "signed"
}
4. 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": "65529448bd87efe59c3b0156ddfce867",
"coin": "tbtc4",
"wallet": "654ec786c07fe8dc0dcfe03916ec5bb0",
"state": "approved",
"approvalsRequired": 1,
"resolvers": [
{
"user": "621d08a634ad8a0007fcddffd7c429cc",
"date": "2023-11-13T21:44:27.794Z",
"resolutionType": "pending"
}
]
}
Overview
The simple withdrawal flow for self-custody MPC wallets enables you to build, sign, and send transactions, all in one call, using the BitGo JavaScript SDK. This flow suffices for most MPC use cases.
1. Build, Sign, and Send Transaction
Build and sign the transaction and send it to BitGo, all in one call.
Endpoints:
import * as dotenv from "dotenv";
import {EnvironmentName} from "bitgo";
import {BitGoAPI} from "@bitgo/sdk-api";
import {Hteth} from "@bitgo/sdk-coin-eth";
dotenv.config();
const config = {
USERNAME: process.env.USERNAME as string,
PASSWORD: process.env.PASSWORD as string,
ENV: process.env.ENV as EnvironmentName,
OTP: process.env.OTP as string,
ENTERPRISE_ID: process.env.ENTERPRISE_ID as string,
WALLET_ID: process.env.WALLET_ID as string,
WALLET_PASS_PHRASE: process.env.WALLET_PASS_PHRASE as string
};
const bitgo = new BitGoAPI({env: config.ENV});
bitgo.register("hteth", Hteth.createInstance);
const coin = bitgo.coin("hteth");
async function auth() {
await bitgo.authenticate({
username: config.USERNAME,
password: config.PASSWORD,
otp: config.OTP,
});
await bitgo.lock();
await bitgo.unlock({otp: "000000", duration: 3600});
}
async function main() {
await auth();
const destinatonAddress = "0x1037c88b10fbd0754b9fbd3ba7be41fe7cb61f59";
const wallet = await coin.wallets().get({id: config.WALLET_ID});
const sendAmount = wallet.balanceString() ?? 0;
const res = await wallet.sendMany({
walletPassphrase: config.WALLET_PASS_PHRASE,
recipients: [{address: destinatonAddress, amount: sendAmount}],
type: "transfer",
});
console.log(res);
}
main().catch((err) => console.error(err));
Step Result
BitGo uses the data you pass to build a half-signed transaction. The transaction remains in a pending-approval status until a wallet admin approves it.
{
"transfer": {
"entries": [
{
"address": "0x1a52b70e708f4aec9d92260aa035b3c71e51df84",
"wallet": "665e33f768f9b4b1cc2ca87a6b87cdfb",
"value": -1,
"valueString": "-1"
},
{
"address": "0xe2c5b494162bd9033283af31e35be27c6ee4bbf7",
"value": 1,
"valueString": "1"
}
],
"id": "665e352e29c225381225e8f5f74f429d",
"coin": "hteth",
"wallet": "665e33f768f9b4b1cc2ca87a6b87cdfb",
"walletType": "hot",
"txid": "0x43f7bef95dd02eea33d447afc625908a429902fbd35a35f0da6b7befddc69d9b",
"state": "signed"
},
"txRequest": {
"apiVersion": "full",
"txRequestId": "2c7e52a6-a422-49d3-876c-c050fb4cbf36",
"walletId": "665e33f768f9b4b1cc2ca87a6b87cdfb",
"walletType": "hot",
"state": "delivered"
},
"txid": "0x43f7bef95dd02eea33d447afc625908a429902fbd35a35f0da6b7befddc69d9b",
"status": "signed"
}
2. Approve Transaction (Optional)
Note: You cannot approve your own transactions - another admin must approve them.
Endpoint: Update Pending Approval
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export APPROVAL_ID="<APPROVAL_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export WALLET_PASSPHRASE="<YOUR_WALLET_PASSPHRASE>"
curl -X PUT \
https://$BITGO_EXPRESS_HOST/api/v2/$COIN/pendingApprovals/$APPROVAL_ID \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"state": "approved",
"walletPassphrase": "'"$WALLET_PASSPHRASE"'"
}'
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": "66b66201c45ab3125fc2d8a77b541d12",
"coin": "tsol",
"wallet": "669569464cec3c82c9b524faf0d42087",
"state": "approved",
"approvalsRequired": 1,
"txRequestId": "0b259f23-d5a8-47d0-8666-47a3df07e244",
"resolvers": [
{
"user": "6672292d89bd038e41cfb93b6ee482f1",
"date": "2024-08-09T18:38:45.245Z",
"resolutionType": "pending",
"resolutionAction": "approve"
}
]
}
Overview
The manual withdrawal flow for self-custody MPC wallets provides granular control by separating the request, sign, and send steps. BitGo uses the data you pass to construct an unsigned transaction that wallet admins can approve or wallet co-signers can sign if there are no approvals needed.
1. Request Transaction
Request the transaction by specifying the transaction details and sending them to BitGo.
Endpoint: Create transaction request
export WALLET_ID="<WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
curl -X POST \
https://app.bitgo-test.com/api/v2/wallet/$WALLET_ID/txrequests \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"idempotencyKey": "string",
"intent": {
"intentType": "payment",
"sequenceId": "abc123",
"comment": "string",
"nonce": "string",
"recipients": [
{
"address": {
"address": "string",
"options": {}
},
"amount": {
"value": "100",
"symbol": "gteth"
},
"data": "string"
}
]
},
"apiVersion": "full",
"preview": false
}'
let params = {
recipients: [
{
amount: 0.01 * 1e8,
address: '2NFfxvXpAWjKng7enFougtvtxxCJ2hQEMo4',
},
],
};
wallet.prebuildTransaction(params).then(function (transaction) {
console.dir(transaction);
});
Step Result
BitGo uses the data you pass to build an unsigned transaction.
{
"txRequestId": "string",
"version": 0,
"latest": true,
"walletId": "string",
"walletType": "hot",
"enterpriseId": "string",
"state": "initialized",
"date": "2018-05-05T19:46:22.019Z",
"userId": "string",
"intent": {
"intentType": "payment",
"sequenceId": "abc123",
"amount": {
"value": "100",
"symbol": "gteth"
}
},
"pendingApprovalId": "string",
"transactions": [
{
"state": "initialized",
"unsignedTx": {
"serializedTxHex": "string",
"signableHex": "string",
"derivationPath": "string",
"feeInfo": {
"feeString": "string",
"fee": 0
}
},
"signatureShares": []
}
]
}
2. Approve Transaction (Optional)
Note: You cannot approve your own transactions - another admin must approve them.
Endpoint: Update Pending Approval
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export APPROVAL_ID="<APPROVAL_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export WALLET_PASSPHRASE="<YOUR_WALLET_PASSPHRASE>"
curl -X PUT \
https://$BITGO_EXPRESS_HOST/api/v2/$COIN/pendingApprovals/$APPROVAL_ID \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"state": "approved",
"walletPassphrase": "'"$WALLET_PASSPHRASE"'"
}'
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.
{
"id": "66b66201c45ab3125fc2d8a77b541d12",
"coin": "tsol",
"wallet": "669569464cec3c82c9b524faf0d42087",
"state": "approved",
"approvalsRequired": 1,
"txRequestId": "0b259f23-d5a8-47d0-8666-47a3df07e244",
"resolvers": [
{
"user": "6672292d89bd038e41cfb93b6ee482f1",
"date": "2024-08-09T18:38:45.245Z",
"resolutionType": "pending",
"resolutionAction": "approve"
}
]
}
3. Sign and Send Transaction
Sign the transaction and send it to the blockchain.
Endpoint: Sign MPC 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 TX_REQUEST_ID="<TX_REQUEST_ID>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/$WALLET_ID/signtxtss \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"txRequestId": "'"$TX_REQUEST_ID"'",
"walletPassphrase": "string",
"keychain": {
"encryptedPrv": "string"
}
}'
const params = {
txRequestId: "string",
keychain: {
encryptedPrv: "encrypted prv"
},
walletPassphrase: "password to decrypt the encryptedPrv"
};
wallet.signTransaction(params).then(function (result) {
console.dir(result);
});
Step Result
BitGo signs and broadcasts the transaction to the blockchain.
{
"apiVersion": "full",
"txRequestId": "ed50bf41-cbc2-454e-81f5-840a024f802e",
"walletId": "6695838af64d6e4dadf93838d29e78d3",
"walletType": "hot",
"version": 10,
"enterpriseId": "6672220dd9e61ce3d62e9e2d710cbd6f",
"state": "delivered",
"latest": true,
"transactions": [
{
"state": "delivered",
"txHash": "5K5k73S8hCZhMMzEMkxoVwMefB7JYgEtb8bGvDx5uSt5bc6ZP9CbnDiFLDfpePhUrDCLvQGq7sCD4t9Pe5rJKCVY",
"signedTx": {
"id": "5K5k73S8hCZhMMzEMkxoVwMefB7JYgEtb8bGvDx5uSt5bc6ZP9CbnDiFLDfpePhUrDCLvQGq7sCD4t9Pe5rJKCVY"
}
}
],
"isCanceled": false
}
Next
See Also
- Custody Starter Architecture Overview
- Create Wallets
- Policies Overview
- API Reference: Build a Transaction
- API Reference: Create transaction request
- API Reference: Create a signature share for the transaction request
- API Reference: Get transaction requests by wallet
- API Reference: Send Half-Signed Transaction
- API Reference: Send to Many
- API Reference: Send Transaction
- API Reference: Sign MPC transaction
- API Reference: Sign Transaction
- API Reference: Update Pending Approval