Widget Embed Guide
How to easily add staking and DeFi to your application
Overview
The Blockdaemon Earn Widget lets you offer staking and DeFi lending in your application via a hosted widget, without building protocol integrations or on-chain logic in-house. Blockdaemon deploys the widget on a per-client basis and connects directly to the Blockdaemon Earn Stack.
The purpose of this guide is to support the evaluation and integration of the widget into your staging or production environments.
Supported Features
The Blockdaemon Earn Widget currently supports the following functionality:
- Staking: Ethereum (via StakeWise vaults) and Solana (native delegation with validator selection, unstaking, and reward claiming).
- DeFi lending: Aave on Ethereum, Base, Arbitrum, Optimism; Morpho and additional chains are planned.
- Swaps: Configurable per deployment via chain allow/block lists (commonly Ethereum, Base, Arbitrum, Optimism, Kaia).
Implementation Checklist
When preparing to go live, typical steps include:
- Widget Deployment: Confirm your
https://<client-slug>.widget.blockdaemon.comdeployment and target environment (staging vs production). - Embedding the Widget: Embed and size the iframe appropriately within your UI.
- Configuration Model: Align features configuration with your product scope (staking, DeFi, swaps) and regulatory requirements.
- Appearance & Branding: Configure appearance to meet brand, theme, locale, and currency guidelines.
- Wallet Integration Modes: Select wallet mode (internal vs external) and, for external mode, implement the
postMessageintegration contract. - Validate transaction flows, balances, and lifecycle (connect / transact / disconnect) in your staging environment.
Widget Deployment
Each customer receives a dedicated widget instance hosted at:
https://<client-slug>.widget.blockdaemon.com
Blockdaemon manages the deployment lifecycle, upgrades, and configuration of the widget instance. A client-specific slug will be provided as part of your implementation onboarding.
Embedding the Widget
To embed the widget, add the iframe to the relevant view or component in your application:
<iframe
src="https://<client-slug>.widget.blockdaemon.com?embed=true"
id="blockdaemon-widget"
title="Blockdaemon Earn Widget"
allow="clipboard-write"
style="width: 100%; height: 100%;">
</iframe>Consider the following when embedding:
embed=trueactivates PostMessage communication mode. Required for external wallet and external balance integration.- Sizing: The widget supports two sizing modes via
appearance.sizingMode.- Fixed mode: Use fixed mode when you want a predictable, static widget footprint. The numeric sizing limits require a width between 375 and 1000 and a height between 480 and 1200.
- Responsive mode: Use the responsive mode when the widget should adapt to the host layout. In this mode, the widget requires a minimum width of 375px and a minimum height of 480px.
Recommended Usage- Use responsive mode + host CSS for adaptive embeds.
- Use fixed mode for stable, known-size placements.
- Use URL overrides for per-placement tuning or testing.
- Scroll behavior: The widget's internal scroll area depends on host container sizing. Constrained iframe heights trigger internal scrolling, while unconstrained heights shift it to the outer page.
- Security & Content Security Policy (CSP): Ensure you allow the widget origin in frame-src, e.g.
https://<client-slug>.widget.blockdaemon.com.
Configuration Model
Blockdaemon currently manages widget configuration on your behalf; self-serve configuration will be provided via the Blockdaemon Partner Portal in a future release. Configuration can be applied per widget instance, with these specific parameters also available via URL query parameters:
| Parameter | Values | Purpose |
|---|---|---|
embed | true | Activate PostMessage mode (required for external wallet/balance) |
colorScheme | light, dark | Override theme |
width | mobile, tablet, or 375–1000 | Override widget width |
height | mobile, tablet, or 480–1200 | Override widget height |
currency | USD, KRW, JPY | Override display currency |
locale | en-US, ko-KR, ja-JP | Override locale |
batchMode | EIP-5792, eip5792, EIP-7702 (if config has batcher), off | Override batch mode (canonical labels or legacy slugs) |
atomicRequired | true, false | Require atomic execution for EIP-5792 batched calls |
enableFee | true, false | Override fee extraction (swap.fees.enabled) |
Features Configuration
The widget’s functionality is controlled through a features configuration object:
{
"features": {
"portfolio": { "enabled": false },
"staking": { "enabled": true },
"swap": {
"enabled": true,
"chains": { "allowList": [], "blockList": [] },
"assets": { "allowList": [], "blockList": [] },
"dexes": { "allowList": [], "blockList": [] },
"bridges": { "allowList": [], "blockList": [] },
"fees": { "enabled": false }
},
"defi": {
"enabled": true,
"markets": [],
"chains": { "allowList": [], "blockList": [] },
"assets": { "allowList": [], "blockList": [] }
},
"walletInfo": true,
"walletMode": "internal",
"balanceMode": "internal",
"batchOption": null
}
}| Field | Description |
|---|---|
portfolio | Enables a unified view to track and manage user balances. |
staking | Provides the interface for depositing assets to earn protocol rewards. |
swap | Enables token exchanges within a single chain or across chains. |
defi | Connects users to liquidity markets for lending and borrowing assets. |
walletInfo | Controls the visibility of connected wallet addresses and balance data. |
walletMode | Determines whether the widget or host application manages wallet connectivity. |
balanceMode | Internal: widget fetches balances via SDK. External: host pushes balances via PostMessage. Requires walletMode: "external". |
batchOption | Batch transaction config. Omit to disable batching. See Batch Transactions. |
Typical configuration patterns include:
- Enabling only staking for a particular chain or product line.
- Restricting swap or DeFi to a curated allow-list of assets, chains, or markets.
- Selecting wallet mode (internal vs external) to match your custody or wallet strategy.
Configuration changes are coordinated with your Blockdaemon contact and applied to your dedicated widget deployment.
Batch Option
Exactly one of eip5792 or eip7702 must be set. Omit batchOption entirely to disable batching.
// EIP-5792: wallet-side batching via wallet_sendCalls
{ "batchOption": { "eip5792": { "atomicRequired": false } } }
// EIP-7702: batcher contract, Type 4 transaction
{ "batchOption": { "eip7702": { "batcherContract": "0xA1fdd934a977898B72ddBFbeC5525e266b7e9991" } } }Appearance & Branding
The widget supports appearance configuration to align with your brand and layout:
{
"appearance": {
"sizingMode": "responsive",
"width": "mobile",
"height": "mobile",
"theme": "light",
"builtByBlockdaemon": true,
"logoUrl": "",
"coreNavigation": true,
"externalLinks": true,
"locale": "en-US",
"currency": "USD"
}
}Field | Description |
|---|---|
| Sets the horizontal size of the widget using presets or custom pixel values to fit your application's layout. Once configured, the iframe container should be adjusted to match the widget’s configured size to ensure a proper visual fit. |
`height | Sets the vertical size of the widget using presets or custom pixel values to fit your application's layout. Once configured, the iframe container should be adjusted to match the widget’s configured size to ensure a proper visual fit. |
| Configuration supports light or dark modes with a default option available. Apps with a light / dark toggle should update this parameter to reflect the active selection. |
| The widget supports a single logo or individual logos tailored for light and dark color schemes. |
| The flag determines whether the "Built by Blockdaemon" logo appears at the bottom of the widget UI. |
| Determines language for balances and yields. |
| Determines displayed currency for balances and yields. |
| Show/hide main navigation tabs |
| Allow/block external links (e.g., block explorer links) |
Wallet Integration Modes
The widget supports two wallet integration modes:
- Internal mode The widget manages wallet connection and network switching within the iframe. Users connect and approve transactions directly in the widget UI.
- External mode
The host application owns wallet state, connection flow, and transaction execution. The widget requests wallet actions via
postMessage, and the host responds with wallet state and transaction results.
In both modes, wallet connectivity is non-custodial and your application remains in control of user session and authentication outside the widget.
Before a wallet is connected, the widget displays a default, disconnected state; once a wallet is connected, the widget updates to a fully interactive state.
External Wallet Mode: Messaging Contract
When using external wallet mode, the host application and the widget communicate via the standard window.postMessage API. All postMessage events must be filtered by origin, and only the widget origin (https://<client-slug>.widget.blockdaemon.com) should be trusted.
Message Types
The following table serves as the formal communication contract between the Widget and the Host:
| Direction | Type | Payload | Purpose | Required Host Behavior |
|---|---|---|---|---|
| Widget → Host | getWalletState | none | Request current wallet status | Respond with walletState |
| Widget → Host | connectWallet | none | Request wallet connection | Open connection flow, then send walletState |
| Widget → Host | disconnectWallet | none | Request wallet disconnection | Disconnect wallet, then send walletState |
| Widget → Host | sendTransaction | { to, data, value?, chainId?, type?, authorizationList? } | Submit a transaction | Execute with host wallet, respond with sendTransactionResponse |
| Widget → Host | switchNetwork | { chainId } | Switch wallet network (CAIP-2) | Trigger switch, then send updated walletState |
| Widget → Host | sendBatchCalls | { calls, chainId, atomicRequired } | Submit EIP-5792 batch | Call wallet_sendCalls, respond with sendBatchCallsResponse |
| Widget → Host | getCallsStatus | { bundleId } | Poll batch confirmation | Call wallet_getCallsStatus, respond with getCallsStatusResponse |
| Widget → Host | getBalances | none | Request token balances | Respond with balanceUpdate |
| Host → Widget | walletState | { isConnected, address?, chainId?, walletInfo?, namespace? } | Current wallet state | Send on request and after any wallet/network change |
| Host → Widget | sendTransactionResponse | { success, data?, error? } | Transaction result | Include hash + chainId on success |
| Host → Widget | sendBatchCallsResponse | { success, data?, error? } | Batch result | Return { bundleId } or { hash } |
| Host → Widget | getCallsStatusResponse | { success, data?, error? } | Batch poll result | Return { status, receipts } |
| Host → Widget | balanceUpdate | ExternalTokenBalance[] | Push balance data | Can send at any time |
Example: App ↔ Widget Integration
Below is a reference implementation illustrating event handling and wallet state propagation:
<script>
const WIDGET_ORIGIN = 'https://<client-slug>.widget.blockdaemon.com';
const iframe = document.getElementById('blockdaemon-widget');
// Your app's wallet state (you manage this)
let walletState = {
isConnected: false,
address: null,
chainId: null,
walletInfo: "Client Wallet"
};
// Listen for messages FROM the widget
window.addEventListener('message', function(event) {
if (event.origin !== WIDGET_ORIGIN) return;
const { type, data } = event.data;
switch (type) {
case 'getWalletState':
sendWalletState();
break;
case 'connectWallet':
connectYourWallet();
break;
case 'disconnectWallet':
disconnectYourWallet();
break;
case 'sendTransaction':
handleTransaction(data);
break;
case 'switchNetwork':
handleSwitchNetwork(data.chainId);
break;
case 'sendBatchCalls':
handleBatchCalls(data);
break;
case 'getCallsStatus':
handleGetCallsStatus(data.bundleId);
break;
case 'getBalances':
sendBalances();
break;
}
});
// --- Wallet State ---
function sendWalletState() {
iframe.contentWindow.postMessage(
{ type: 'walletState', data: walletState },
WIDGET_ORIGIN
);
}
function connectYourWallet() {
// Implement your wallet connection logic here
walletState = {
isConnected: true,
address: '0x1234...',
chainId: 'eip155:1',
walletInfo: "Client Wallet"
};
sendWalletState();
}
function disconnectYourWallet() {
walletState = {
isConnected: false,
address: null,
chainId: null,
walletInfo: "Client Wallet"
};
sendWalletState();
}
// --- Transactions ---
function handleTransaction(txData) {
// Execute the transaction with your wallet provider
// txData: { to, data, value?, chainId?, type?, authorizationList? }
try {
const hash = '0x...'; // Your wallet provider's response
iframe.contentWindow.postMessage({
type: 'sendTransactionResponse',
success: true,
data: { hash, chainId: walletState.chainId }
}, WIDGET_ORIGIN);
} catch (err) {
iframe.contentWindow.postMessage({
type: 'sendTransactionResponse',
success: false,
error: err.message
}, WIDGET_ORIGIN);
}
}
function handleSwitchNetwork(chainId) {
// Switch your wallet to the requested chain (CAIP-2 format)
// After switching, update walletState.chainId and notify the widget
walletState.chainId = chainId;
sendWalletState();
}
// --- Batch Transactions (EIP-5792) ---
async function handleBatchCalls({ calls, chainId, atomicRequired }) {
// calls: Array<{ to, value, data }>
// Use wallet_sendCalls on the connected wallet
try {
const result = await wallet.sendCalls({ calls, chainId, atomicRequired });
// Option A: Return bundleId — widget will poll via getCallsStatus
iframe.contentWindow.postMessage({
type: 'sendBatchCallsResponse',
success: true,
data: { bundleId: result.bundleId }
}, WIDGET_ORIGIN);
// Option B: Poll internally, return final hash
// iframe.contentWindow.postMessage({
// type: 'sendBatchCallsResponse',
// success: true,
// data: { hash: result.transactionHash }
// }, WIDGET_ORIGIN);
} catch (err) {
iframe.contentWindow.postMessage({
type: 'sendBatchCallsResponse',
success: false,
error: err.message
}, WIDGET_ORIGIN);
}
}
async function handleGetCallsStatus(bundleId) {
// Call wallet_getCallsStatus(bundleId) and return the result
try {
const status = await wallet.getCallsStatus(bundleId);
iframe.contentWindow.postMessage({
type: 'getCallsStatusResponse',
success: true,
data: status // { status: "CONFIRMED", receipts: [{ transactionHash, status }] }
}, WIDGET_ORIGIN);
} catch (err) {
iframe.contentWindow.postMessage({
type: 'getCallsStatusResponse',
success: false,
error: err.message
}, WIDGET_ORIGIN);
}
}
// --- External Balances ---
function sendBalances() {
// Provide token balances in smallest denomination (e.g., wei)
// Use 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for native tokens
iframe.contentWindow.postMessage({
type: 'balanceUpdate',
data: [
{
chainId: 'eip155:1',
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
rawBalance: '1500000000000000000'
},
{
chainId: 'eip155:1',
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
rawBalance: '5000000000'
}
]
}, WIDGET_ORIGIN);
}
// Send initial wallet state when iframe loads
iframe.addEventListener('load', function() {
setTimeout(() => sendWalletState());
});
</script>External Balance Mode
When balanceMode: "external" is configured (requires walletMode: "external"), the host provides token balances instead of the widget fetching them via the Blockdaemon SDK.
Flow
- Widget sends
getBalanceson load, after wallet connect, after transactions, and every 10 seconds. - Host responds with
balanceUpdatecontaining an array of token balances. - Host can also push
balanceUpdateat any time (not just in response togetBalances).
Balance Payload
{
"type": "balanceUpdate",
"data": [
{
"chainId": "eip155:1",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"rawBalance": "5000000000"
},
{
"chainId": "eip155:1",
"address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"rawBalance": "1500000000000000000"
}
]
}| Field | Type | Description |
|---|---|---|
chainId | string | CAIP-2 chain ID (e.g., "eip155:1") |
address | string | Token contract address. Use "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" for native tokens (ETH, MATIC, etc.) |
rawBalance | string | Balance in smallest denomination (e.g., wei for ETH, 6-decimal units for USDC) |
The widget handles decimal conversion using on-chain token metadata. Invalid balance entries are silently dropped.
Batch Transactions
Batch mode combines fee + approval + swap into a single wallet confirmation. Two protocols are supported.
Uses wallet_sendCalls — the wallet itself batches and submits the calls.
Config:
{ "batchOption": { "eip5792": { "atomicRequired": false } } }PostMessage flow (external wallet mode):
- Widget sends
sendBatchCallswith{ calls, chainId, atomicRequired }. - Host calls
wallet_sendCallson the connected wallet. - Host responds with
sendBatchCallsResponse:- Two-phase: Return
{ bundleId }. The widget will poll viagetCallsStatusevery 2 seconds (up to 30 attempts). - One-phase: Return
{ hash }after polling internally. Widget skips polling.
- Two-phase: Return
MetaMask Smart TransactionsMetaMask Smart Transactions don't support
wallet_getCallsStatus. If the host forwards a "No matching bundle found" error, the widget retries up to 5 times, then treats the transaction as submitted but unconfirmed (success UI shown without explorer link).
EIP-7702 (Batcher Contract)
Uses a Type 4 transaction with an authorization list to delegate execution to a batcher contract.
Config:
{ "batchOption": { "eip7702": { "batcherContract": "0xA1fdd934a977898B72ddBFbeC5525e266b7e9991" } } }When EIP-7702 is configured, the widget may send a sendTransaction with additional fields:
{
"type": "sendTransaction",
"data": {
"to": "0xUserEOA",
"data": "0xe9ae5c53...",
"value": "0x0",
"chainId": "eip155:8453",
"type": "0x04",
"authorizationList": [
{ "chainId": "0x2105", "address": "0xBatcherContract" }
]
}
}The host must construct and sign the Type 4 transaction accordingly.
Next Steps and Support
For configuration changes, production cutover, or to discuss additional feature flags and chains, please contact your Blockdaemon representative or support channel. Blockdaemon can work with you to align widget behavior, branding, and wallet flows with your institutional requirements.
👋 Need Help?
Contact us through email or our support page for any issues, bugs, or assistance you may need.
Updated 7 days ago
