> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Send gasless transactions

## Install dependencies

:::code-group
```sh [npm]
npm install viem

```

```sh [yarn]
yarn add viem

```

```sh [pnpm]
pnpm add viem

```
:::

`sendTransaction` handles the full gasless flow in a single call: it upgrades the wallet to a [Delegated Account](https://eips.ethereum.org/EIPS/eip-7702) if needed, creates the transaction intent, signs it, and submits it. You only need a [gas sponsorship](https://www.openfort.io/docs/configuration/gas-sponsorship) configured.

::::steps
## Create a backend wallet

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

const account = await openfort.accounts.evm.backend.create()
```

## Send a gasless transaction

```ts
const result = await openfort.accounts.evm.backend.sendTransaction({
  account,
  chainId: 84532, // Base Sepolia
  interactions: [
    {
      to: '{{CONTRACT_ADDRESS}}',
      data: '{{CALLDATA}}',
    },
  ],
  policy: '{{FEE_SPONSORSHIP_ID}}',
})
```

## Wait for the transaction to land

`sendTransaction` returns as soon as the transaction **intent** exists, which is not the same as the transaction being mined. `result.response?.transactionHash` is often `undefined` on the first read — poll the intent until a hash appears:

```ts
async function waitForHash(id: string, tries = 20) {
  for (let i = 0; i < tries; i++) {
    const intent = await openfort.transactionIntents.get(id)
    const hash = intent.response?.transactionHash
    if (hash) return hash
    await new Promise((resolve) => setTimeout(resolve, 1000))
  }
  throw new Error(`Transaction ${id} produced no hash — it may not have been broadcast.`)
}

const hash = result.response?.transactionHash ?? (await waitForHash(result.id))
console.log('Transaction hash:', hash)
```

:::warning
Treating the `sendTransaction` return value as success is the most common mistake with this API. The call resolves for a transaction that has been *created*, not one that has *landed*, so an application that logs the result and moves on will report success for transfers that never reached the chain. Always confirm a transaction hash before recording a movement.

If no hash ever arrives, read the intent's own state — `intent.transitions` shows the statuses it moved through, and `intent.nextAction` is set when it is waiting on a signature and was therefore never submitted.
:::
::::

## Batching

`interactions` is an array, and every entry is executed in **one** transaction. A wallet moving three tokens produces three ERC-20 `transfer` calls signed and submitted together, not three transactions to sequence and reconcile:

```ts
const result = await openfort.accounts.evm.backend.sendTransaction({
  account,
  chainId: 84532,
  interactions: [
    { to: USDC, value: '0', data: transferUsdc },
    { to: USDT, value: '0', data: transferUsdt },
    { to: DAI, value: '0', data: transferDai },
  ],
  policy: '{{FEE_SPONSORSHIP_ID}}',
})
```

:::info
A single transaction accepts at most **9 interactions**. More than that is rejected with `A maximum of 9 interactions per transaction are allowed`. Split larger batches into chunks and send them sequentially — they share one wallet, and therefore one nonce.
:::

## Gas sponsorship IDs

The `policy` parameter takes a **fee sponsorship** ID, not a policy ID. The two are different objects with different prefixes, and passing a policy ID is rejected with `Invalid pol`:

| Prefix | Object |
|--------|--------|
| `acc_` | Account |
| `ply_` | Policy |
| `pol_` | Fee sponsorship |
| `pla_` | Wallet |
| `tin_` | Transaction intent |

[Gas sponsorship](https://www.openfort.io/docs/configuration/gas-sponsorship) is two linked objects: a policy (`ply_`) whose rules accept the `sponsorEvmTransaction` operation, and a fee sponsorship (`pol_`) that references it. `policy` wants the fee sponsorship.

:::tip
The `policy` parameter is optional. If you omit it, Openfort uses your project-scoped gas sponsorship automatically. Pass a specific `{{FEE_SPONSORSHIP_ID}}` to override with a particular sponsorship from the [Openfort dashboard](https://www.openfort.io/docs/configuration/gas-sponsorship). See the [full sendTransaction example](https://github.com/openfort-xyz/openfort-node/tree/main/examples/evm/transactions/sendTransaction.ts) in the Node SDK repository.
:::

## Choosing a send path

There are two ways to send a sponsored transaction from a backend wallet. They are not interchangeable:

| | `backend.sendTransaction` | Bundler + paymaster |
|---|---|---|
| Setup | One call | Build a smart account and bundler client |
| Delegation | Handled for you (EIP-7702) | You choose the account implementation |
| Address that holds funds | The backend wallet address | The smart account address, **not** the backend wallet |
| Batching | `interactions` array | `calls` array |
| Confirmation | Poll the intent for a hash | `waitForUserOperationReceipt` |

Use `sendTransaction` unless you need control over the account implementation or want to drive the ERC-4337 stack yourself. For the second path, see [viem integration](https://www.openfort.io/docs/products/server/evm/viem-integration#sending-a-gasless-transaction), where the backend wallet only signs hashes and owns a smart account that holds the balances.

Which accounts each API accepts, and why `create()` alone is not enough for the second path, is covered in [account types](https://www.openfort.io/docs/products/server/accounts#account-types).
