A reusable SDK for implementing gasless transactions using EIP-7702, Turnkey wallet management, and your own paymaster. This package provides clean abstractions and utility methods to quickly integrate with Turnkey's contracts for sponsored transaction execution.
This SDK enables you to:
- Bring your own paymaster to sponsor user transactions
- Use Turnkey for secure wallet management and transaction signing
- Execute gasless transactions via EIP-7702 delegation and EIP-712 signed intents
- Support any on-chain action through generic execution parameters
Perfect for building dApps where users don't need ETH for gas, enabling seamless onboarding and better UX.
- EIP-7702 Authorization: One-time setup where an EOA authorizes a gas station contract
- EIP-712 Signed Intents: User signs off-chain intents for what they want to execute
- Paymaster Execution: Your paymaster submits the transaction and pays for gas
- Turnkey Integration: All signatures handled securely through Turnkey
pnpm install @turnkey/gas-station @turnkey/sdk-server @turnkey/viem viemCreate .env.local:
# Turnkey Configuration
BASE_URL=https://api.turnkey.com
API_PRIVATE_KEY=your_turnkey_api_private_key
API_PUBLIC_KEY=your_turnkey_api_public_key
ORGANIZATION_ID=your_turnkey_organization_id
# Wallet Addresses
EOA_ADDRESS=0x... # User's wallet address
PAYMASTER_ADDRESS=0x... # Your paymaster address
# RPC Configuration
BASE_RPC_URL=https://mainnet.base.org
ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/...
# Gas Station Contracts (Optional - defaults to deterministic addresses)
DELEGATE_CONTRACT=0x... # EIP-7702 delegate contract
EXECUTION_CONTRACT=0x... # Gas Sponsorship entrypoint contract which calls the delegate.Note: The gas station contracts are currently deployed at deterministic addresses on the following chains:
- Ethereum Mainnet
- Base Mainnet
These addresses are built into the SDK, so you don't need to specify them unless you are using custom deployments.
import { GasStationClient } from "@turnkey/gas-station";
import { Turnkey } from "@turnkey/sdk-server";
import { createAccount } from "@turnkey/viem";
import { parseEther, parseUnits, createWalletClient, http } from "viem";
import { base } from "viem/chains";
// Initialize Turnkey
const turnkeyClient = new Turnkey({
apiBaseUrl: process.env.BASE_URL!,
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
});
// Create Turnkey accounts
const userAccount = await createAccount({
client: turnkeyClient.apiClient(),
organizationId: process.env.ORGANIZATION_ID!,
signWith: process.env.EOA_ADDRESS as `0x${string}`,
});
const paymasterAccount = await createAccount({
client: turnkeyClient.apiClient(),
organizationId: process.env.ORGANIZATION_ID!,
signWith: process.env.PAYMASTER_ADDRESS as `0x${string}`,
});
// Create viem wallet clients
const userWalletClient = createWalletClient({
account: userAccount,
chain: base,
transport: http(process.env.BASE_RPC_URL!),
});
const paymasterWalletClient = createWalletClient({
account: paymasterAccount,
chain: base,
transport: http(process.env.BASE_RPC_URL!),
});
// Create Gas Station clients
const userClient = new GasStationClient({
walletClient: userWalletClient,
});
const paymasterClient = new GasStationClient({
walletClient: paymasterWalletClient,
});
// One-time: Authorize the EOA to use gas station
const authorization = await userClient.signAuthorization();
await paymasterClient.submitAuthorizations([authorization]);
// Execute a gasless ETH transfer
let nonce = await userClient.getNonce();
const ethIntent = await userClient
.createIntent()
.transferETH("0xRecipient...", parseEther("0.1"))
.sign(nonce);
await paymasterClient.execute(ethIntent);
// Execute a gasless token transfer
nonce = await userClient.getNonce();
const usdcIntent = await userClient
.createIntent()
.transferToken(
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
"0xRecipient...",
parseUnits("10", 6),
)
.sign(nonce);
await paymasterClient.execute(usdcIntent);Main client for gas station operations. Each client instance wraps a viem wallet client.
new GasStationClient({
walletClient: WalletClient, // Viem wallet client (e.g., with Turnkey account)
delegateContract?: `0x${string}`, // Optional: defaults to deterministic address
executionContract?: `0x${string}`, // Optional: defaults to deterministic address
})End User Methods (call with user client):
signAuthorization(): Promise<SignedAuthorization>
- Sign an EIP-7702 authorization for the gas station contract
- Returns authorization that can be submitted by paymaster
createIntent(): IntentBuilder
- Create a builder for composing transactions
- Intent must be signed before execution
getNonce(address?: Address): Promise<bigint>
- Get current nonce from gas station contract
- Defaults to the signer's address if not specified
Paymaster Methods (call with paymaster client):
submitAuthorizations(authorizations: SignedAuthorization[]): Promise<{ txHash, blockNumber }>
- Submit signed EIP-7702 authorization transaction(s)
- Supports authorizing multiple EOAs in a single transaction
- Paymaster pays for gas
execute(intent: ExecutionIntent): Promise<{ txHash, blockNumber, gasUsed }>
- Execute a signed intent through the gas station
- Paymaster pays for gas
Composable builder for complex multi-step transactions.
const nonce = await userClient.getNonce();
const builder = userClient.createIntent();
const intent = await builder
.transferToken(usdcAddress, recipient, amount)
.sign(nonce);
await paymasterClient.execute(intent);// Gasless USDC payment
const nonce = await userClient.getNonce();
const intent = await userClient
.createIntent()
.transferToken(usdcAddress, recipientAddress, parseUnits("50", 6))
.sign(nonce);
const result = await paymasterClient.execute(intent);
console.log(`Payment sent: ${result.txHash}`);// Step 1: Approve DEX to spend tokens
let nonce = await userClient.getNonce();
const approvalIntent = await userClient
.createIntent()
.approveToken(usdcAddress, dexAddress, parseUnits("100", 6))
.sign(nonce);
await paymasterClient.execute(approvalIntent);
// Step 2: Execute swap
nonce = await userClient.getNonce();
const swapIntent = await userClient
.createIntent()
.callContract({
contract: dexAddress,
abi: DEX_ABI,
functionName: "swapExactTokensForTokens",
args: [amountIn, amountOutMin, path, recipient, deadline],
})
.sign(nonce);
await paymasterClient.execute(swapIntent);async function onboardUser(userAddress: string) {
// Create viem wallet client for user
const userAccount = await createAccount({
client: turnkeyClient.apiClient(),
organizationId: ORGANIZATION_ID,
signWith: userAddress as `0x${string}`,
});
const userWalletClient = createWalletClient({
account: userAccount,
chain: base,
transport: http(BASE_RPC_URL),
});
// Create Gas Station clients
const userClient = new GasStationClient({
walletClient: userWalletClient,
});
// Authorize user (paymaster pays)
const authorization = await userClient.signAuthorization();
await paymasterClient.submitAuthorizations([authorization]);
// User can now execute transactions without ETH
console.log("✅ User ready for gasless transactions!");
}- Delegate Contract: Authorized to EOA via EIP-7702
- Execution Contract: Contains execution logic and nonce management
- EOA: Signs EIP-712 intents off-chain
- Paymaster: Submits transactions and pays gas
User (EOA)
↓ Signs EIP-712 intent off-chain
↓
SDK (GasStationClient)
↓ Builds transaction
↓
Paymaster
↓ Submits transaction, pays gas
↓
Gas Station Contract
↓ Validates signature & nonce
↓ Executes on behalf of EOA
↓
Target Contract (USDC, NFT, DEX, etc.)
Available presets for quick setup:
- BASE_MAINNET - Base mainnet (includes USDC address)
- ETHEREUM_MAINNET - Ethereum mainnet (includes USDC address)
// Chain presets are available for quick configuration
import { CHAIN_PRESETS, GasStationClient } from "@turnkey/gas-station";
import { createWalletClient, http } from "viem";
const basePreset = CHAIN_PRESETS.BASE_MAINNET;
const userWalletClient = createWalletClient({
account: userAccount,
chain: basePreset.chain,
transport: http(basePreset.rpcUrl),
});
const userClient = new GasStationClient({
walletClient: userWalletClient,
});- EIP-712 Signed Intents: All executions require valid typed signatures
- EIP-7702 Scoping: Authorization is per-EOA and can be revoked
- Deadline Enforcement: Each transaction includes a deadline (Unix timestamp) to prevent replay attacks; signatures expire after this time
- Turnkey Integration: Private keys never leave Turnkey's secure infrastructure
Turnkey policies provide additional security layers by restricting what transactions can be signed and executed. The Gas Station SDK includes helpers for creating these policies.
Restrict what EIP-712 intents the EOA can sign:
import { buildIntentSigningPolicy } from "@turnkey/gas-station";
// USDC-only policy
const eoaPolicy = buildIntentSigningPolicy({
organizationId: "a5b89e4f-1234-5678-9abc-def012345678",
eoaUserId: "3c7d6e8a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
restrictions: {
allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"], // USDC on Base
disallowEthTransfer: true, // Disallow ETH transfers
},
policyName: "USDC Only Policy",
});
// Resulting policy restricts signing to USDC transfers only:
// {
// organizationId: "a5b89e4f-1234-5678-9abc-def012345678",
// policyName: "USDC Only Policy",
// effect: "EFFECT_ALLOW",
// consensus: "approvers.any(user, user.id == '3c7d6e8a-4b5c-6d7e-8f9a-0b1c2d3e4f5a')",
// condition: "activity.resource == 'PRIVATE_KEY' && " +
// "activity.action == 'SIGN' && " +
// "eth.eip_712.primary_type == 'Execution' && " +
// "(eth.eip_712.message['to'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
// "eth.eip_712.message['value'] == '0'",
// notes: "Restricts which EIP-712 intents the EOA can sign for gas station execution"
// }Restrict what on-chain transactions the paymaster can submit:
import {
buildPaymasterExecutionPolicy,
DEFAULT_EXECUTION_CONTRACT,
ensureGasStationInterface,
} from "@turnkey/gas-station";
import { parseGwei, parseEther } from "viem";
// First, ensure the Gas Station ABI is uploaded (enables ABI-based policies)
await ensureGasStationInterface(
turnkeyClient.apiClient(),
"your-org-id",
DEFAULT_EXECUTION_CONTRACT,
undefined,
"Base Mainnet",
);
// Paymaster protection policy with ETH amount limit
const paymasterPolicy = buildPaymasterExecutionPolicy({
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
paymasterUserId: "8f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
restrictions: {
allowedEOAs: ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"],
allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
maxEthAmount: parseEther("0.1"), // Max 0.1 ETH per EOA transaction
maxGasPrice: parseGwei("50"), // Max 50 gwei gas price
maxGasLimit: 500000n, // Max 500k gas limit
},
policyName: "Paymaster Protection",
});
// Resulting policy uses ABI parsing for direct argument access:
// {
// organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
// policyName: "Paymaster Protection",
// effect: "EFFECT_ALLOW",
// consensus: "approvers.any(user, user.id == '8f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c')",
// condition: "activity.resource == 'PRIVATE_KEY' && " +
// "activity.action == 'SIGN' && " +
// "eth.tx.to == '0x00000000008c57a1ce37836a5e9d36759d070d8c' && " +
// "(eth.tx.contract_call_args['_to'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
// "(eth.tx.contract_call_args['_target'] == '0x742d35cc6634c0532925a3b844bc9e7595f0beb') && " +
// "eth.tx.contract_call_args['_ethAmount'] <= 100000000000000000 && " +
// "eth.tx.gasPrice <= 50000000000 && " +
// "eth.tx.gas <= 500000",
// notes: "Restricts which transactions the paymaster can execute on the gas station"
// }Note: The ensureGasStationInterface() function uploads the Gas Station ABI to Turnkey's Smart Contract Interface feature. This enables Turnkey's policy engine to parse the ABI-encoded transaction data and directly compare the _ethAmount parameter as a uint256 value, rather than raw bytes. The function checks if the ABI already exists before uploading to avoid duplicates.
Combine both policy types for maximum security:
import {
buildIntentSigningPolicy,
buildPaymasterExecutionPolicy,
DEFAULT_EXECUTION_CONTRACT,
} from "@turnkey/gas-station";
import { parseGwei } from "viem";
// Layer 1: EOA can only sign USDC intents
const eoaPolicy = buildIntentSigningPolicy({
organizationId: "a5b89e4f-1234-5678-9abc-def012345678",
eoaUserId: "3c7d6e8a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
restrictions: {
allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
disallowEthTransfer: true, // No ETH transfers
},
});
// Layer 2: Paymaster can only execute for specific users with gas limits
const paymasterPolicy = buildPaymasterExecutionPolicy({
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
paymasterUserId: "8f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
restrictions: {
allowedEOAs: ["0xUserAddress..."],
allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
maxGasPrice: parseGwei("50"),
maxGasLimit: 500000n,
},
});When using buildPaymasterExecutionPolicy, the SDK creates Turnkey policies that parse the transaction calldata to enforce restrictions. Understanding the transaction structure allows you to write custom policies for advanced use cases.
When the paymaster signs an execution transaction calling execute(address _target, address _to, uint256 _ethAmount, bytes _data), the transaction data (eth.tx.data) has the following structure:
| Position in eth.tx.data | Length | Content | Example |
|---|---|---|---|
[2..10] |
8 chars | Function selector | 6c5c2ed9 (execute) |
[10..74] |
64 chars | _target (EOA, padded) | 0000...742d35cc6634c0532925a3b844bc9e7595f0beb |
[74..138] |
64 chars | _to (output contract) | 0000...833589fcd6edb6e08f4c7c32d4f71b54bda02913 |
[138..202] |
64 chars | _ethAmount (uint256) | 0000...0000 (0 ETH) or amount in wei |
[202..266] |
64 chars | Offset to _data bytes | 0000...0080 (128 bytes) |
[266..330] |
64 chars | Packed data length | 0000...0055 (85 bytes: 65+16+4) |
[330..460] |
130 chars | Signature (65 bytes) | EIP-712 signature from EOA |
[460..492] |
32 chars | Nonce (16 bytes) | 00000000000000000000000000000000 |
[492..500] |
8 chars | Deadline (4 bytes) | 6ac7d340 (Unix timestamp) |
[500+] |
Variable | Call data | Encoded function call for target contract |
Important: Turnkey's eth.tx.data includes the 0x prefix, so positions start at index 2 (after 0x).
Note: The deadline is a Unix timestamp that prevents replay attacks by expiring signatures after a specified time. The SDK defaults to 1 hour, customizable with withDeadline().
Check execution contract address:
eth.tx.to == "0x00000000008c57a1ce37836a5e9d36759d070d8c";Check which EOA is executing:
eth.tx.data[10..74] == '0000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0beb'Check target contract (output contract):
eth.tx.data[74..138] == '0000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913'Check ETH amount:
eth.tx.data[138..202].hex_to_uint() <= 100000000000000000; // Max 0.1 ETH in weiCheck gas price:
eth.tx.gasPrice <= 50000000000; // 50 gwei in weiCheck gas limit:
eth.tx.gas <= 500000;Allow paymaster to execute for USDC or DAI only:
const policy = {
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
policyName: "Stablecoin Execution Policy",
effect: "EFFECT_ALLOW",
consensus: `approvers.any(user, user.id == '${paymasterUserId}')`,
condition: [
"activity.resource == 'PRIVATE_KEY'",
"activity.action == 'SIGN'",
"eth.tx.to == '0x00000000008c57a1ce37836a5e9d36759d070d8c'",
// Allow USDC or DAI
"(eth.tx.data[74..138] == '0000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913' || eth.tx.data[74..138] == '00000000000000000000006b175474e89094c44da98b954eedeac495271d0f')",
// Gas limits
"eth.tx.gasPrice <= 100000000000",
"eth.tx.gas <= 500000",
].join(" && "),
notes: "Allow USDC and DAI execution with gas limits",
};
await turnkeyClient.apiClient().createPolicy(policy);Only allow execution for approved user wallets:
const approvedEOAs = [
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"0x1234567890123456789012345678901234567890",
];
const eoaConditions = approvedEOAs
.map((addr) => {
const padded = addr.slice(2).toLowerCase().padStart(64, "0");
return `eth.tx.data[10..74] == '${padded}'`;
})
.join(" || ");
const policy = {
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
policyName: "Approved Users Only",
effect: "EFFECT_ALLOW",
consensus: `approvers.any(user, user.id == '${paymasterUserId}')`,
condition: [
"activity.resource == 'PRIVATE_KEY'",
"activity.action == 'SIGN'",
"eth.tx.to == '0x00000000008c57a1ce37836a5e9d36759d070d8c'",
`(${eoaConditions})`,
].join(" && "),
};
await turnkeyClient.apiClient().createPolicy(policy);For most cases, use the built-in helpers which handle the byte positions correctly:
import {
buildPaymasterExecutionPolicy,
DEFAULT_EXECUTION_CONTRACT,
} from "@turnkey/gas-station";
import { parseGwei } from "viem";
const policy = buildPaymasterExecutionPolicy({
organizationId: subOrgId,
paymasterUserId: paymasterUserId,
executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
restrictions: {
allowedContracts: [
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", // DAI
],
allowedEOAs: ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"],
maxGasPrice: parseGwei("100"),
maxGasLimit: 500000n,
},
policyName: "Production Paymaster Policy",
});
await turnkeyClient.apiClient().createPolicy(policy);The helper functions automatically:
- Convert addresses to lowercase
- Add proper padding for EOA addresses
- Calculate correct byte positions (accounting for
0xprefix) - Generate proper OR conditions for multiple allowed values
- Ensure paymaster has ETH for gas
- Verify delegate contract address is correct
- Confirm EOA is authorized (check with
isAuthorized()) - Verify execution contract address matches deployment
- Check nonce hasn't been reused
- Ensure target contract call is valid
- EOA must have sufficient token balance for transfers
- Paymaster must have ETH for gas
- Verify EOA address is correct
- Ensure chain ID matches the network
- Check intent was signed with correct nonce
- Client Separation: Create separate client instances for users and paymasters
- Authorization: Only call
authorize()once per EOA - Nonce Management: Always fetch fresh nonce before creating intents
- Rate Limiting: Implement paymaster rate limits to prevent abuse
See the main SDK repository for license information.