Automated cross-chain token bridging using Biconomy Smart Sessions (EIP-7702) and Across Protocol V3. Users connect a Privy embedded wallet, delegate it to a Nexus smart account, grant session permissions, and the server continuously polls for incoming deposits and bridges them to a chosen destination chain.
src/
├── app/ # Next.js App Router
│ ├── page.tsx # Entry — renders <App /> (SSR disabled)
│ ├── layout.tsx # Root layout
│ ├── providers.tsx # PrivyProvider (embedded wallets, supported chains)
│ ├── admin/page.tsx # Admin panel UI
│ └── api/
│ ├── sessions/
│ │ ├── register/route.ts # POST — register a session
│ │ └── [address]/route.ts # GET / PATCH / DELETE a session
│ ├── cron/poll/route.ts # GET — Vercel Cron: poll & bridge
│ └── admin/sessions/route.ts # GET / DELETE — admin (sig-gated)
│
├── components/ # React UI components
│ ├── Pipeline.tsx # Setup pipeline (steps 1–6)
│ ├── ListeningDashboard.tsx # Post-setup monitoring dashboard
│ ├── PaymentPage.tsx # Public payment link (?pay=0x…)
│ ├── ManageFunds.tsx # Fund management tab
│ └── steps/ # Individual pipeline step cards
│ ├── ConnectWalletStep.tsx
│ ├── SelectDestinationStep.tsx
│ ├── SignAuthorizationStep.tsx
│ ├── InitializeNexusStep.tsx
│ ├── InstallSessionsStep.tsx
│ ├── GrantPermissionStep.tsx
│ └── ...
│
├── hooks/
│ ├── usePipeline.ts # All pipeline state, handlers, auto-advance
│ └── useManageFunds.ts # Fund management logic
│
├── sessions/ # Session setup & execution logic
│ ├── createSessionSigner.ts # Generate or restore a session keypair
│ ├── createSmartSessionModule.ts # Wrap signer into a SmartSessions module
│ ├── createSessionMeeClient.ts # Build multichain Nexus account + MEE client
│ ├── installSessionModule.ts # Install the sessions module on-chain
│ ├── grantDepositV3Permission.ts # Sign the typed-data permission grant
│ ├── buildDepositV3Actions.ts # Build per-chain action descriptors
│ ├── executeDepositV3.ts # Execute an Across depositV3 bridge (server)
│ ├── executeForwardTransfer.ts # Execute an ERC-20 transfer (server)
│ ├── getScheduledExecutionBounds.ts # Timestamp bounds for supertxs
│ ├── sessionStore.ts # Dual-layer storage (localStorage + server API)
│ └── types.ts # SessionDetails type
│
├── lib/ # Server-side utilities
│ ├── db.ts # Upstash Redis storage (session CRUD)
│ ├── encrypt.ts # AES-256-GCM encryption for session keys at rest
│ ├── pollAndBridge.ts # Core polling loop — balance check → bridge/forward
│ ├── bigintJson.ts # JSON serialisation preserving BigInt values
│ └── log.ts # Coloured logging helpers
│
├── config.ts # Chains, tokens, contract addresses, env RPC URLs
├── constants.ts # UI metadata (chain colours, step themes)
├── types.ts # Shared UI types (Status, StepStatus)
└── utils.ts # Formatting & derivation helpers
scripts/
└── local-cron.mjs # Local dev replacement for Vercel Cron
vercel.json # Cron schedule: /api/cron/poll every minute
The usePipeline hook orchestrates six auto-advancing steps. Each step triggers the next on success.
Privy creates an embedded wallet (EOA) on login. No external wallet required.
User picks a destination chain (Optimism, Base, Polygon, or Arbitrum) and an optional custom recipient address. If recipient is "self", bridged funds stay in the user's wallet on the destination chain.
What is signed: An EIP-7702 authorization that delegates the user's EOA to the Nexus singleton (0x00000000383e8cBe298514674Ea60Ee1d1de50ac) with chainId: 0 (valid on all chains).
Who signs: The user's Privy embedded wallet via useSign7702Authorization.
Effect: The EOA becomes a smart account (Nexus) on every supported chain once the authorization is propagated on-chain.
Builds a MultichainNexusAccount and a MeeClient using the user's Privy provider. The MEE client is configured for all supported chains (Optimism, Base, Polygon, Arbitrum) with version V2_1_0.
No signing occurs; this is client-side object construction.
A session signer keypair is generated (or restored from localStorage). The public key becomes the redeemer for future permission grants. The private key is persisted locally and later sent to the server.
The SmartSessions validator module is installed via prepareForPermissions. The 7702 authorization from Step 3 is piggybacked into this same supertransaction (multichain7702Auth: true), so the delegation is activated on all chains in a single step.
What is signed: The MEE supertransaction that installs the sessions module and propagates the 7702 delegation. Signed by the user's Privy wallet (the MEE client uses the user's provider as signer).
What is signed: A typed-data (EIP-712) permission grant via grantPermissionTypedDataSign. This is the core session permission.
Who signs: The user's Privy wallet (owner of the Nexus account).
Granted permissions (per supported chain):
| Action | Target | Selector | Policy |
|---|---|---|---|
approve |
Each token address (USDC, USDT, WETH) | 0x095ea7b3 |
Sudo |
depositV3 |
Across SpokePool | 0xe7a7ed02 |
Sudo |
The redeemer is the session signer's address (from Step 5). The fee token is USDC on Arbitrum (max 2 USDC).
The returned sessionDetails object is the proof that the session signer is authorized to execute these actions. It is saved to localStorage and sent to the server.
After Step 6 completes, the client calls POST /api/sessions/register with:
walletAddress— the user's EOAsessionPrivateKey— the session signer's private key (encrypted at rest with AES-256-GCM)sessionSignerAddress— the session signer's public addresssessionDetails— the typed-data grant result (contains BigInts serialised as__bigint:…)listeningConfig—{ destChainId, recipientIsSelf, recipientAddr }sessionVersion— bumped when permission scope changes (invalidates old sessions)
Returned by grantPermissionTypedDataSign. Contains the EIP-712 typed-data signature, the permission ID, the list of granted actions, and chain-specific enable data. This is opaque to the app — it is passed directly to usePermission when executing.
session:<walletAddress> → {
walletAddress: string,
encryptedKey: string, // AES-256-GCM encrypted session private key
sessionSignerAddress: string,
sessionDetails: object, // BigInts as "__bigint:…"
listeningConfig: {
destChainId: number,
recipientIsSelf: boolean,
recipientAddr: string,
},
sessionVersion: number,
registeredAt: string, // ISO timestamp
lastPollAt: string | null,
active: boolean,
}
Redis also maintains a SET at sessions:active with all actively-monitored wallet addresses.
Three keys per wallet (prefixed nexus_session:<address>:):
key— session signer private key (hex)details— versioned envelope{ version, details: SessionDetails }(BigInt-safe JSON)listening—{ destChainId, recipientIsSelf, recipientAddr }
On page load, the client restores from localStorage for instant UI hydration, then verifies against the server. If the server no longer has the session, local state is cleared.
- Vercel Cron hits
GET /api/cron/pollevery minute (protected byCRON_SECRET). pollAllSessions()fetches all addresses from thesessions:activeRedis SET.- For each wallet:
- Determines watched chains = all supported chains except the destination. If recipient ≠ self, the destination chain is also watched (for forwarding).
- Reads ERC-20 balances (USDC, USDT, WETH) on every watched chain via
balanceOf. - If a balance exceeds the minimum threshold (0.1 USDC/USDT, 0.00001 WETH), a deposit is detected.
- On deposit detection:
- The session private key is decrypted from Redis.
- A server-side MEE client is built using the session signer (not the user's wallet).
checkEnabledPermissionsdetermines if the permission is already enabled on-chain → usesUSEmode (cheaper) orENABLE_AND_USE.- If deposit is on a source chain:
executeDepositV3callsapprove+depositV3on the Across SpokePool viausePermission. The supertransaction is gas-sponsored. - If deposit is on the destination chain and recipient ≠ self:
executeForwardTransfercallstransferto move tokens to the recipient.
scripts/local-cron.mjs replaces Vercel Cron locally. It polls http://localhost:3000/api/cron/poll on a configurable interval (default 10s for dev, 60s for prod).
npm run dev:cron # polls every 60s
npm run dev:full # starts Next.js + cron together| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/sessions/register |
None | Register a wallet for server-side monitoring |
GET |
/api/sessions/[address] |
None | Check registration status |
PATCH |
/api/sessions/[address] |
None | Update config (listeningConfig, active, etc.) |
DELETE |
/api/sessions/[address] |
None | Stop monitoring and delete session |
GET |
/api/cron/poll |
CRON_SECRET |
Trigger a poll cycle (Vercel Cron) |
GET |
/api/admin/sessions |
Admin signature | List all sessions |
DELETE |
/api/admin/sessions?address= |
Admin signature | Delete a specific session |
Admin endpoints require an x-admin-signature header — an EIP-191 personal sign from the hardcoded admin address, with a timestamp that must be within 5 minutes.
| Step | What is Signed | Signer | Standard |
|---|---|---|---|
| 3 | EIP-7702 authorization (delegate EOA → Nexus) | User (Privy wallet) | EIP-7702 |
| 5 | Supertransaction: install sessions module + propagate 7702 | User (Privy wallet) | MEE supertx |
| 6 | Permission grant (approve + depositV3 on all chains) | User (Privy wallet) | EIP-712 typed data |
| Cron | Bridge execution via usePermission |
Session signer (server-side) | MEE supertx |
| Admin | Admin panel access | Admin EOA | EIP-191 personal sign |
- @biconomy/abstractjs — Nexus smart accounts, MEE client, smart sessions
- @privy-io/react-auth — Embedded wallets, EIP-7702 authorization signing
- @upstash/redis — Serverless Redis for session persistence
- viem — Ethereum client library (ABI encoding, contract reads, signing)
- next — App Router, API routes, Vercel Cron integration
| Variable | Where | Description |
|---|---|---|
NEXT_PUBLIC_PRIVY_APP_ID |
Client | Privy application ID |
NEXT_PUBLIC_BICONOMY_API_KEY |
Client + Server | Biconomy MEE service API key |
NEXT_PUBLIC_RPC_BASE |
Client + Server | RPC URL for Base |
NEXT_PUBLIC_RPC_OPTIMISM |
Client + Server | RPC URL for Optimism |
NEXT_PUBLIC_RPC_POLYGON |
Client + Server | RPC URL for Polygon |
NEXT_PUBLIC_RPC_ARBITRUM |
Client + Server | RPC URL for Arbitrum |
NEXT_PUBLIC_RPC_ETHEREUM |
Client + Server | RPC URL for Ethereum mainnet |
NEXT_PUBLIC_RPC_BNB |
Client + Server | RPC URL for BNB Chain |
KV_REST_API_URL |
Server | Upstash Redis URL (auto-injected by Vercel) |
KV_REST_API_TOKEN |
Server | Upstash Redis token (auto-injected by Vercel) |
SESSION_ENCRYPTION_KEY |
Server | 64-char hex string (32 bytes) for AES-256-GCM |
CRON_SECRET |
Server | Bearer token protecting the cron endpoint |
Chains: Optimism, Base, Polygon, Arbitrum
Tokens: USDC, USDT, WETH (each with per-chain addresses defined in config.ts)
Bridge: Across Protocol V3 SpokePool contracts (per-chain addresses in config.ts)