Skip to main content
When ILLA needs your application to execute an on-chain action, the response includes pendingTools. Your app becomes the executor: run those tools, collect the result, and send the outcome back with sendToolResults.

What this guide covers

  • how to read pendingTools
  • how to run the standard execution loop
  • how to handle ActionSequence and signature requests
  • how to use simulation previews safely
  • when to use polling helpers

Read pendingTools

Each item in pendingTools includes:
  • toolCallId
  • toolName
  • input
  • simulated (optional)
  • simulation (optional simulation preview payload)
input.type can be:
  • SingleTransaction
  • BatchTransactions
  • SignatureRequests
  • ActionSequence
tool.input is always the execution source of truth. Optional simulation fields are for display and review.

Run the standard execution loop

Use batched outcome submission (sendToolResults) and continue until no more tools are returned.
import { IllaSDK } from '@illalabs/sdk'
import {
  IllaToolError,
  IllaToolOutcome,
  type IllaToolOutcomeJSON,
  type PendingToolCallType,
} from '@illalabs/interfaces'

const sdk = new IllaSDK({ apiKey: process.env.ILLA_API_KEY! })

async function executePendingTool(tool: PendingToolCallType): Promise<IllaToolOutcomeJSON> {
  try {
    // Your executor should handle:
    // SingleTransaction / BatchTransactions / SignatureRequests / ActionSequence
    const executionResult = await walletExecutor.execute(tool.input)

    return IllaToolOutcome
      .success(tool.toolCallId, tool.toolName, executionResult)
      .toJSON()
  } catch (error) {
    return IllaToolOutcome
      .error(
        tool.toolCallId,
        tool.toolName,
        IllaToolError.execution(
          tool.toolCallId,
          tool.toolName,
          error instanceof Error ? error.message : 'Unknown execution error',
        ),
      )
      .toJSON()
  }
}

async function runToolLoop(
  chatId: string,
  initialTools: ReadonlyArray<PendingToolCallType>,
): Promise<void> {
  let pendingTools = [...initialTools]

  while (pendingTools.length > 0) {
    const outcomes = await Promise.all(pendingTools.map(executePendingTool))

    const followUp = await sdk.sendToolResults(chatId, outcomes)
    if (followUp.response.isError) {
      throw new Error(followUp.response.error.message)
    }

    pendingTools = [...(followUp.response.data.pendingTools ?? [])]
  }
}
This is the default pattern for SDK integrations.

Handle ActionSequence

When input.type is ActionSequence, the value is an ordered array of steps. Execute steps sequentially and preserve the server-defined order.
async function executeActionSequence(steps: ActionStep[]): Promise<unknown[]> {
  const results = []

  for (const step of steps) {
    switch (step.type) {
      case 'SingleTransaction':
        results.push(await sendTransaction(step.value.transaction))
        break
      case 'BatchTransactions':
        results.push(await sendBatchTransactions(step.value))
        break
      case 'SignatureRequests':
        results.push(await collectSignatures(step.value.signatureRequests))
        break
    }
  }

  return results
}
A common pattern is a prediction-market flow that transfers funds first and collects a signature second.

Handle signature requests

Some tools return SignatureRequests directly, or as a step inside ActionSequence. Signature requests use either EIP-712 typed data (signMethod: 'typedData') or raw hash signing (signMethod: 'message'). Your executor should collect signatures and return them in the tool result payload, then continue the loop normally. Common flows include:
  • predictionMarketsBet
  • predictionMarketsRedeem
  • polymarketPostOrder
  • polymarketPostRedeem

Use simulation previews for review UI

When simulation is enabled server-side, pending tools may include a simulation object that you can use to build a confirmation UI before execution.
for (const tool of pendingTools) {
  if (!tool.simulation) continue

  const sim = tool.simulation

  if (!sim.willSucceed) {
    console.warn('Simulation predicts failure:', sim.errorExplanation)
  }

  for (const change of sim.assetChanges) {
    const sign = change.direction === 'out' ? '-' : '+'
    console.log(`${sign}${change.formattedAmount} ${change.tokenSymbol} ($${change.usdValue})`)
  }

  console.log(
    `Gas: ${sim.gasEstimate.gasCostNative} ${sim.nativeTokenSymbol ?? 'ETH'} ($${sim.gasEstimate.gasCostUsd})`,
  )
}
Key fields worth surfacing:
  • willSucceed
  • assetChanges
  • gasEstimate
  • actionType
  • contractInteractions
  • actionMetadata
Treat simulation as display-only metadata. tool.input remains the execution source of truth.

End-to-end example

const first = await sdk.sendMessage(
  'Swap 1 ETH to USDC on Base',
  { userContext: { address: '0x1234...' } },
)

if (first.response.isError) {
  throw new Error(first.response.error.message)
}

await runToolLoop(first.chatId, first.response.data.pendingTools ?? [])

const finalMessages = await sdk.getMessages(first.chatId)
console.log(finalMessages)

Track long-running tools

For tools that may take longer after submission (for example bridge completion), use polling helpers with the transaction hash:
const subscription = sdk.subscribeToToolStatus(
  {
    toolName: 'bridge',
    protocol: 'lifi',
    descriptor: { txHash: '0x...' },
  },
  {
    onStatusChange: ({ current }) => console.log(current.pollStatus),
    onError: (error) => console.error(error),
  },
  {
    interval: 5,
    retryThreshold: 30,
    maxRetries: 5,
    maxDuration: 600,
  },
)

subscription.unsubscribe()

Common mistakes

  1. Ignoring failed tools instead of returning an error outcome.
  2. Executing ActionSequence steps out of order.
  3. Using simulation output as the execution payload.
  4. Sending many single tool results when one batched call would work.