> ## Documentation Index
> Fetch the complete documentation index at: https://docs.turnkey.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Broadcasting (construction, signing, broadcast, gas abstraction)

> Guide for sending sponsored EVM transactions using @turnkey/core for custom frameworks, Node.js servers, or full manual control.

# Overview

Traditionally, sending blockchain transactions onchain has been painful:

* You need to fund wallets with native gas tokens, creating onboarding friction
* Network congestion and fee spikes can cause transactions to stall or get dropped altogether

Turnkey reduces this to a couple of API calls. We handle fees and our battle-tested broadcast logic ensures inclusion even under adverse network conditions. You and your users never touch gas tokens or deal with stuck transactions.

## Supported chains

**EVM (sponsored and non-sponsored):**

* **Base** - eip155:8453
* **Polygon** - eip155:137
* **Ethereum** - eip155:1
* **Arbitrum** - eip155:42161
* **Tempo** - eip155:4217
* **BNB Chain** - eip155:56

**EVM testnets (sponsored and non-sponsored):**

* **Base (Sepolia)** - eip155:84532
* **Polygon (Amoy)** - eip155:80002
* **Ethereum (Sepolia)** - eip155:11155111
* **Arbitrum (Sepolia)** - eip155:421614
* **Tempo Moderato** - eip155:42431
* **BNB Chain Testnet** - eip155:97

**Solana (sponsored):**

* **Solana mainnet** - solana:mainnet
* **Solana devnet** - solana:devnet

> Interested in another chain? Reach out to us!

<Note>
  To access sponsored transactions, ensure that Gas Sponsorship is first enabled within your Turnkey dashboard. Then set `sponsor: true` and update the `caip2` parameter with the corresponding chain identifier.
</Note>

## Construction and Broadcast

### EVM

A successful EVM transaction requires:

* **Transaction construction**: assembling the payload (recipient, value, calldata)
* **Nonce**: set correctly to order transactions and prevent conflicts
* **Gas and tip fee**: estimated to ensure inclusion even during network congestion
* **Signature**: cryptographically signing the transaction with the sender's private key
* **Broadcast**: submitting the signed transaction to the network and monitoring for inclusion

Turnkey handles all of this for you via `ethSendTransaction`. Whether or not you use sponsorship, you pass through minimal payloads and we take care of the rest. We auto-fill any fields you omit.

This endpoint supports arbitrary EVM transactions — not just simple sends. You can interact with smart contracts, deploy contracts, or execute any valid EVM operation.

### Solana

A successful Solana transaction requires:

* **Transaction construction**: assembling the list of instructions (program, accounts, data)
* **Recent blockhash**: fetched and attached at broadcast time to ensure the transaction is valid
* **Compute unit limit**: estimated and set to prevent failed transactions due to insufficient compute
* **Priority fee**: set to ensure timely inclusion under current network conditions
* **Signature**: cryptographically signing the transaction with the sender's private key
* **Broadcast**: submitting the signed transaction to the network and monitoring for confirmation

Turnkey handles all of this for you via `solSendTransaction`. Whether or not you use sponsorship, you pass through a minimal payload and we manage the rest.

<Note>
  On Solana, fee sponsorship and rent sponsorship are separate. `Sponsor Solana Rent` is disabled by default and must be enabled in the dashboard before Turnkey will pre-fund rent for account creation. If created accounts are later closed, refunded rent can go back to the signer rather than the sponsor. See [Solana Rent Sponsorship](/features/networks/solana-rent-refunds). For payer behavior, static-key requirements, and account-creation caveats in sponsored flows, see [Solana transaction construction for sponsored flows](/features/networks/solana-transaction-construction).
</Note>

## Concepts

### Gas sponsorship (aka gas abstraction, gasless transactions, fee abstraction)

A single endpoint lets you toggle between standard and sponsored transactions. With sponsorship enabled, your users never need to hold native tokens to pay transaction fees — Turnkey covers them. Set `sponsor: true` to enable sponsorship, or `sponsor: false` to have fees paid by the sender's wallet.

Either way, Turnkey handles construction, signing, broadcast, and status monitoring. The `sponsor` flag only controls who pays the fee.

<Note>
  Gas Sponsorship is available on **Enterprise** plans.

  * **Enterprise:** Unlimited spend, with configurable time windows

  Pay-as-you-go and Pro customers can still access transaction construction, signing, and broadcast. If you'd like to leverage gas sponsorship, please reach out!
</Note>

### Spend limits

Turnkey gives you USD-denominated controls over gas sponsorship spend at two levels:

* **Organization-wide limit** — the cap on total sponsored spend across your parent organization and all of its sub-organizations.
* **Sub-organization limit** — the cap that applies to each of your sub-organizations

You can set limit values and time intervals through the dashboard. You can also query current usage against the active limit via the [`get_gas_usage`](https://docs.turnkey.com/api-reference/queries/get-gas-usage) endpoint.

<Tip>
  Turnkey provides fee sponsorship and transaction broadcasting services only. In high-fee or congested network conditions, delays or non-inclusion may occur. It is the developer's responsibility to ensure appropriate spend limits are in place.
</Tip>

### Policy engine

You can write policies against both sponsored and non-sponsored transactions using Turnkey's policy DSL:

* **EVM**: use the `eth.tx` namespace
* **Solana**: use the `solana.tx` namespace

This means you can seamlessly switch between sponsored and non-sponsored transactions and still use the same policies.

*Note:* Turnkey sets all fee-related fields to 0 for sponsored transactions.

### Billing

Turnkey passes transaction fee costs through to you as a line item at the end of the month. You pay based on the USD value of fees at time of broadcast; Turnkey internalizes the inventory risk of token price changes. Our battle-tested fee estimation aims to be cost-efficient while ensuring quick transaction inclusion.

### Advanced

#### Gas sponsorship smart contracts (EVM)

We could not find a satisfactory setup for gas sponsorship contracts that were both fast and safe, so we made our own. The contracts are open source and you can check them out on [GitHub](https://github.com/tkhq/gas-station).

Based on our benchmarks, these are the most efficient gas sponsorship contracts on the market. They achieve this through optimized logic, calldata encoding, and extensive use of assembly, which reduces gas overhead per sponsored transaction. The result: lower costs for you and faster execution for your users.

#### Security

Some gas sponsorship setups by other providers are subject to replay attacks. If a malicious actor compromises the provider infrastructure, they can replay the gas sponsorship request multiple times with different nonces to create multiple transactions from a single request.

At Turnkey, we never cut corners on security: we perform transaction construction in enclaves, and as long as the request includes the relevant nonce or blockhash, only one transaction can be created from it. Since the user's authenticator signs requests and the enclave verifies signatures, a malicious actor cannot modify or replay the request. This is in line with Turnkey's core system design principle: everything can be compromised outside of the enclaves and funds will still be safe.

By default, our SDKs include a special gas station nonce for sponsored transaction requests.

### RPCs

Turnkey's send transaction and transaction status endpoints eliminate the need for third-party RPC providers. You save costs and reduce latency because we holistically incorporate internal data and minimize external calls.

## SDK Overview

> The SDK primarily abstracts three endpoints: `eth_send_transaction`, `get_send_transaction_status`, and `get_gas_usage`.

You can sign and broadcast transactions in two primary ways:

1. **Using the React handler (`handleSendTransaction`) from `@turnkey/react-wallet-kit`**
   This gives you:
   * modals
   * spinner + chain logo
   * success screen
   * explorer link
   * built-in polling

2. **Using low-level functions in `@turnkey/core`**
   You manually call:
   * `ethSendTransaction` OR `solSendTransaction` → submit
   * `pollTransactionStatus` → wait for inclusion

3. **Using server-side `@turnkey/sdk-server`**
   This is the right choice for Node.js backends. It exposes the same methods via the server SDK client.

This page walks you through the `@turnkey/core` flow with full code examples. For using the React handler, see [Sending Sponsored Transactions (React)](/features/transaction-management/sending-sponsored-transactions).

***

## Using `@turnkey/core` directly

For custom frameworks or full manual control (client-side).

You will call:

### `ethSendTransaction(params)`

→ returns `{ sendTransactionStatusId }`

### `solSendTransaction(params)`

→ returns `{ sendTransactionStatusId }`

### `pollTransactionStatus(params)`

→ returns chain-specific status (`eth.txHash` or `sol.signature`)

### Step 1 — Create a client

If you're on a Node.js backend, use `@turnkey/sdk-server` and initialize the server client like this:

```ts theme={"system"}
import { Turnkey } from "@turnkey/sdk-server";

const client = new Turnkey({
  apiBaseUrl: "https://api.turnkey.com/",
  apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY,
  apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
  defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,
}).apiClient();
```

For `@turnkey/core`, create the client like this:

```ts theme={"system"}
import { Turnkey } from "@turnkey/core";

const client = new Turnkey({
  apiBaseUrl: "https://api.turnkey.com",
  defaultOrganizationId: process.env.TURNKEY_ORG_ID,
});
```

***

### Step 2 — Submit the transaction (Ethereum)

```ts theme={"system"}
const sendTransactionStatusId = await client.ethSendTransaction({
  transaction: {
    from: walletAccount.address,
    to: "0xRecipient",
    caip2: "eip155:8453",
    sponsor: true,
    value: "0",
    data: "0x",
    nonce: "0",
  },
});
```

OR (Solana):

```ts theme={"system"}
const sendTransactionStatusId = await client.solSendTransaction({
  transaction: {
    signWith: walletAccount.address, // Solana address
    unsignedTransaction: "<hex-serialized-unsigned-solana-tx>",
    caip2: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // devnet
    sponsor: true,
    // recentBlockhash: "<recent blockhash>", // optional
  },
});
```

***

### Step 3 — Wait for inclusion

```ts theme={"system"}
const pollResult = await client?.pollTransactionStatus({
  sendTransactionStatusId,
});

if (!pollResult) {
  throw new TurnkeyError(
    "Polling returned no result",
    TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR,
  );
}

const txHash = pollResult.eth?.txHash; // Ethereum
const signature = pollResult.sol?.signature; // Solana
const transactionId = txHash ?? signature;

if (!transactionId) {
  throw new TurnkeyError(
    "Missing transaction id in transaction result",
    TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR,
  );
}
console.log(transactionId);
```

***

* `ethSendTransaction` Implementation [here](https://github.com/tkhq/sdk/blob/e1dfe3e2eeb0976069aad1799597bbed64ec52f5/packages/core/src/__clients__/core.ts#L2711)
* `solSendTransaction` Implementation [here](https://github.com/tkhq/sdk/blob/e1dfe3e2eeb0976069aad1799597bbed64ec52f5/packages/core/src/__clients__/core.ts#L2848)
* `pollTransactionStatus` Implementation [here](https://github.com/tkhq/sdk/blob/e1dfe3e2eeb0976069aad1799597bbed64ec52f5/packages/core/src/__clients__/core.ts#L2936)

***

## Transaction status and enriched errors

After you send a transaction, Turnkey monitors its status until it fails or is confirmed onchain.
You can [query the transaction status](#querying-via-api) or subscribe to status updates [via
webhooks](#webhooks).

### Transaction statuses

The following statuses apply to both EVM and Solana transactions:

| **Status**   | **Description**                                                                                          |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| INITIALIZED  | Turnkey has constructed and signed the transaction and prepared fees, but it has not yet been broadcast. |
| BROADCASTING | Turnkey is actively broadcasting the transaction to the network and awaiting inclusion.                  |
| INCLUDED     | The transaction has been included in a block (EVM) or confirmed onchain (Solana).                        |
| FAILED       | The transaction could not be included onchain and will not be retried automatically.                     |

### EVM smart contract transaction errors

For EVM transactions that revert, Turnkey runs a simulation to produce structured execution traces
and decode common revert reasons — giving you actionable error messages instead of opaque hex data.

| **Error type** | **Description**                                                                                                                                        |
| :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| UNKNOWN        | The transaction reverted during onchain execution or simulation, but the revert reason could not be decoded (e.g. missing ABI or unverified contract). |
| NATIVE         | The transaction reverted due to a built-in Solidity error, such as `require()`, `assert()`, or a plain `revert()`.                                     |
| CUSTOM         | The transaction reverted due to a contract-defined custom error declared using Solidity's `error` keyword.                                             |

<Note>
  These error types describe how an EVM smart contract reverted during onchain execution or
  pre-flight simulation. Turnkey application-level errors (e.g. signing failures, policy rejections)
  are not classified here and are instead surfaced via `Error.Message`.
</Note>

### Querying via API

Use the [Get Send Transaction Status](/api-reference/queries/get-send-transaction-status) endpoint
to poll for the current status of any transaction by its `sendTransactionStatusId` (returned when
you call `ethSendTransaction` or `solSendTransaction`).

The response includes a `txStatus` field with the current status and, when applicable, an `error`
object containing a human-readable `message` and either `eth.revertChain` (for EVM reverts) or
`solana` (for Solana failures) with full structured details.

### Webhooks

Turnkey Webhooks let you react to transaction status updates in real time, without polling. Instead
of repeatedly calling the [Get Send Transaction Status](/api-reference/queries/get-send-transaction-status)
API, you register an endpoint and Turnkey pushes updates to you — an HTTP POST fires when a
transaction status changes (e.g. from `BROADCASTING` to `INCLUDED` or `FAILED`).

You subscribe to webhooks at the parent organization level. Subscriptions cover transactions across
the parent organization and all of its sub-organizations.

#### Subscribing

Use the [Create Webhook Endpoint](/api-reference/activities/create-webhook-endpoint) API with the
`SEND_TRANSACTION_STATUS_UPDATES` event type to register your endpoint on the parent organization.

#### Delivery payload

Each delivery is an HTTP POST with a JSON body containing a `type`, `organizationId`,
`parentOrganizationId`, and a `msg` object. The `type` is always `"transaction:status"`. Fields
present in `msg` depend on the status:

* **BROADCASTING**: base fields only — no `txHash`, no `error`
* **INCLUDED**: base fields + `txHash`. If the transaction reverted onchain, `error` is also present.
* **FAILED**: base fields + `error`. No `txHash` (the transaction never landed onchain).

| **Field**                     | **Description**                                                                                               |
| :---------------------------- | :------------------------------------------------------------------------------------------------------------ |
| `type`                        | Always `"transaction:status"`.                                                                                |
| `organizationId`              | The organization ID that initiated the transaction.                                                           |
| `parentOrganizationId`        | The parent organization ID.                                                                                   |
| `msg.sendTransactionStatusId` | The ID of the send transaction status record.                                                                 |
| `msg.activityId`              | The ID of the originating Turnkey activity.                                                                   |
| `msg.status`                  | One of `BROADCASTING`, `INCLUDED`, or `FAILED`.                                                               |
| `msg.caip2`                   | The chain identifier where the transaction was sent.                                                          |
| `msg.idempotencyKey`          | A stable, unique key for this status event. Use this to safely deduplicate webhook deliveries.                |
| `msg.timestamp`               | Unix timestamp (seconds) when the notification was generated.                                                 |
| `msg.txHash`                  | *(INCLUDED only)* The onchain transaction hash or Solana signature.                                           |
| `msg.error`                   | Structured error object. Contains `message`, and either `eth.revertChain` (EVM) or `solana` (Solana) details. |

**BROADCASTING**

```json theme={"system"}
{
  "type": "transaction:status",
  "organizationId": "9e8d7c6b-aaaa-bbbb-cccc-ddddeeee0000",
  "parentOrganizationId": "9e8d7c6b-aaaa-bbbb-cccc-ddddeeee0000",
  "msg": {
    "sendTransactionStatusId": "f3a2b1c0-1234-5678-abcd-ef0123456789",
    "activityId": "a1b2c3d4-0000-1111-2222-333344445555",
    "status": "BROADCASTING",
    "caip2": "eip155:1",
    "idempotencyKey": "3f4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3",
    "timestamp": 1746000000
  }
}
```

**INCLUDED**

```json theme={"system"}
{
  "type": "transaction:status",
  "organizationId": "9e8d7c6b-aaaa-bbbb-cccc-ddddeeee0000",
  "parentOrganizationId": "9e8d7c6b-aaaa-bbbb-cccc-ddddeeee0000",
  "msg": {
    "sendTransactionStatusId": "f3a2b1c0-1234-5678-abcd-ef0123456789",
    "activityId": "a1b2c3d4-0000-1111-2222-333344445555",
    "status": "INCLUDED",
    "caip2": "eip155:1",
    "idempotencyKey": "7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8",
    "timestamp": 1746000042,
    "txHash": "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1"
  }
}
```

**FAILED**

```json theme={"system"}
{
  "type": "transaction:status",
  "organizationId": "9e8d7c6b-aaaa-bbbb-cccc-ddddeeee0000",
  "parentOrganizationId": "9e8d7c6b-aaaa-bbbb-cccc-ddddeeee0000",
  "msg": {
    "sendTransactionStatusId": "f3a2b1c0-1234-5678-abcd-ef0123456789",
    "activityId": "a1b2c3d4-0000-1111-2222-333344445555",
    "status": "FAILED",
    "caip2": "eip155:1",
    "idempotencyKey": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2",
    "timestamp": 1746000015,
    "error": {
      "message": "Execution reverted on chain: insufficient balance for transfer",
      "eth": {
        "revertChain": [
          {
            "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
            "errorType": "native",
            "nativeType": "error_string",
            "displayMessage": "insufficient balance for transfer"
          }
        ]
      }
    }
  }
}
```

<Tip>
  See the [with-tx-webhooks](https://github.com/tkhq/sdk/tree/main/examples/transaction-management/with-tx-webhooks) SDK
  example for a working integration.
</Tip>

***

## Checking Gas Usage

You can configure gas limits for both sub-orgs and all orgs. We recommend checking sub-org gas usage against the limit on the client side so your application can handle edge cases when approaching or exceeding the gas limit.

You may also want to monitor your all org gas usage regularly to see if you are approaching your gas limit.

```ts theme={"system"}
const resp = await httpClient?.getGasUsage({})
if (resp?.usageUsd! > resp?.windowLimitUsd!) { // you can also configure this to be a threshold
  console.error("Gas usage limit exceeded for sponsored transactions");
  return
}
```
