Batua.sh is an easy-to-integrate, embedded smart-account secured by passkeys. It offers built-in support for sponsoring transactions, batching multiple calls, and gives you complete ownership of the code. Thanks to shadcn/ui, it embeds seamlessly into your app’s theme, and it works out of the box with Wagmi, Viem, Ethers, Privy, Dynamic, and more.
In this tutorial, you’ll scaffold a Next.js 13 TypeScript app, install and configure Batua’s smart-account SDK alongside Wagmi & RainbowKit, and build a simple interface to connect your passkey-secured wallet and send batch ERC-20 transactions.
You’ll start by scaffolding a new Next.js 13 TypeScript project.
npx create-next-app@latest batua-demo --typescript
cd batua-demo
Install React-Query, Wagmi, RainbowKit, and utilities:
npm install wagmi viem @tanstack/react-query @rainbow-me/rainbowkit @radix-ui/react-accordion @radix-ui/react-dialog @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-tooltip class-variance-authority clsx lucide-react zod zustand
These packages power wallet connections, on-chain RPC calls, state management, and UI primitives.
Run the installation helper to pull in Batua’s SDK hooks and UI:
npx shadcn@latest add https://batua.sh/install
This command installs:
@batua/react
hooks for smart-account creation & batching
shadcn/ui primitives for consistent styling
You need to wrap your application in Wagmi, React-Query, and RainbowKit providers. Create or replace app/layout.tsx
with:
"use client" ;
import { Geist, Geist_Mono } from "next/font/google" ;
import "./globals.css" ;
import { QueryClient, QueryClientProvider } from "@tanstack/react-query" ;
import { WagmiProvider } from "wagmi" ;
import { RainbowKitProvider } from "@rainbow-me/rainbowkit" ;
import { config } from "@/lib/utils/wagmiConfig" ;
import "@rainbow-me/rainbowkit/styles.css" ;
const queryClient = new QueryClient ();
const geistSans = Geist ({ variable: "--font-geist-sans" , subsets: [ "latin" ] });
const geistMono = Geist_Mono ({ variable: "--font-geist-mono" , subsets: [ "latin" ] });
export default function RootLayout ({ children } : { children : React . ReactNode }) {
return (
< html lang = "en" >
< body className = { `${ geistSans . variable } ${ geistMono . variable } antialiased` }>
< WagmiProvider config = {config}>
< QueryClientProvider client = {queryClient}>
< RainbowKitProvider >{children}</ RainbowKitProvider >
</ QueryClientProvider >
</ WagmiProvider >
</ body >
</ html >
);
}
WagmiProvider : manages blockchain connections
QueryClientProvider : handles server/state caching
RainbowKitProvider : adds wallet UI components
Create lib/utils/wagmiConfig.ts
to define your wallet & RPC settings:
"use client" ;
import { createConfig, http } from "wagmi" ;
import { injected } from "wagmi/connectors" ;
import { sepolia } from "wagmi/chains" ;
export const config = createConfig ({
chains: [sepolia],
connectors: [ injected ()],
transports: {
[sepolia.id]: http ( "https://1rpc.io/sepolia" ),
},
});
Batua only supports Sepolia at the moment.
injected()
lets users connect any injected wallet including the Batua Smart Account Wallet (MetaMask, etc.).
RPC transport points at a public Sepolia endpoint.
Create lib/utils/batuaClient.ts
to bootstrap the Batua smart-account client:
"use client" ;
import { Batua } from "@/lib/batua" ;
import { http } from "viem" ;
import { sepolia } from "viem/chains" ;
const PIMLICO_KEY = process.env. NEXT_PUBLIC_PIMLICO_API_KEY ! ;
export function setupBatua () {
{
const res = Batua. create ({
rpc: {
transports: {
[sepolia.id]: http ( "https://1rpc.io/sepolia" ),
},
},
paymaster: {
transports: {
[sepolia.id]: http (
`https://api.pimlico.io/v2/${ sepolia . id }/rpc?apikey=${ pimlicoApiKey }`
),
},
},
bundler: {
transports: {
[sepolia.id]: http (
`https://api.pimlico.io/v2/${ sepolia . id }/rpc?apikey=${ pimlicoApiKey }`
),
},
},
});
console. log ( `Batua Created \n ` , res);
}
}
Be sure to set the following in your .env.local
:
NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID = your_rainbowkit_project_id
NEXT_PUBLIC_PIMLICO_API_KEY=your_pimlico_key
Now we’ll implement the app/page.tsx
to:
Initialize Batua on mount
Connect your batua via RainbowKit
Batch-send ERC20 calls (approve + transfer)
export const TEST_ERC20_TOKEN_ADDRESS =
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ;
export const randomAddressOne = "0xE38fcB5145397B0c723222CDaDa3987425EFc0e5" ;
export const randomAddressTwo = "0x13AF61A3ec244456645fcE7881E0795E826CE1bD" ;
Refer to the constants file for the TEST_ERC20_TOKEN_ADDRESS
and randomAddressOne
values or define custom ones here.
"use client" ;
import { setupBatua } from "@/lib/utils/batuaClient" ;
import { useAccount } from "wagmi" ;
import { useEffect, useState } from "react" ;
import { useSendCalls, useWaitForCallsStatus } from "wagmi" ;
import { encodeFunctionData, parseUnits } from "viem" ;
import { TEST_ERC20_TOKEN_ADDRESS, randomAddressOne } from "@/lib/constants" ;
import { ConnectButton } from "@rainbow-me/rainbowkit" ;
import { config } from "@/lib/utils/wagmiConfig" ;
import { erc20Abi } from "viem" ;
import Link from "next/link" ;
export default function BatuaPage () {
const account = useAccount ();
const { data : callStatus , sendCallsAsync } = useSendCalls ({ config });
const { data : callReceipts } = useWaitForCallsStatus ({ id: callStatus?.id });
const [ hasMounted , setHasMounted ] = useState ( false );
const [ batchTxUrl , setBatchTxUrl ] = useState ( "" );
useEffect (() => {
setupBatua ();
setHasMounted ( true );
}, []);
if ( ! hasMounted) return null ;
const executeBatchTx = async () => {
const res = await sendCallsAsync ({
account: account.address,
chainId: account.chainId as 1 | 11155111 ,
calls: [
{
to: TEST_ERC20_TOKEN_ADDRESS ,
data: encodeFunctionData ({
abi: erc20Abi,
functionName: "approve" ,
args: [randomAddressOne, parseUnits ( "1" , 6 )],
}),
},
{
to: TEST_ERC20_TOKEN_ADDRESS ,
data: encodeFunctionData ({
abi: erc20Abi,
functionName: "transfer" ,
args: [randomAddressOne, parseUnits ( "1" , 6 )],
}),
},
] as any ,
});
if (callReceipts?.status === "success" && callReceipts.receipts) {
setBatchTxUrl (
`https://sepolia.etherscan.io/tx/${ callReceipts . receipts [ 0 ]. transactionHash }`
);
}
};
return (
< main className = "flex min-h-screen flex-col items-center justify-center p-24 space-y-8" >
< h1 className = "text-4xl" >Welcome to the Batua Dapp</ h1 >
{ ! account.isConnected ? (
< ConnectButton />
) : (
< div className = "flex flex-col items-center space-y-4" >
< ConnectButton />
< button
onClick = {executeBatchTx}
className = "px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
Send Batch Transaction
</ button >
{callReceipts?.status === "pending" && (
< p className = "text-yellow-500" >Executing...</ p >
)}
{callReceipts?.status === "failure" && (
< p className = "text-red-500" >Execution Failed</ p >
)}
{batchTxUrl && (
< p className = "text-green-500" >
Execution Successful!
< br />
< Link
href = {batchTxUrl}
target = "_blank"
className = "font-bold underline break-words"
>
{batchTxUrl}
</ Link >
</ p >
)}
</ div >
)}
</ main >
);
}
useEffect
calls setupBatua()
once the component mounts.
useSendCalls
& useWaitForCallsStatus
handle batching & status polling.
Two ERC20 calls are batched: approve
then transfer
.
Transaction result shown via Etherscan link.
Start your development server:
After running the command, open http://localhost:3000 to see your Batua Dapp in action.
Initial Dapp view before connecting a wallet.
Sign in with a Passkey-enabled smart account.
View after connecting the Batua smart account.
Prepared approve
+ transfer
calls queued for batch execution.
Transaction being sent via Batua.
Batch transaction completed successfully.
Inspect the user-operation tx on Etherscan.
Inspect the user-operation event emissions on Etherscan.
In this tutorial, you learned how to:
Embed the Batua smart-account wallet into your Next.js app
Initialize the Batua SDK with passkey and sponsorship support
Batch multiple ERC-20 calls in a single transaction
Check out the source code on GitHub here