Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.startale.com/llms.txt

Use this file to discover all available pages before exploring further.

StartaleAccountClient exposes a small set of actions that cover almost every contract interaction you will write. This page shows how to encode calldata, send a single call, batch multiple calls atomically, and handle the most common errors.

Encode the calldata

Use encodeFunctionData from viem to turn a function call into ABI-encoded 0x bytes. The result becomes the data field of a Call object.
import { encodeFunctionData } from "viem"

const counterAbi = [
  { name: "count", type: "function", stateMutability: "nonpayable", inputs: [], outputs: [] },
] as const

const callData = encodeFunctionData({
  abi: counterAbi,
  functionName: "count",
})
SymbolSourceRole
encodeFunctionDataviemABI-encodes a function name + args into hex calldata.
If you already use viem’s getContract or writeContract, you can reuse those ABI fragments directly here.

Send a single call

const hash = await smartAccountClient.sendUserOperation({
  calls: [
    {
      to: counterAddress,
      data: callData,
      value: 0n,
    },
  ],
})

const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash })
SymbolSourceRole
smartAccountClient.sendUserOperation@startale-scs/aa-sdkBuilds, signs, and submits a UserOperation through the SCS bundler. Returns the UserOperation hash.
smartAccountClient.waitForUserOperationReceipt@startale-scs/aa-sdkPolls the bundler until the UserOperation is mined, then returns { success, receipt, ... }.
calls[].value is in wei. The smart account itself does not need to hold ETH unless you set a non-zero value and your paymaster does not cover it.

Batch multiple calls atomically

A single UserOperation can carry many calls. They are executed in order inside the same EVM transaction; if any one reverts, the whole UserOperation reverts and nothing is settled. This is built in to sendUserOperation: just append more entries to the calls array.
import type { Address } from "viem"

const calls: { to: Address; data: `0x${string}`; value: bigint }[] = [
  { to: tokenAddress, data: approveCalldata, value: 0n },
  { to: dexAddress, data: swapCalldata, value: 0n },
]

const hash = await smartAccountClient.sendUserOperation({ calls })
const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash })
Each entry in calls is a { to, value, data } object; ordering is preserved and execution is atomic. A working multi-call example using this exact pattern lives in scs-aa-quickstart/src/startale-minato/demo_install_modules.ts.
Batching is the canonical way to fix the “approve then swap” two-transaction UX. The user signs once, the bundler ships one transaction, and gas accounting is single-shot.

Read state from the same publicClient

The smart account does not change how you read onchain data. Reuse the publicClient you passed to createSmartAccountClient:
const count = await publicClient.readContract({
  abi: counterAbi,
  address: counterAddress,
  functionName: "getCount",
})

Handle errors

sendUserOperation throws when the bundler rejects the UserOperation, when the paymaster declines to sponsor it, or when validation reverts. A small triage helper goes a long way:
function explainUserOpError(error: unknown): string {
  const message = error instanceof Error ? error.message : String(error)

  if (message.includes("paymaster")) return "The paymaster declined to sponsor this UserOperation. Check policy limits and `paymasterId`."
  if (message.includes("insufficient funds")) return "The smart account or sponsor lacks funds for gas. Top up or switch to sponsored mode."
  if (message.includes("AA21") || message.includes("AA22")) return "Signature validation failed at the EntryPoint. Check the signer and validator module."
  if (message.includes("execution reverted")) return "A target contract reverted. Inspect the trace for the failing call."
  return `UserOperation failed: ${message}`
}
The AA** codes come from the EntryPoint contract; their full meaning is documented in the ERC-4337 spec.

Send a transaction-style call

If your code expects the viem sendTransaction shape, the smart account client supports it too. It compiles the call into a single-call UserOperation under the hood:
const hash = await smartAccountClient.sendTransaction({
  to: counterAddress,
  data: callData,
  value: 0n,
})
The return value is still a UserOperation hash; pass it to waitForUserOperationReceipt.

Next steps

Sponsored paymaster

Send the same calls with gas paid by your paymaster.

ERC-20 paymaster

Quote a token, approve it once, and pay gas in tokens.

Parallel transactions

Run independent UserOperations on different nonce lanes.

Smart sessions

Drop the signature prompt for repeated, scoped calls.