Widget Embed Guide

How to easily add staking and DeFi to your application

Overview

The Blockdaemon Earn Widget lets you offer staking, DeFi lending, and token swaps 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.

Supported Features

  • 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).

Widget Deployment

Each customer receives a dedicated deployment at:

https://<client-slug>.widget.blockdaemon.com

Blockdaemon manages deployment lifecycle, upgrades, and configuration. A client-specific slug is provided during onboarding.

Embedding the Widget

<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>

Notes:

  • embed=true activates PostMessage communication mode. Required for external wallet and external balance integration.
  • Container sizing: Place the iframe in a responsive container. Typical layouts use full-width in a main content column or a fixed-height panel in a dashboard card.
  • Scroll behavior: Controlled by your container and CSS, not the widget.
  • CSP: Ensure your Content Security Policy allows https://<client-slug>.widget.blockdaemon.com in frame-src.

URL Parameters

The following parameters can be appended to the widget URL to override configuration at runtime.

ParameterValuesPurpose
embedtrueActivate PostMessage mode (required for external wallet/balance)
colorSchemelight, darkOverride theme
widthmobile, tablet, or 3751000Override widget width
heightmobile, tablet, or 4801200Override widget height
currencyUSD, KRW, JPYOverride display currency
localeen-US, ko-KR, ja-JPOverride locale
batchModeEIP-5792, eip5792, EIP-7702 (if config has batcher), offOverride batch mode (canonical labels or legacy slugs)
atomicRequiredtrue, falseRequire atomic execution for EIP-5792 batched calls
enableFeetrue, falseOverride fee extraction (swap.fees.enabled)

Example:

https://acme.widget.blockdaemon.com?embed=true&colorScheme=dark&locale=ko-KR&batchMode=eip5792&atomicRequired=true

Configuration Model

Blockdaemon manages widget configuration on your behalf. Configuration changes are coordinated with your Blockdaemon contact. Self-serve configuration via the Partner Portal is planned.

Features Configuration

{
  "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
  }
}
FieldTypeDescription
walletMode"internal" | "external"Internal: widget manages wallet UI. External: host manages wallet via PostMessage.
balanceMode"internal" | "external"Internal: widget fetches balances via SDK. External: host pushes balances via PostMessage. Requires walletMode: "external".
batchOptionobject | omittedBatch transaction config. Omit to disable batching. See Batch Transactions.
walletInfobooleanShow/hide wallet info bar. Auto-disabled when walletMode: "external".

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

{
  "appearance": {
    "sizingMode": "responsive",
    "width": "mobile",
    "height": "mobile",
    "theme": "light",
    "builtByBlockdaemon": true,
    "logoUrl": "",
    "coreNavigation": true,
    "externalLinks": true,
    "locale": "en-US",
    "currency": "USD"
  }
}
FieldTypeDefaultDescription
sizingMode"fixed" | "responsive""fixed"Fixed dimensions vs. adapt to host container
width"mobile" | "tablet" | numberWidget width preset or custom pixel value (375–1000)
height"mobile" | "tablet" | number"mobile"Widget height preset or custom pixel value (480–1200)
theme"light" | "dark""light"Color theme
builtByBlockdaemonbooleantrueShow "Built by Blockdaemon" badge
logoUrlstringCustom logo URL
coreNavigationbooleantrueShow/hide main navigation tabs
externalLinksbooleantrueAllow/block external links (e.g., block explorer links)
localestring"en-US"Language (en-US, ko-KR, ja-JP)
currencystring"USD"Display currency (USD, KRW, JPY)

Wallet Integration Modes

Internal Mode

The widget renders its own wallet connection bar. End users connect wallets directly within the widget UI. No PostMessage integration needed.

External Mode

Your application owns wallet connectivity and injects wallet state into the widget via PostMessage. Set walletMode: "external" in the config and include ?embed=true in the iframe URL.

Before a wallet is connected, the widget displays a disconnected state. Once connected, the widget updates to a fully interactive state.

External Wallet: PostMessage Contract

All messages use window.postMessage. Filter by origin — only trust https://<client-slug>.widget.blockdaemon.com.

Message Types

DirectionTypePayloadPurposeRequired Host Behavior
Widget → HostgetWalletStatenoneRequest current wallet statusRespond with walletState
Widget → HostconnectWalletnoneRequest wallet connectionOpen connection flow, then send walletState
Widget → HostdisconnectWalletnoneRequest wallet disconnectionDisconnect wallet, then send walletState
Widget → HostsendTransaction{ to, data, value?, chainId?, type?, authorizationList? }Submit a transactionExecute with host wallet, respond with sendTransactionResponse
Widget → HostswitchNetwork{ chainId }Switch wallet network (CAIP-2)Trigger switch, then send updated walletState
Widget → HostsendBatchCalls{ calls, chainId, atomicRequired }Submit EIP-5792 batchCall wallet_sendCalls, respond with sendBatchCallsResponse
Widget → HostgetCallsStatus{ bundleId }Poll batch confirmationCall wallet_getCallsStatus, respond with getCallsStatusResponse
Widget → HostgetBalancesnoneRequest token balancesRespond with balanceUpdate
Host → WidgetwalletState{ isConnected, address?, chainId?, walletInfo?, namespace? }Current wallet stateSend on request and after any wallet/network change
Host → WidgetsendTransactionResponse{ success, data?, error? }Transaction resultInclude hash + chainId on success
Host → WidgetsendBatchCallsResponse{ success, data?, error? }Batch resultReturn { bundleId } or { hash }
Host → WidgetgetCallsStatusResponse{ success, data?, error? }Batch poll resultReturn { status, receipts }
Host → WidgetbalanceUpdateExternalTokenBalance[]Push balance dataCan send at any time

Wallet State Payload

{
  "isConnected": true,
  "address": "0x1234...",
  "chainId": "eip155:1",
  "walletInfo": "Client Wallet",
  "namespace": "eip155"
}
  • chainId uses CAIP-2 format (e.g., "eip155:1" for Ethereum mainnet).

Integration Example

Below is a reference implementation illustrating event handling and wallet state propagation, including batch transactions and external balance support.

<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

  1. Widget sends getBalances on load, after wallet connect, after transactions, and every 10 seconds.
  2. Host responds with balanceUpdate containing an array of token balances.
  3. Host can also push balanceUpdate at any time (not just in response to getBalances).

Balance Payload

{
  "type": "balanceUpdate",
  "data": [
    {
      "chainId": "eip155:1",
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "rawBalance": "5000000000"
    },
    {
      "chainId": "eip155:1",
      "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
      "rawBalance": "1500000000000000000"
    }
  ]
}
FieldTypeDescription
chainIdstringCAIP-2 chain ID (e.g., "eip155:1")
addressstringToken contract address. Use "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" for native tokens (ETH, MATIC, etc.)
rawBalancestringBalance 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.

EIP-5792 (Wallet-Side Batching)

Uses wallet_sendCalls — the wallet itself batches and submits the calls.

Config:

{ "batchOption": { "eip5792": { "atomicRequired": false } } }

PostMessage flow (external wallet mode):

  1. Widget sends sendBatchCalls with { calls, chainId, atomicRequired }.
  2. Host calls wallet_sendCalls on the connected wallet.
  3. Host responds with sendBatchCallsResponse:
    • Two-phase: Return { bundleId }. The widget will poll via getCallsStatus every 2 seconds (up to 30 attempts).
    • One-phase: Return { hash } after polling internally. Widget skips polling.

MetaMask Smart Transactions note: MetaMask 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

For configuration changes, production cutover, or to discuss additional features, contact your Blockdaemon representative. Blockdaemon works with you to align widget behavior, branding, and wallet flows with your requirements.