Skip to main content

Overview

This functionality is in closed beta - please reach out to us if you are interested in integrating!
Traditionally, sending blockchain transactions onchain has been painful:
  • You need to fund wallets with native gas tokens, creating onboarding friction
  • Network congestion and gas spikes can cause transactions to stall or get dropped altogether
Turnkey reduces this to a couple of API calls. We handle gassing 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. Chain Support:
  • Base - eip155:8453
  • Polygon - eip155:137
  • Ethereum - eip155:1 (Coming soon)
Interested in another chain? Reach out to us!

Concepts

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

A single endpoint lets you toggle between standard EIP-1559 transactions and sponsored transactions. Set sponsor: true to enable sponsorship.

Construction and Broadcast

Whether or not you use sponsorship, Turnkey handles transaction construction and broadcast. We auto-fill any fields you omit, so you can submit minimal payloads and let us handle the rest.

Transaction status and enriched transaction errors

After you send a transaction, Turnkey monitors its status until it fails or gets included in a block. For transactions that experience reversion errors, Turnkey runs a transaction simulation to produce structured execution traces and decode common revert reasons.

Spend limits

To prevent runaway spend, you must configure USD gas limits. Turnkey supports two separate limits: all orgs and each sub-org. This allows you to control both total USD spent and the amount of USD a single user has spent. You can configure the limit values and time window through the dashboard. You can query current gas usage and limits through our endpoints.

Policy engine

You can write policies against both sponsored and non-sponsored transactions using the normal eth.tx namespace in Turnkey’s policy DSL. This means you can seamlessly switch between sponsored and non-sponsored transactions and still use the same policies. Note: Turnkey sets all gas-related fields to 0 for sponsored transactions.

Billing

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

Advanced

Gas sponsorship smart contracts

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. Based on our benchmarks, these are the most efficient gas sponsorship contracts on the market.

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. Concretely, this means if Bob signs a request to send Alice 1 ETH, a malicious actor could replay that request many times, draining all of Bob’s ETH. At Turnkey, we never cut corners on security: we perform transaction construction in enclaves, and as long as the request includes the relevant nonce, 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. By default, our SDKs include a special gas station nonce for sponsored transaction requests.

RPCs

Turnkey’s transaction send and status endpoints eliminate the need for third-party RPC providers. You save costs and reduce latency because we holistically incorporate internal data and minimize 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:
    • signAndSendTransaction → submit
    • pollTransactionStatus → wait for inclusion
This page walks you through the React flow with full code examples. For using @turnkey/core directly, see Sending Sponsored Transactions.

Using handleSendTransaction (React)

This handler wraps everything: intent creation, signing, Turnkey submission, polling, modal UX, and final success UI.

Step 1 — Configure the Provider

import { TurnkeyProvider } from "@turnkey/react-wallet-kit";

const turnkeyConfig = {
  apiBaseUrl: "https://api.turnkey.com",
  defaultOrganizationId: process.env.NEXT_PUBLIC_TURNKEY_ORG_ID,
  rpId: window.location.hostname,
  iframeUrl: "https://auth.turnkey.com",
};

export default function App({ children }) {
  return (
    <TurnkeyProvider config={turnkeyConfig}>
      {children}
    </TurnkeyProvider>
  );
}

Step 2 — Use handleSendTransaction inside your UI

const { handleSendTransaction, wallets } = useTurnkey();

const walletAccount = wallets[0].accounts[0];

await handleSendTransaction({
  from: walletAccount.address,
  to: "0xRecipient",
  value: "1000000000000000",
  data: "0x",
  caip2: "eip155:8453",
  sponsor: true,
});
This automatically:
  • opens Turnkey modal
  • shows chain logo
  • polls until INCLUDED
  • displays success page + explorer link

Full React Handler Implementation

const handleSendTransaction = useCallback(
  async (params: HandleSendTransactionParams): Promise<void> => {
    const s = await getSession();
    const organizationId = params.organizationId || s?.organizationId;

    const {
      from,
      to,
      value,
      data,
      caip2,
      sponsor,
      gasLimit,
      maxFeePerGas,
      maxPriorityFeePerGas,
      nonce: providedNonce,
      successPageDuration = 2000,
    } = params;

    const { nonce } = await generateNonces({
      from: from,
      rpcUrl: DEFAULT_RPC_BY_CHAIN[caip2],
      providedNonce,
    });

    return new Promise((resolve, reject) => {
      const SendTxContainer = () => {
        const cleanedData =
          data && data !== "0x" && data !== "" ? data : undefined;

        const action = async () => {
          const { sendTransactionStatusId } =
            await client.signAndSendTransaction({
              walletAccount,
              organizationId,
              from,
              to,
              caip2,
              sponsor: !!sponsor,
              value,
              data: cleanedData,
              nonce,
              gasLimit,
              maxFeePerGas,
              maxPriorityFeePerGas,
            });

          const { txHash } = await client.pollTransactionStatus({
            httpClient: client.httpClient,
            organizationId,
            sendTransactionStatusId,
          });

          return { txHash };
        };

        return (
          <SendTransactionPage
            icon={<img src={getChainLogo(caip2)} className="h-10 w-10" />}
            action={action}
            caip2={caip2}
            successPageDuration={successPageDuration}
            onSuccess={() => resolve()}
            onError={(err) => reject(err)}
          />
        );
      };

      pushPage({
        key: "Send Transaction",
        content: <SendTxContainer />,
        preventBack: true,
        showTitle: false,
      });
    });
  },
  [pushPage, client]
);

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.
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
}