Turnkey provides institutional-grade key management for your applications. This guide shows you how to combine Turnkey’s secure signing with Biconomy’s MEE using the AbstractJS SDK for gasless, cross-chain transactions.
What We’re Building
By the end of this tutorial, you’ll have a backend service that can:
- Securely manage private keys with Turnkey
- Execute transactions without holding ETH for gas
- Perform operations across multiple chains
How it works: Turnkey stores and manages private keys in secure enclaves. When you need to sign a transaction, Turnkey signs it without ever exposing the private key. We then route the signed transaction through Biconomy’s MEE for gas abstraction.
Step 1: Install Dependencies
npm install @turnkey/sdk-server @turnkey/viem @biconomy/abstractjs viem dotenv
What these packages do:
@turnkey/sdk-server — Turnkey’s server SDK for key management
@turnkey/viem — Adapter to use Turnkey with viem
@biconomy/abstractjs — Biconomy’s SDK for smart accounts and MEE
viem — Ethereum library for building transactions
Step 2: Set Up Environment Variables
Create a .env file with your Turnkey credentials:
# Turnkey API endpoint
BASE_URL=https://api.turnkey.com
# Your API keys from Turnkey dashboard
API_PRIVATE_KEY=your_api_private_key
API_PUBLIC_KEY=your_api_public_key
# Your organization ID
ORGANIZATION_ID=your_organization_id
# The private key ID to sign with
SIGN_WITH=your_private_key_id
Never commit API keys to version control. Use environment variables or a secrets manager.
Step 3: Initialize Turnkey Client
Set up the Turnkey client that will handle all signing operations:
import { Turnkey } from "@turnkey/sdk-server";
import * as dotenv from "dotenv";
dotenv.config();
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,
});
Step 4: Create a Wallet Client
Create a viem wallet client using the Turnkey account:
import { createWalletClient, http } from "viem";
import { createAccount } from "@turnkey/viem";
import { optimism } from "viem/chains";
// Create a Turnkey account adapter
const account = await createAccount({
client: turnkeyClient.apiClient(),
organizationId: process.env.ORGANIZATION_ID,
signWith: process.env.SIGN_WITH,
});
// Create the wallet client for signing
const walletClient = createWalletClient({
account,
chain: optimism,
transport: http(),
});
What’s happening here? The createAccount function creates an adapter that lets viem use Turnkey for signing. The actual private key never leaves Turnkey’s secure infrastructure.
Step 5: Sign EIP-7702 Authorization
Sign the authorization that upgrades the EOA to a smart account:
// Biconomy Nexus smart account implementation
const NEXUS_IMPLEMENTATION = "0x000000004F43C49e93C970E84001853a70923B03";
// Sign authorization to delegate to Nexus
const authorization = await walletClient.signAuthorization({
contractAddress: NEXUS_IMPLEMENTATION,
account,
});
What is this authorization? It’s a signed message saying “I authorize my wallet address to execute code from this smart contract.” This enables smart account features while keeping the same address.
Step 6: Create the Smart Account
Create a Nexus account that works across multiple chains:
import {
toMultichainNexusAccount,
getMEEVersion,
MEEVersion
} from "@biconomy/abstractjs";
import { optimism, base } from "viem/chains";
const nexusAccount = await toMultichainNexusAccount({
chainConfigurations: [
{
chain: optimism,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
// Use EOA address for EIP-7702 mode
accountAddress: account.address,
},
{
chain: base,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
accountAddress: account.address,
},
],
signer: walletClient,
});
Step 7: Create the MEE Client
The MEE client handles transaction execution with gas abstraction:
import { createMeeClient } from "@biconomy/abstractjs";
const meeClient = await createMeeClient({
account: nexusAccount,
});
Step 8: Execute a Gasless Transaction
Now execute a transaction with gas paid in any ERC-20:
const USDC_OPTIMISM = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85";
const { hash } = await meeClient.execute({
// Required for EIP-7702 flow
authorization,
delegate: true,
// Pay gas with USDC
feeToken: {
address: USDC_OPTIMISM,
chainId: optimism.id,
},
// Your transaction instructions
instructions: [
{
chainId: optimism.id,
calls: [
{
to: "0xRecipientAddress",
value: 0n,
},
],
},
],
});
console.log("Transaction submitted:", hash);
Step 9: Wait for Confirmation
const receipt = await meeClient.waitForSupertransactionReceipt({ hash });
console.log("Transaction confirmed:", receipt.hash);
Complete Example
Here’s a full Node.js script:
import { Turnkey } from "@turnkey/sdk-server";
import { createAccount } from "@turnkey/viem";
import { createWalletClient, http } from "viem";
import { optimism, base } from "viem/chains";
import {
createMeeClient,
toMultichainNexusAccount,
getMEEVersion,
MEEVersion,
} from "@biconomy/abstractjs";
import * as dotenv from "dotenv";
dotenv.config();
const NEXUS_IMPLEMENTATION = "0x000000004F43C49e93C970E84001853a70923B03";
const USDC_OPTIMISM = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85";
async function main() {
// 1. Initialize Turnkey
const turnkey = new Turnkey({
apiBaseUrl: process.env.BASE_URL!,
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
});
// 2. Create Turnkey account adapter
const account = await createAccount({
client: turnkey.apiClient(),
organizationId: process.env.ORGANIZATION_ID!,
signWith: process.env.SIGN_WITH!,
});
console.log("Using address:", account.address);
// 3. Create wallet client
const walletClient = createWalletClient({
account,
chain: optimism,
transport: http(),
});
// 4. Sign EIP-7702 authorization
const authorization = await walletClient.signAuthorization({
contractAddress: NEXUS_IMPLEMENTATION,
account,
});
console.log("Authorization signed");
// 5. Create multichain Nexus account
const nexusAccount = await toMultichainNexusAccount({
chainConfigurations: [
{
chain: optimism,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
accountAddress: account.address,
},
{
chain: base,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
accountAddress: account.address,
},
],
signer: walletClient,
});
// 6. Create MEE client
const meeClient = await createMeeClient({ account: nexusAccount });
// 7. Execute gasless transaction
const { hash } = await meeClient.execute({
authorization,
delegate: true,
feeToken: {
address: USDC_OPTIMISM,
chainId: optimism.id,
},
instructions: [
{
chainId: optimism.id,
calls: [
{
to: "0x0000000000000000000000000000000000000000",
value: 0n,
},
],
},
],
});
console.log("Transaction submitted:", hash);
// 8. Wait for confirmation
const receipt = await meeClient.waitForSupertransactionReceipt({ hash });
console.log("Transaction confirmed:", receipt.hash);
}
main().catch(console.error);
Cross-Chain Execution
Execute on multiple chains with one signature:
const { hash } = await meeClient.execute({
authorization,
delegate: true,
instructions: [
{
chainId: optimism.id,
calls: [{ to: "0x...", value: 0n }],
},
{
chainId: base.id,
calls: [{ to: "0x...", value: 0n }],
},
],
});
The MEE infrastructure handles routing transactions to each chain and managing execution order.
Key Takeaways
- Turnkey manages keys securely — Private keys never leave Turnkey’s secure infrastructure
- EIP-7702 adds smart features — The wallet gets upgraded without deploying a new contract
- MEE handles gas — Pay with any ERC-20 token or sponsor gas entirely
- Cross-chain ready — Add chains to
chainConfigurations and include them in instructions
Next Steps