Tokenization
Deploy, Mint, and Burn Tokens with Institutional Vault
This guide walks through deploying an ERC-20 token contract, registering it with the wallet, and performing mint and burn operations from a sandbox.
Base URL format: https://<sandbox>.api.blockdaemon-wallet.com/
Published documentation: https://vault.docs.blockdaemon.com/
Prerequisites
- A provisioned Institutional Vault sandbox ( enrollment process)
- An admin user signed in to the wallet UI with the BD Approver app configured
- The sandbox must have at least one EVM testnet chain enabled (e.g. Ethereum Sepolia, Base Sepolia)
- The wallet account address must hold testnet native tokens (ETH) to pay gas fees
- Node.js 18+ and
npm/pnpm(for SDK generation and running examples) - An RPC URL for the target EVM testnet (for building unsigned transactions when using the raw signing path)
Step 1: Create a System User and Obtain an API Key
System users provide programmatic API access. Only admin users can create them.
- Log in to the Institutional Vault UI at
https://<sandbox>.blockdaemon-wallet.com/ - Navigate to Settings > System Users
- Click Create System Users
- Set the system user Name, Role (
MarketOpsis sufficient for transactions), and Confirmer - Click Create and approve the operation on the BD Approver app
- Copy the API Key from the system users list (shown only once)
Pass the API key on every CWP request in the authorization header (no Bearer prefix):
authorization: <your-api-key>
Docs: Quick Start — Get API Key
Step 2: Access the OpenAPI Specification
The full OpenAPI spec for your sandbox (including all CWP paths) is available at:
https://<sandbox>.blockdaemon-wallet.com/swagger.html
Download openapi.yaml from Swagger UI and generate a typed client from the spec for TypeScript, Go, Python, Java, or C#.
Published API reference: https://vault.docs.blockdaemon.com/reference
Generate Client SDKs: https://vault.docs.blockdaemon.com/reference/generate-client-sdks
Step 3: Identify Your Chain, Address, and CAIP-19 Values
API requests use CAIP-19 asset identifiers and an address-only Source (or full AddressSpec).
List registered chains
Bash
curl -s https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/chains/list \
-H "authorization: <your-api-key>" | jq
Note the chain CAIP-2 (e.g. eip155:11155111 for Ethereum Sepolia).
Create an account and get an address
You need a master key, a named account under that master key, and an on-chain address for the chain you will use. If you already have an address from the wallet UI or a prior API call, skip to Resolve a wallet-managed address.
Use Default for MasterKey unless your deployment is multitenanted and you created a different master key name.
1. Create the account (skip if the account already exists under your master key):
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/start/createAccount \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"Name": "<account-name>",
"MasterKey": "Default",
"InitiatorID": "<vault-user-email>"
}' | jq
Poll GET /api/cwp/operations/id/{operationID}/status until Succeeded. Approve on the BD Approver app when required. Optional InitiatorID: registered Vault user email (recommended so the gateway can sync the account to the Vault UI).
2. Get the account's native address for your EVM chain (use the CAIP-2 from the chains list, e.g. Sepolia):
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/addresses/get \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"MasterKey": "Default",
"CAIP2": "eip155:11155111",
"Account": "<account-name>",
"Index": 0
}' | jq
The response Address is the value to use as Source.Address in later makeTransaction calls. Index defaults to 0 (main address) when omitted.
Resolve a wallet-managed address
Confirm the signing address is owned by the vault:
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/addresses/resolve \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"Address": "<your-wallet-address>"}' | jq
The response includes MasterKey, Account, and Index for that address.
Native asset CAIP-19 (gas)
For EVM contract deploy, mint, and burn, set CAIP19 to the chain's native asset (slip44), not the ERC-20 token. Example for Ethereum Sepolia:
eip155:11155111/slip44:60
Fund the signing address with enough native token on that chain to pay gas before you deploy, mint, or burn (for example, send Sepolia ETH to the address from a faucet or another wallet). Deploy and mint transactions fail if the address has insufficient balance for fees.
API reference:
Step 4: Deploy the Token Contract
Contract deployment uses POST /api/cwp/operations/start/makeTransaction with a pre-built unsigned transaction in RawTransaction. The vault performs MPC signing only; you broadcast the returned SignedTransaction to the network.
4a. Compile the contract
Use Foundry or Hardhat to compile your ERC-20 contract and extract the bytecode:
Bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init my-token && cd my-token
forge install OpenZeppelin/openzeppelin-contracts --no-commit
forge build
Extract the bytecode field from the compiler output JSON.
4b. Build an unsigned deploy transaction
Use your EVM library to construct an unsigned EIP-1559 transaction whose data field is the contract bytecode (no to address). Example with viem:
TypeScript
import { createPublicClient, http, serializeTransaction } from 'viem';
import { sepolia } from 'viem/chains';
const client = createPublicClient({ chain: sepolia, transport: http('<rpc-url>') });
const fromAddress = '<your-wallet-address>' as `0x${string}`;
const bytecode = '0x6080604052...' as `0x${string}`;
const nonce = await client.getTransactionCount({ address: fromAddress });
const gas = await client.estimateGas({ account: fromAddress, data: bytecode, value: 0n });
const { maxFeePerGas, maxPriorityFeePerGas } = await client.estimateFeesPerGas();
const rawTransaction = serializeTransaction({
chainId: sepolia.id,
nonce,
value: 0n,
gas,
maxFeePerGas: maxFeePerGas!,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
accessList: [],
type: 'eip1559',
data: bytecode,
});
4c. Start make transaction (sign only)
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/start/makeTransaction \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"CAIP19": "eip155:11155111/slip44:60",
"Source": { "Address": "<your-wallet-address>" },
"RawTransaction": "0x02f8..."
}' | jq
Optional InitiatorID: set to the Vault user's email when your policies require an identified initiator.
The response includes OperationID. Approve the operation on the BD Approver app when prompted.
4d. Poll status and broadcast
Bash
curl -s https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/id/<operationID>/status \
-H "authorization: <your-api-key>" | jq
When Status is Succeeded, read Result.Transaction.SignedTransaction and publish it to your RPC:
Bash
cast publish --rpc-url <rpc-url> "0x<signed-transaction-hex>"
Find the deployed contract address from the transaction receipt on the block explorer (e.g. Sepolia Etherscan) using the broadcast tx hash.
API reference: Start Make Transaction
Step 5: Register the Token in the Wallet
Register the deployed ERC-20 so the vault tracks balances and policies can reference the token. To require approvals on deploy, mint, burn, or other on-chain operations, configure Transaction Restrictions.
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/start/registerToken \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"CAIP19": "eip155:11155111/erc20:0x<deployed-contract-address>",
"Symbol": "MYTKN",
"Decimals": 18
}' | jq
Poll GET /api/cwp/operations/id/{operationID}/status until Succeeded or Failed. Approve on the BD Approver app when required.
You can also register through the UI: Settings > Assets > Add Assets.
Confirm registration:
Bash
curl -s https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/tokens/list \
-H "authorization: <your-api-key>" | jq
Docs: Add ERC-20 Assets
API reference: Start Register Token
Step 6: Mint Tokens
Option A: Structured contract call (vault builds, signs, and broadcasts)
Use makeTransaction with EVM.Data set to ABI-encoded mint(address,uint256) calldata. Point Destination at the token contract with Amount: "0" (native-value transfer to the contract). Use the chain's native CAIP19 for gas, same as deploy.
Encode calldata (TypeScript with viem):
TypeScript
import { encodeFunctionData, parseAbi, parseEther } from 'viem';
const calldata = encodeFunctionData({
abi: parseAbi(['function mint(address to, uint256 amount)']),
functionName: 'mint',
args: ['<recipient-address>' as `0x${string}`, parseEther('100')],
});
Submit:
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/start/makeTransaction \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"CAIP19": "eip155:11155111/slip44:60",
"Source": { "Address": "<your-wallet-address>" },
"Destination": [\
{ "Address": "<token-contract-address>", "Amount": "0" }\
],
"EVM": { "Data": "0x40c10f19..." }
}' | jq
Poll operation status until Succeeded. On success, Result.Transaction.ID contains the on-chain transaction hash.
API reference: Start Make Transaction
Option B: Raw transaction (client builds unsigned tx, vault signs, you broadcast)
Build and serialize an unsigned EIP-1559 transaction to the token contract with mint calldata in data, then pass the hex string as RawTransaction (same pattern as Step 4). Do not set Destination or EVM when using RawTransaction.
After Status is Succeeded, broadcast Result.Transaction.SignedTransaction with your RPC client or cast publish.
Step 7: Burn Tokens
Burn uses the same makeTransaction patterns as mint; only the calldata changes.
Calldata encoding
Encode burn(uint256) (burns from the signer's balance):
TypeScript
import { encodeFunctionData, parseAbi, parseEther } from 'viem';
const calldata = encodeFunctionData({
abi: parseAbi(['function burn(uint256 amount)']),
functionName: 'burn',
args: [parseEther('50')],
});
Or with Foundry cast:
Bash
cast calldata "burn(uint256)" 50000000000000000000
Structured contract call
Bash
curl -s -X POST https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/start/makeTransaction \
-H "authorization: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"CAIP19": "eip155:11155111/slip44:60",
"Source": { "Address": "<your-wallet-address>" },
"Destination": [\
{ "Address": "<token-contract-address>", "Amount": "0" }\
],
"EVM": { "Data": "0x42966c68..." }
}' | jq
Raw transaction
Follow Step 6, Option B, substituting burn calldata. The signing address must hold at least the burn amount of tokens.
Step 8: Monitor Operations
All CWP operations (deploy, register token, mint, burn) follow the same asynchronous pipeline: policy evaluation, approvals, MPC signing, and (for structured makeTransaction calls) broadcasting.
Poll operation status
Bash
curl -s https://<sandbox>.blockdaemon-wallet.com/api/cwp/operations/id/<operationID>/status \
-H "authorization: <your-api-key>" | jq
Terminal Status values:
| Status | Meaning |
|---|---|
Succeeded |
Completed successfully. For structured makeTransaction, Result.Transaction.ID is the tx hash. For RawTransaction flows, use Result.Transaction.SignedTransaction and broadcast locally. |
Failed |
Rejected by policy, approver, or an execution error. See ErrorDetails. |
Pending approval |
Waiting for approver action on the BD Approver app. |
Pending confirmation |
Waiting for initiator confirmation when required by policy. |
Executing / Pending signature |
In progress. |
Approval details
Bash
curl -s https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/id/<operationID>/approvalstatus \
-H "authorization: <your-api-key>" | jq
Inspect the stored intent
Bash
curl -s https://<sandbox>.api.blockdaemon-wallet.com/api/cwp/operations/id/<operationID>/intent \
-H "authorization: <your-api-key>" | jq