Avalanche Quick Start (ERC-4337)
Add gasless UserOperation flows on Avalanche through SmoothSend gateway.
1Global Configuration
import { SmoothSendAvaxProvider } from '@smoothsend/sdk/avax'; <SmoothSendAvaxProvider apiKey="pk_nogas_..." network="testnet" // Optional: defaults to testnet mode="developer-sponsored" // Optional: defaults to developer-sponsored> <YourApp /></SmoothSendAvaxProvider>Add this provider in your app root (for example main.tsx, WalletProvider.tsx, or your top-level layout/providers file).
Default mode is developer-sponsored, so users pay 0 native gas unless you switch to user-pays-erc20.
2The Hook Overhaul
Standard Wagmi
import { useWriteContract } from 'wagmi';SmoothSend Gasless
import { useSmoothSendWrite as useWriteContract } from '@smoothsend/sdk/avax';* Every writeContract() call in your file is now automatically sponsored by SmoothSend.
In wagmi apps, this is the only component-level change needed: import { useSmoothSendWrite as useWriteContract } from '@smoothsend/sdk/avax'.
| Mode | Gas payer | User pays |
|---|---|---|
| developer-sponsored | SmoothSend paymaster | Nothing |
| user-pays-erc20 | SmoothSend paymaster | ERC20 (for launch: USDC) |
msg.sender = smart account), not the EOA. If your contract uses owner checks tied to an EOA, keep those admin actions on direct wagmi writes (EOA path) or set ownership/admin roles to the smart account.import { createSmoothSendAvaxClient } from '@smoothsend/sdk/avax';Dependency setup lives in AVAX Installation.
1import { createSmoothSendAvaxClient } from '@smoothsend/sdk/avax';2 3const avax = createSmoothSendAvaxClient({4 apiKey: process.env.SMOOTHSEND_API_KEY!, // sk_nogas_* on backend5 network: 'testnet', // fuji6 publicClient, // viem public client7 walletClient, // viem wallet client8});9 10const result = await avax.submitCall({11 call: {12 to: '0xYourTargetContract',13 data: '0xYourEncodedCalldata',14 value: 0n,15 },16 mode: 'developer-sponsored', // or 'user-pays-erc20'17 // optional: override smart account if not provided in constructor18 // smartAccountAddress: '0xYourSmartAccount',19 // optional: override factory or provide owner for counterfactuals20 // accountFactory: '0xYourFactoryAddress',21 waitForReceipt: false,22});23 24console.log(result.userOpHash);1const quote = await avax.estimateUserPaysFee({2 calls: [{3 to: '0xYourTargetContract',4 data: '0xYourEncodedCalldata',5 value: 0n,6 }],7 paymaster: { token: '0xUSDC' },8 // optional: specify which smart account will pay the fee9 // smartAccountAddress: '0xYourSmartAccount',10});11 12console.log(quote.feePreview); // token + amount + usd + policy fieldsuserOpHash: returned immediately by eth_sendUserOperation.
transactionHash: available after inclusion from eth_getUserOperationReceipt. Use this for explorer links.
1import { useSmoothSendWrite } from '@smoothsend/sdk/avax';2 3// Your existing ABI and Contract Address4const YOUR_ABI = [...] as const;5const YOUR_CONTRACT_ADDRESS = '0x...';6 7function GaslessButton() {8 // 1. Swap wagmi's useWriteContract for this!9 // data here is a UserOperation hash (not an EVM tx hash)10 const { writeContract, data: userOpHash, isPending: isWriting } = useSmoothSendWrite();11 const isConfirming = false; // optional: track via eth_getUserOperationReceipt12 13 return (14 <button15 disabled={isWriting || isConfirming}16 onClick={() =>17 writeContract({18 address: YOUR_CONTRACT_ADDRESS,19 abi: YOUR_ABI,20 functionName: 'mint', // Your function name21 args: [1], // Your arguments22 mode: 'developer-sponsored',23 })24 }25 >26 {isWriting ? 'Sponsoring...' : isConfirming ? 'Confirming...' : 'Submit Gasless'}27 </button>28 );29}Best Practices: The 3 UI States
Idle: Button is active. User is ready to start.
Writing (Sponsoring):The SDK is generating the UserOperation and requesting sponsorship. Disable the button here to prevent duplicate submissions.
Confirming:The UserOperation is sent. Poll eth_getUserOperationReceipt (or SDK receipt helpers) and keep the button disabled until confirmation.
Pro Tip: If you need to target a different network for a specific action, you can pass network directly to the hook or the writeContract call.
1import { SmoothSendAvaxProvider, useSmoothSendAvax } from '@smoothsend/sdk/avax';2import { usePublicClient, useWalletClient } from 'wagmi';3 4function TransferButton({ to, data }: { to: `0x${string}`; data: `0x${string}` }) {5 const publicClient = usePublicClient();6 const { data: walletClient } = useWalletClient();7 const { submitCall } = useSmoothSendAvax({8 publicClient,9 walletClient: walletClient ?? undefined,10 });11 12 return (13 <button14 onClick={() =>15 submitCall({16 to,17 data,18 mode: 'developer-sponsored',19 })20 }21 >22 Submit gasless call23 </button>24 );25}26 27export function Root({ children }: { children: React.ReactNode }) {28 return (29 <SmoothSendAvaxProvider30 apiKey={process.env.NEXT_PUBLIC_SMOOTHSEND_API_KEY!} // pk_nogas_* in browser31 network="testnet"32 smartAccountAddress={'0xYourSmartAccount' as `0x${string}`}33 >34 {children}35 </SmoothSendAvaxProvider>36 );37}import { PrivyProvider } from '@privy-io/react-auth';import { avalancheFuji } from 'viem/chains';import { SmoothSendAvaxProvider } from '@smoothsend/sdk/avax'; <PrivyProvider appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!} config={{ embeddedWallets: { ethereum: { createOnLogin: 'users-without-wallets' } }, defaultChain: avalancheFuji, supportedChains: [avalancheFuji], }}> <SmoothSendAvaxProvider apiKey={process.env.NEXT_PUBLIC_SMOOTHSEND_API_KEY!} network="testnet" > <YourApp /> </SmoothSendAvaxProvider></PrivyProvider>Full minimal integration (with one hook call) lives in AVAX Privy Integration.
submitSponsoredUserOp with a calls array to execute multiple operations in a single UserOperation — one signature, one on-chain transaction. In user-pays-erc20 mode, batch the ERC20 approval and the transfer together so no separate approval step is required.1import { useSmoothSendAvax, fetchAvaxAaPublicDefaults } from '@smoothsend/sdk/avax';2import { encodeFunctionData, parseUnits } from 'viem';3import { usePublicClient, useWalletClient } from 'wagmi';4 5const ERC20_ABI = [6 {7 name: 'approve', type: 'function', stateMutability: 'nonpayable',8 inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }],9 outputs: [{ type: 'bool' }],10 },11 {12 name: 'transfer', type: 'function', stateMutability: 'nonpayable',13 inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],14 outputs: [{ type: 'bool' }],15 },16] as const;17 18const USDC = '0x5425890298aed601595a70AB815c96711a31Bc65'; // Fuji testnet USDC19 20function USDCTransferButton({ recipient }: { recipient: `0x${string}` }) {21 const publicClient = usePublicClient();22 const { data: walletClient } = useWalletClient();23 const { submitSponsoredUserOp } = useSmoothSendAvax({24 publicClient,25 walletClient: walletClient ?? undefined,26 });27 28 const handleTransfer = async () => {29 // 1. Fetch paymaster address dynamically from SmoothSend30 const { paymasterFuji } = await fetchAvaxAaPublicDefaults();31 if (!paymasterFuji) throw new Error('Paymaster not found');32 33 const amount = parseUnits('1', 6); // 1 USDC (6 decimals)34 35 // [approve paymaster, transfer to recipient] — one signature, atomic on-chain36 await submitSponsoredUserOp({37 calls: [38 {39 to: USDC,40 data: encodeFunctionData({41 abi: ERC20_ABI,42 functionName: 'approve',43 args: [paymasterFuji, parseUnits('1000', 6)],44 }),45 value: 0n,46 },47 {48 to: USDC,49 data: encodeFunctionData({50 abi: ERC20_ABI,51 functionName: 'transfer',52 args: [recipient, amount],53 }),54 value: 0n,55 },56 ],57 sponsorshipMode: 'user-pays-erc20',58 paymaster: { token: USDC },59 });60 };61 62 return <button onClick={handleTransfer}>Send 1 USDC (gasless)</button>;63}The SDK automatically encodes the batch via the smart account's executeBatch(dest[], value[], func[]) function. By using fetchAvaxAaPublicDefaults(), you ensure your app always uses the correct Paymaster address for approvals without hardcoding it.