Add gas sponsorship and smart wallet features to your Privy app in under 5 minutes. Works with React Web and React Native (Expo).
If you're already using Privy for authentication, this package lets you upgrade your users' wallets with:
- 🔄 EIP-7702 Delegation - Upgrade your wallets to smart accounts without migration
- ⛽ Gas Sponsorship - Pay gas fees for your users via Alchemy Gas Manager (EVM & Solana)
- 💱 Token Swaps - Execute swaps through Alchemy's swap infrastructure
- 🚀 Batched Transactions - Send multiple operations in a single transaction using
sendTransaction([...]) - ☀️ Solana Support - Send sponsored Solana transactions with Privy's embedded Solana wallets (Web only)
- 📱 React Native Support - Full EVM support for React Native apps using
@privy-io/expo
All while keeping Privy as your authentication provider. No need to change your auth flow or migrate user accounts.
Already using Privy? Add smart account features without changing your existing setup:
- Drop-in React hooks that replace Privy's transaction hooks
- Automatic EIP-7702 delegation to upgrade wallets on-the-fly
- Route transactions through Alchemy's infrastructure for sponsorship and reliability
npm install @account-kit/privy-integration
# or
yarn add @account-kit/privy-integration
# or
pnpm add @account-kit/privy-integrationnpm install @account-kit/privy-integration @privy-io/expo
# or
yarn add @account-kit/privy-integration @privy-io/expo
# or
pnpm add @account-kit/privy-integration @privy-io/expoTo use Solana features (like useAlchemySolanaTransaction), you'll need to install the Solana Web3.js library:
npm install @solana/web3.js
# or
yarn add @solana/web3.js
# or
pnpm add @solana/web3.jsThen import from the /solana export:
import { useAlchemySolanaTransaction } from "@account-kit/privy-integration/solana";Note: The Solana functionality is completely optional and currently only available for React web. If you only need EVM features, you don't need to install
@solana/web3.js.
Choose your platform:
React Web
Important: AlchemyProvider must be nested inside PrivyProvider to access authentication state.
import { PrivyProvider } from "@privy-io/react-auth";
import { AlchemyProvider } from "@account-kit/privy-integration";
function App() {
return (
<PrivyProvider
appId="your-privy-app-id"
config={
{
/* your privy config */
}
}
>
<AlchemyProvider
apiKey="your-alchemy-api-key"
policyId="your-gas-policy-id" // optional, for gas sponsorship
>
<YourApp />
</AlchemyProvider>
</PrivyProvider>
);
}import { useAlchemySendTransaction } from "@account-kit/privy-integration";
function SendButton() {
const { sendTransaction, isLoading, error, data } =
useAlchemySendTransaction();
const handleSend = async () => {
try {
// Single transaction
const result = await sendTransaction({
to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
data: "0x...",
value: "0xde0b6b3a7640000", // 1 ETH in hex (also accepts decimal string or bigint)
});
console.log("Transaction hash:", result.txnHash);
} catch (err) {
console.error("Transaction failed:", err);
}
};
const handleBatch = async () => {
try {
// Batch transactions
const result = await sendTransaction([
{ to: "0x...", data: "0x...", value: "0xde0b6b3a7640000" }, // 1 ETH
{ to: "0x...", data: "0x..." },
{ to: "0x...", data: "0x..." },
]);
console.log("Batch transaction hash:", result.txnHash);
} catch (err) {
console.error("Batch transaction failed:", err);
}
};
return (
<>
<button onClick={handleSend} disabled={isLoading}>
{isLoading ? "Sending..." : "Send Transaction"}
</button>
<button onClick={handleBatch} disabled={isLoading}>
{isLoading ? "Sending..." : "Send Batch"}
</button>
</>
);
}import {
useAlchemyPrepareSwap,
useAlchemySubmitSwap,
} from "@account-kit/privy-integration";
function SwapButton() {
const { prepareSwap } = useAlchemyPrepareSwap();
const { submitSwap, isLoading } = useAlchemySubmitSwap();
const handleSwap = async () => {
try {
// Step 1: Get quote and prepare swap
// Two modes available:
// Option A: Specify exact amount to swap FROM
const preparedSwap = await prepareSwap({
fromToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH
toToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
fromAmount: "0xde0b6b3a7640000", // Swap exactly 1 ETH
});
// Option B: Specify minimum amount to receive TO
/* const preparedSwap = await prepareSwap({
fromToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH
toToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
minimumToAmount: "0x5f5e100", // Receive at least 100 USDC (6 decimals)
}); */
console.log(
"Quote expiry:",
new Date(parseInt(preparedSwap.quote.expiry) * 1000),
);
// Step 2: Execute swap
const result = await submitSwap(preparedSwap);
console.log("Swap confirmed:", result.txnHash);
} catch (err) {
console.error("Swap failed:", err);
}
};
return (
<button onClick={handleSwap} disabled={isLoading}>
{isLoading ? "Swapping..." : "Swap Tokens"}
</button>
);
}import { useAlchemySolanaTransaction } from "@account-kit/privy-integration/solana";
function SolanaSendButton() {
const { sendTransactionAsync, isPending, error, data } =
useAlchemySolanaTransaction({
rpcUrl: "https://solana-mainnet.g.alchemy.com/v2/your-api-key",
policyId: "your-solana-policy-id", // optional, for gas sponsorship
});
const handleTransfer = async () => {
try {
// Simple SOL transfer
const result = await sendTransactionAsync({
transfer: {
amount: 1_000_000_000, // 1 SOL in lamports
toAddress: "recipient-base58-address",
},
});
console.log("Transaction hash:", result.hash);
} catch (err) {
console.error("Transaction failed:", err);
}
};
const handleCustomInstructions = async () => {
try {
// Custom instructions
import { SystemProgram, PublicKey } from "@solana/web3.js";
const instruction = SystemProgram.transfer({
fromPubkey: new PublicKey(walletAddress),
toPubkey: new PublicKey(recipientAddress),
lamports: 1_000_000,
});
const result = await sendTransactionAsync({
instructions: [instruction],
});
console.log("Transaction hash:", result.hash);
} catch (err) {
console.error("Transaction failed:", err);
}
};
return (
<>
<button onClick={handleTransfer} disabled={isPending}>
{isPending ? "Sending..." : "Send SOL"}
</button>
<button onClick={handleCustomInstructions} disabled={isPending}>
{isPending ? "Sending..." : "Custom Instructions"}
</button>
</>
);
}React Native (Expo)
Important: Use @privy-io/expo and import from /react-native for React Native apps.
import { PrivyProvider } from "@privy-io/expo";
import { AlchemyProvider } from "@account-kit/privy-integration/react-native";
function App() {
return (
<PrivyProvider appId="your-privy-app-id" clientId="your-privy-client-id">
<AlchemyProvider
apiKey="your-alchemy-api-key"
policyId="your-gas-policy-id" // optional, for gas sponsorship
>
<YourApp />
</AlchemyProvider>
</PrivyProvider>
);
}import { useAlchemySendTransaction } from "@account-kit/privy-integration/react-native";
import { Button } from "react-native";
function SendButton() {
const { sendTransaction, isLoading, error, data } =
useAlchemySendTransaction();
const handleSend = async () => {
try {
// Single transaction
const result = await sendTransaction({
to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
data: "0x...",
value: "0xde0b6b3a7640000", // 1 ETH in hex (also accepts decimal string or bigint)
});
console.log("Transaction hash:", result.txnHash);
} catch (err) {
console.error("Transaction failed:", err);
}
};
const handleBatch = async () => {
try {
// Batch transactions
const result = await sendTransaction([
{ to: "0x...", data: "0x...", value: "0xde0b6b3a7640000" }, // 1 ETH
{ to: "0x...", data: "0x..." },
{ to: "0x...", data: "0x..." },
]);
console.log("Batch transaction hash:", result.txnHash);
} catch (err) {
console.error("Batch transaction failed:", err);
}
};
return (
<>
<Button
onPress={handleSend}
disabled={isLoading}
title={isLoading ? "Sending..." : "Send Transaction"}
/>
<Button
onPress={handleBatch}
disabled={isLoading}
title={isLoading ? "Sending..." : "Send Batch"}
/>
</>
);
}import {
useAlchemyPrepareSwap,
useAlchemySubmitSwap,
} from "@account-kit/privy-integration/react-native";
import { Button } from "react-native";
function SwapButton() {
const { prepareSwap } = useAlchemyPrepareSwap();
const { submitSwap, isLoading } = useAlchemySubmitSwap();
const handleSwap = async () => {
try {
// Prepare swap
const preparedSwap = await prepareSwap({
fromToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH
toToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
fromAmount: "0xde0b6b3a7640000", // Swap exactly 1 ETH
});
// Execute swap
const result = await submitSwap(preparedSwap);
console.log("Swap confirmed:", result.txnHash);
} catch (err) {
console.error("Swap failed:", err);
}
};
return (
<Button
onPress={handleSwap}
disabled={isLoading}
title={isLoading ? "Swapping..." : "Swap Tokens"}
/>
);
}If your users have multiple Privy wallets, specify which one to use:
<AlchemyProvider
apiKey="your-alchemy-api-key"
policyId="your-gas-policy-id"
walletAddress="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
>
<YourApp />
</AlchemyProvider>| Prop | Type | Required | Description |
|---|---|---|---|
apiKey |
string |
Conditional* | Your Alchemy API key for @account-kit/infra transport |
jwt |
string |
Conditional* | JWT token for authentication (alternative to apiKey) |
rpcUrl |
string |
Conditional* | Custom RPC URL for EVM chains (can be used alone or with jwt) |
solanaRpcUrl |
string |
No | Custom RPC URL for Solana (separate from EVM rpcUrl) |
policyId |
string | string[] |
No | Gas Manager policy ID(s) for EVM sponsorship. If array is provided, backend uses first applicable policy |
solanaPolicyId |
string | string[] |
No | Gas Manager policy ID(s) for Solana sponsorship |
disableSponsorship |
boolean |
No | Set to true to disable gas sponsorship by default (default: false) |
accountAuthMode |
'eip7702' | 'owner' |
No | Authorization mode for EVM smart accounts (default: 'eip7702') |
walletAddress |
string |
No | Specific wallet address to use if user has multiple wallets (defaults to first wallet) |
* Required configuration (pick one):
apiKeyalonejwtalonerpcUrlalonerpcUrl+jwttogether
Control sponsorship per transaction:
// Sponsored transaction (default if policyId is set and disableSponsorship is not true)
await sendTransaction({ to: "0x...", data: "0x..." });
// Disable sponsorship for this specific transaction
await sendTransaction(
{ to: "0x...", data: "0x..." },
{ disableSponsorship: true },
);Send single or batch EVM transactions with optional gas sponsorship.
Returns:
sendTransaction(input, options?)- Send a single transaction or batch of transactionsinput- SingleUnsignedTransactionRequestor array of themoptions- OptionalSendTransactionOptions
isLoading- Loading stateerror- Error object if faileddata- Transaction result withtxnHashreset()- Reset hook state
Request swap quotes and prepare swap calls.
Returns:
prepareSwap(request)- Get quote and prepare swap (returns full response withquoteand call data)isLoading- Loading stateerror- Error object if faileddata- Prepared swap resultreset()- Reset hook state
Sign and submit prepared swap calls.
Returns:
submitSwap(preparedSwap)- Execute prepared swap (accepts result fromprepareSwap)isLoading- Loading stateerror- Error object if faileddata- Swap result withtxnHashreset()- Reset hook state
Send Solana transactions with optional gas sponsorship via Alchemy.
Parameters:
options.rpcUrl- Solana RPC URL (overrides provider config)options.policyId- Gas sponsorship policy ID (overrides provider config)options.walletAddress- Specific wallet address to use (defaults to first wallet)options.confirmationOptions- Transaction confirmation options
Returns:
sendTransactionAsync(params)- Send transaction and await result (throws on error)params.transfer- Simple SOL transfer withamount(lamports) andtoAddressparams.instructions- Custom Solana transaction instructions array
sendTransaction(params)- Send transaction (fire-and-forget, errors caught internally)connection- Active Solana connection instanceisPending- Whether a transaction is currently being senterror- Error object if faileddata- Transaction result withhash(base58 signature)reset()- Reset hook state
Get the underlying smart wallet client and account (advanced use cases).
Returns:
getClient()- Async function that returns{ client: SmartWalletClient, account: SmartContractAccount }client- The smart wallet client instanceaccount- The smart account with address and other account info
This package uses EIP-7702 to upgrade your users' Privy wallets into smart accounts without requiring them to deploy new contracts or migrate funds.
When a user sends their first transaction:
- Their wallet signs an EIP-7702 authorization
- The authorization delegates to Alchemy's smart account implementation
- The transaction is executed with smart account features (batching, sponsorship, etc.)
- Gas is optionally sponsored by your Gas Manager policy
Under the hood, this package:
- Connects to your user's Privy embedded wallet
- Wraps it with
WalletClientSignerfrom@aa-sdk/core - Creates a
SmartWalletClientwith EIP-7702 support (default) or traditional smart account support - Routes transactions through Alchemy infrastructure
- Automatically handles sponsorship via Gas Manager policies
The package supports two authorization modes via the accountAuthMode prop:
'eip7702'(default, recommended): Uses EIP-7702 to delegate the Privy wallet to a smart account. No separate deployment needed, funds stay at the wallet address. This is the recommended mode for most applications.'owner': Uses a traditional smart account with the Privy wallet as the owner/signer. The smart account has a separate address from the owner wallet. Use this if you need compatibility with environments that don't support EIP-7702 yet.
// Default behavior (EIP-7702)
<AlchemyProvider apiKey="...">
<YourApp />
</AlchemyProvider>
// Traditional smart account mode
<AlchemyProvider apiKey="..." accountAuthMode="owner">
<YourApp />
</AlchemyProvider>Getting the Smart Account Address:
When using owner mode, the smart account has a different address from your Privy signer. Access it via useAlchemyClient:
import { useAlchemyClient } from "@account-kit/privy-integration";
function MyComponent() {
const { getClient } = useAlchemyClient();
const getSmartAccountAddress = async () => {
const { account } = await getClient();
console.log("Smart account address:", account.address);
// This is different from the Privy signer address in owner mode
};
}- Go to Alchemy Dashboard
- Create or select an app
- Copy your API key
- Go to Gas Manager
- Create a new policy with your desired rules
- Copy the policy ID
- Go to Privy Dashboard
- Create or select an app
- Copy your app ID
If you're currently using Privy's useSendTransaction hook:
import { useSendTransaction } from "@privy-io/react-auth";
const { sendTransaction } = useSendTransaction({
onSuccess: (txHash) => console.log(txHash),
});import { useAlchemySendTransaction } from "@account-kit/privy-integration";
const { sendTransaction, data } = useAlchemySendTransaction();
// Now with gas sponsorship!The API is nearly identical, making migration seamless.
For advanced use cases, access the underlying client and account directly:
import { useAlchemyClient } from "@account-kit/privy-integration";
function AdvancedComponent() {
const { getClient } = useAlchemyClient();
const doAdvancedOperation = async () => {
const { client, account } = await getClient();
// Access the smart account address
console.log("Smart account address:", account.address);
// Direct access to sendCalls with full control
await client.sendCalls({
from: account.address,
calls: [
{ to: "0x...", data: "0x..." },
{ to: "0x...", data: "0x..." },
],
capabilities: {
eip7702Auth: true, // Set to true for EIP-7702 mode
paymasterService: { policyId: "your-policy-id" },
},
});
// Note: For most cases, use useAlchemySendTransaction instead
};
return <button onClick={doAdvancedOperation}>Advanced Op</button>;
}The provider requires exactly one valid transport configuration. Valid combinations:
apiKeyonlyjwtonlyrpcUrlonlyrpcUrl+jwttogether
Invalid combinations like apiKey + jwt will now show TypeScript errors.
The swap API should return prepared calls by default. This error means the API returned raw calls. Ensure you're not setting returnRawCalls: true in the request.
Check out the examples/ directory for complete applications:
- Privy Integration Demo - Demos sponsored transactions and sponsored swaps
MIT