Skip to main content

Webview Base URL

https://deframe-sdk-iframe.vercel.app
All iframe integrations point to this hosted webview. You append query parameters to configure which widget to display and how to connect to the Deframe API.

Iframe URL Query Parameters

ParameterTypeRequiredDescription
smartAccountAddress0x${string}YesThe user’s smart account address
chainIdnumberYesTarget chain ID (e.g., 137 for Polygon)
themestringYesUI theme ("dark" or "light")
presetstringYesWidget preset (e.g., "cryptocontrol")
deframeApiUrlstringYesDeframe API base URL
deframeApiKeystringYesDeframe API key
alchemyPolicyIdstringYesAlchemy gas policy ID (for paymaster)
alchemyRpcIdstringYesAlchemy RPC/API key
widget"earn" | "swap"NoWhich widget to display (defaults to "earn")

URL Builder Example

function buildIframeUrl(params: {
  webviewBaseUrl: string
  smartAccountAddress: string
  chainId: number
  theme?: string
  preset?: string
  deframeApiUrl: string
  deframeApiKey: string
  alchemyPolicyId: string
  alchemyRpcId: string
  widget?: 'earn' | 'swap'
}): string {
  const url = new URL(params.webviewBaseUrl)
  url.searchParams.set('smartAccountAddress', params.smartAccountAddress)
  url.searchParams.set('chainId', String(params.chainId))
  if (params.theme) url.searchParams.set('theme', params.theme)
  if (params.preset) url.searchParams.set('preset', params.preset)
  url.searchParams.set('deframeApiUrl', params.deframeApiUrl)
  url.searchParams.set('deframeApiKey', params.deframeApiKey)
  url.searchParams.set('alchemyPolicyId', params.alchemyPolicyId)
  url.searchParams.set('alchemyRpcId', params.alchemyRpcId)
  if (params.widget) url.searchParams.set('widget', params.widget)
  return url.toString()
}

postMessage Protocol

The iframe and wrapper communicate via window.postMessage. The protocol is typed in TypeScript below — copy this into your project as src/types/postmessage-protocol.ts.

Shared Types

export interface BytecodeTransaction {
  to: string
  data: string
  value: string
  chainId?: number
  gasLimit?: string
}

export type TxStatusPayload =
  | { type: 'HOST_ACK'; clientTxId: string }
  | { type: 'SIGNATURE_PROMPTED'; clientTxId: string }
  | { type: 'SIGNED'; clientTxId: string }
  | { type: 'TX_SUBMITTED'; clientTxId: string; chainId: number; txHash?: string; userOperationHash?: string }
  | { type: 'TX_CONFIRMED'; clientTxId: string; txHash: string; blockNumber?: number; confirmations?: number }
  | { type: 'TX_FINALIZED'; clientTxId: string; txHash?: string }
  | { type: 'SIGNATURE_DECLINED'; clientTxId: string }
  | { type: 'SIGNATURE_ERROR'; clientTxId: string; code: string; message: string }
  | { type: 'TX_REVERTED'; clientTxId: string; txHash: string; reason?: string }

export type UpdateTxStatus = (event: TxStatusPayload) => void

Message Directions

// Webview → Wrapper
export type WebviewToWrapperMessage =
  | { kind: 'READY' }
  | { kind: 'PROCESS_BYTECODE_REQUEST'; clientTxId: string; bytecodes: BytecodeTransaction[]; simulateError?: boolean }

// Wrapper → Webview
export type WrapperToWebviewMessage =
  | { kind: 'TX_STATUS_UPDATE'; payload: TxStatusPayload }

// Union
export type PostMessage = WebviewToWrapperMessage | WrapperToWebviewMessage

Type Guards

export function isWebviewMessage(msg: unknown): msg is WebviewToWrapperMessage {
  if (typeof msg !== 'object' || msg === null) return false
  const m = msg as Record<string, unknown>
  return m.kind === 'READY' || m.kind === 'PROCESS_BYTECODE_REQUEST'
}

export function isWrapperMessage(msg: unknown): msg is WrapperToWebviewMessage {
  if (typeof msg !== 'object' || msg === null) return false
  const m = msg as Record<string, unknown>
  return m.kind === 'TX_STATUS_UPDATE'
}

processBytecode Flow via postMessage

The iframe version of processBytecode works over postMessage instead of in-process callbacks. The transaction lifecycle statuses and error handling are the same as the widget processBytecode — the only difference is the transport: messages are wrapped in { kind: 'TX_STATUS_UPDATE', payload } and sent via postMessage instead of calling ctx.updateTxStatus() directly.

Happy Path

Webview → Wrapper:  { kind: 'PROCESS_BYTECODE_REQUEST', clientTxId, bytecodes }
Wrapper → Webview:  { kind: 'TX_STATUS_UPDATE', payload: { type: 'HOST_ACK', clientTxId } }
Wrapper → Webview:  { kind: 'TX_STATUS_UPDATE', payload: { type: 'SIGNATURE_PROMPTED', clientTxId } }
  ... user signs in wallet / smart wallet signs automatically ...
Wrapper → Webview:  { kind: 'TX_STATUS_UPDATE', payload: { type: 'SIGNED', clientTxId } }
Wrapper → Webview:  { kind: 'TX_STATUS_UPDATE', payload: { type: 'TX_SUBMITTED', clientTxId, chainId, txHash } }
  ... wait for confirmation ...
Wrapper → Webview:  { kind: 'TX_STATUS_UPDATE', payload: { type: 'TX_CONFIRMED', clientTxId, txHash, blockNumber } }
Wrapper → Webview:  { kind: 'TX_STATUS_UPDATE', payload: { type: 'TX_FINALIZED', clientTxId, txHash } }

Error Paths

See processBytecode — Error Handling for the full list of error statuses (SIGNATURE_DECLINED, SIGNATURE_ERROR, TX_REVERTED).

Listening for postMessage Events

The wrapper must listen for message events on window and filter using the type guards from the protocol types:
useEffect(() => {
  const handler = (event: MessageEvent) => {
    if (!isWebviewMessage(event.data)) return

    if (event.data.kind === 'READY') {
      console.log('Iframe is ready')
    }

    if (event.data.kind === 'PROCESS_BYTECODE_REQUEST') {
      const { clientTxId, bytecodes } = event.data
      processBytecode(clientTxId, bytecodes)
    }
  }

  window.addEventListener('message', handler)
  return () => window.removeEventListener('message', handler)
}, [processBytecode])

Iframe HTML Element

<iframe
  src={iframeUrl}
  allow="clipboard-write"
  style="width: 100%; height: 100%; border: none;"
/>
No iframe should be present in the DOM before the smart account is ready. Show a loading state or placeholder instead.

Wrapper Implementations

We provide two reference wrapper implementations, each targeting a different wallet/auth stack:

LLM Resources

Use these resources to generate iframe wrapper projects with AI assistants.

One-Shot Prompts

ResourceStackUse
/llms-iframe-wagmi-viem.txtwagmi + viem + permissionlessGenerate a complete iframe wrapper with browser wallet + ERC-4337
/llms-iframe-privy-multichain.txtPrivy + multi-chainGenerate a complete iframe wrapper with Privy auth + 6-chain support