Skip to main content

What Are Action Delegates?

Action delegates are callback functions you pass to KeystoneProvider that handle on-chain operations. When the widget needs to deposit tokens or approve a token transfer, it calls your delegate instead of interacting with the blockchain directly. This is how Tier 2 (Interactive + Custody) integration works - your frontend collects the user’s intent, and your backend executes it through Fireblocks, BitGo, or any signing infrastructure.

The Deposit Flow

When a user clicks “Deposit” in the widget:

Setting Up Delegates

<KeystoneProvider
  sessionToken={token}
  actionDelegates={{
    onDepositRequired: async (leg, depositInfo) => {
      // leg: the settlement leg being deposited
      // depositInfo: pre-encoded calldata and metadata
      const result = await yourBackend.submitDeposit(depositInfo);
      return { txHash: result.txHash };
    },
    onApprovalRequired: async (leg, approvalInfo) => {
      // ERC-20 approve() before deposit
      const result = await yourBackend.submitApproval(approvalInfo);
      return { txHash: result.txHash };
    },
  }}
>

Deposit Info Object

The depositInfo parameter passed to onDepositRequired contains everything needed to execute the deposit:
FieldTypeDescription
calldatastringABI-encoded depositLeg() calldata (0x-prefixed)
escrowAddressstringThe escrow contract address to send the transaction to
chainIdnumberThe blockchain network (e.g., 11155111 for Sepolia)
tokenAddressstringThe ERC-20 token contract address
depositAmountstringAmount in token smallest units
approvalCalldatastringABI-encoded approve() calldata
Your backend can submit the calldata directly to escrowAddress on the specified chainId. No ABI knowledge needed.

Approval Info Object

The approvalInfo parameter passed to onApprovalRequired:
FieldTypeDescription
tokenAddressstringThe ERC-20 token to approve
escrowAddressstringThe spender (escrow contract)
amountstringAmount to approve
approvalCalldatastringPre-encoded approve() calldata

Backend Implementation

Your backend receives the deposit info and routes it to your custody provider:
// Express example
app.post('/api/deposit', async (req, res) => {
  const { calldata, escrowAddress, chainId } = req.body;

  // Option 1: Fireblocks
  const tx = await fireblocks.createTransaction({
    assetId: chainIdToFireblocksAsset(chainId),
    destination: { type: 'ONE_TIME_ADDRESS', oneTimeAddress: { address: escrowAddress } },
    extraParameters: { contractCallData: calldata },
  });

  // Option 2: BitGo
  const tx = await bitgo.sendTransaction({
    address: escrowAddress,
    data: calldata,
    chain: chainId,
  });

  // Option 3: Any signing infrastructure
  const tx = await yourSigner.sendTransaction({
    to: escrowAddress,
    data: calldata,
    chainId,
  });

  res.json({ txHash: tx.hash });
});

Without Delegates (Tier 3)

If no delegates are provided and a wagmi provider is detected, the widget uses the connected wallet directly. No backend needed.
<WagmiProvider config={wagmiConfig}>
  <KeystoneProvider sessionToken={token}>
    {/* useDeposit will use the connected wallet */}
  </KeystoneProvider>
</WagmiProvider>

Without Delegates and No Wallet (Tier 1)

If no delegates and no wallet context, the deposit button is hidden. The settlement displays as view-only, and your backend handles deposits entirely through the SDK.