Light Infra
Gasless smart wallet infrastructure with client-side signing. No private keys on the server—ever.
Light Infra is a production API for gasless smart account transactions using ERC-4337. Send tokens, approve contracts, execute swaps, bridge cross-chain — all without users needing ETH for gas. The V2 API uses a prepare → sign → submit flow so private keys never leave the client.
Client-Side Signing
Private keys never touch the server. Sign with Privy, MetaMask, wagmi, ethers, viem — any wallet.
Any Transaction, Gasless
Transfers, approvals, swaps, mints, contract calls — gas paid in USDC/USDT, not ETH.
Multi-Chain + Bridging
9 chains, same smart account address everywhere. Cross-chain bridging via Across Protocol.
Architecture
Supported Chains
| Chain | ID | Bundler | Paymaster | Gas Tokens |
|---|---|---|---|---|
| Ethereum | 1 | Pimlico | Pimlico ERC-20 | USDC |
| Base | 8453 | Pimlico | Pimlico ERC-20 | USDC |
| Polygon | 137 | Pimlico | Pimlico ERC-20 | USDC, USDT |
| Celo | 42220 | Pimlico | Pimlico ERC-20 | USDC, USDT |
| Arbitrum | 42161 | Pimlico | Pimlico ERC-20 | USDC |
| Avalanche | 43114 | Pimlico | Pimlico ERC-20 | USDC |
| BNB Chain | 56 | Pimlico | Pimlico ERC-20 | USDT |
| Berachain | 80094 | Pimlico | Pimlico ERC-20 | USDC |
| Lisk | 1135 |
Gelato | Custom LiskPaymaster | Bridged USDC, Bridged USDT |
Note: Lisk uses bridged versions of USDC and USDT — these are not native Circle/Tether tokens but ERC-20s bridged to Lisk via the canonical bridge. Same 6-decimal format.
LiskPaymaster Contract
A self-hosted verifying paymaster for Lisk (chain 1135), deployed because Pimlico does not support Lisk. ERC-4337 v0.7 paymaster spec with EntryPoint 0x0000000071727De22E5E9d8BAf0edAc6f37da032.
| Contract | 0x427236c2FbF46372e115027c367E63Bbb11F932C |
| Chain | Lisk Mainnet (1135) |
| EntryPoint | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 (v0.7) |
| Standard | ERC-4337 IPaymaster via BasePaymaster |
| Source | View on Blockscout |
Supported Tokens
| Token | Address | Decimals |
|---|---|---|
| Bridged USDC | 0xF242275d3a6527d877f2c927a82D9b057609cc71 | 6 |
| Bridged USDT | 0x05D032ac25d322df992303dCa074EE7392C117b9 | 6 |
How It Works
Key Contract Functions
| Function | Access | Description |
|---|---|---|
configureToken(address, uint256, bool) | Owner | Set token price per ETH and enable/disable |
setVerifyingSigner(address) | Owner | Update the off-chain signer address |
getHash(PackedUserOperation, address, uint48, uint48) | View | Compute the hash that the backend signer signs |
getTokenCostForGas(uint256, address) | View | Calculate token amount for a given gas cost in wei |
deposit() | Owner | Fund the paymaster's EntryPoint deposit (pays for gas) |
addStake(uint32) | Owner | Stake ETH with EntryPoint (required for paymasters) |
paymasterData Layout
The paymasterData field in the UserOperation is packed as:
| Offset | Bytes | Field |
|---|---|---|
| 0 | 20 | token — ERC-20 address (USDC/USDT) |
| 20 | 6 | validUntil — uint48 expiry timestamp |
| 26 | 6 | validAfter — uint48 earliest timestamp |
| 32 | 65 | signature — ECDSA sig from verifyingSigner |
Bundlers
Pimlico — 8 chains
Industry-standard ERC-4337 bundler. Handles UserOp submission, gas estimation, and ERC-20 paymaster sponsorship for Ethereum, Base, Polygon, Celo, Arbitrum, Avalanche, BNB, and Berachain.
https://api.pimlico.io/v2/{chain}/rpc?apikey=KEY
Gelato — Lisk
Bundler for Lisk (chain 1135). Supports EntryPoint v0.7, eth_sendUserOperation,
eth_estimateUserOperationGas, and eth_supportedEntryPoints.
https://api.gelato.cloud/rpc/1135?apiKey=KEY
Paymaster Routing
Gas Pricing (Lisk)
ETH price is fetched from the Enso API using Ethereum mainnet (chain ID 1) because Enso does not support the Lisk chain directly. We query the WETH price on mainnet — ETH price is the same regardless of chain:
GET https://api.enso.build/api/v1/prices/1/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
Authorization: Bearer ENSO_API_KEY
Token cost calculation (matches on-chain getTokenCostForGas):
// tokenCost = gasCostWei * ethPriceUsd * 10^decimals * markup / (1e18 * 10000)
const pricePerEth = ethPriceUsd * 10 ** tokenDecimals; // e.g. 2500 * 1e6
const tokenCost = gasCostWei * pricePerEth * markup / (1e18 * 10000);
Gas fee estimation queries the Gelato bundler's minimum priority fee and the Lisk RPC gas price, takes the highest, then adds a 20% buffer:
// Lisk gas fees (never fails -- aggressive fallbacks)
maxPriorityFeePerGas = max(rpcGasPrice, bundlerMinPriority, 1_000_000) * 1.2
maxFeePerGas = rpcGasPrice * 3 + maxPriorityFeePerGas
V2 API — Client-Side Signing
The V2 endpoints use a prepare → sign → submit pattern. The server builds the UserOperation and returns a hash. The client signs it locally. The server relays it to the bundler.
Prepare UserOperation
Request Body — Token Transfer
{
"ownerAddress": "0xYourEOAAddress",
"chainId": 1135,
"rpcUrl": "https://rpc.api.lisk.com",
"tokenAddress": "0xF242275d3a6527d877f2c927a82D9b057609cc71",
"recipient": "0xRecipientAddress",
"amount": 5.0
}
Request Body — Universal Transaction
{
"ownerAddress": "0xYourEOAAddress",
"chainId": 8453,
"rpcUrl": "https://mainnet.base.org",
"calls": [
{
"to": "0xRecipientAddress",
"value": "1000000000000000000"
}
],
"paymasterToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
}
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ownerAddress | address | Yes | EOA address (NOT private key) |
chainId | number | Yes | Target chain ID |
rpcUrl | string | Yes | Chain RPC endpoint |
tokenAddress | address | Transfer mode | ERC-20 token to transfer |
recipient | address | Transfer mode | Transfer recipient |
amount | number | Transfer mode | Amount (human readable, e.g. 5.0) |
calls | array | Transaction mode | Array of calls (to, value, data, abi, ...) |
paymasterToken | address | No | Token to pay gas with (for transaction mode) |
Response
{
"success": true,
"data": {
"smartAccountAddress": "0xYourSafeAddress",
"userOperation": {
"sender": "0x...",
"nonce": "0x...",
"callData": "0x...",
"callGasLimit": "0x...",
"verificationGasLimit": "0x...",
"preVerificationGas": "0x...",
"maxFeePerGas": "0x...",
"maxPriorityFeePerGas": "0x...",
"paymaster": "0x...",
"paymasterData": "0x...",
"signature": "0x..."
},
"hashToSign": "0xabcdef...",
"chainId": 1135
},
"message": "UserOperation prepared. Sign hashToSign with personal_sign..."
}
Submit Signed UserOperation
Request Body
{
"chainId": 1135,
"userOperation": { /* full userOperation from /api/v2/prepare */ },
"signature": "0x...your_personal_sign_result..."
}
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
chainId | number | Yes | Must match the chain from prepare |
userOperation | object | Yes | Full UserOp object from prepare response |
signature | hex string | Yes | 65-byte ECDSA signature (0x + 130 hex chars) |
Response
{
"success": true,
"data": {
"userOperationHash": "0x1234...",
"chainId": 1135
},
"message": "UserOperation submitted successfully on chain 1135"
}
The server encodes the signature in Safe format (adjusts v by +4 for the eth_sign path, prepends validAfter=0 + validUntil=0) and sends to the bundler via eth_sendUserOperation.
Cross-Chain Bridging (Across Protocol)
Bridge tokens between any supported chains using Across Protocol. The V2 bridge endpoint uses the same prepare → sign → submit flow — no private key needed. The server fetches an Across quote, builds the approve + depositV3 calls, and returns a UserOp for you to sign. Gas is paid in the token being bridged.
depositV3 calls manually via /api/v2/prepare.
Request Body
{
"ownerAddress": "0xYourEOAAddress",
"sourceChainId": 137,
"destChainId": 42161,
"rpcUrl": "https://polygon-rpc.com",
"tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"amount": 50.0,
"recipient": "0xRecipientOnDestChain"
}
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ownerAddress | address | Yes | EOA address (NOT private key) |
sourceChainId | number | Yes | Source chain ID |
destChainId | number | Yes | Destination chain ID |
rpcUrl | string | Yes | Source chain RPC |
tokenAddress | address | Yes | Token to bridge (USDC, USDT) |
amount | number | Yes | Amount (human-readable) |
recipient | address | No | Recipient on dest chain (defaults to same smart account) |
Response
{
"success": true,
"data": {
"smartAccountAddress": "0x...",
"userOperation": { /* ...full UserOp... */ },
"hashToSign": "0xabcdef...",
"chainId": 137,
"bridge": {
"sourceChainId": 137,
"destChainId": 42161,
"inputAmount": "50.0",
"outputAmount": "49.85",
"fee": "0.15",
"estimatedFillTime": "~2-5 minutes"
}
},
"message": "Sign hashToSign and submit to /api/v2/submit"
}
Sign the hashToSign and submit via POST /api/v2/submit — same as any other v2 operation.
Wallet Integrations & Signing
The hashToSign from /api/v2/prepare is the Safe operation hash (EIP-712). Sign it with personal_sign using any wallet:
personal_sign (EIP-191), not eth_sign. The Safe4337Module expects the \x19Ethereum Signed Message:\n32 prefix. The server adjusts v (+4) for Safe's eth_sign verification path.
Privy
Privy embedded wallets work seamlessly. Use the useSignMessage hook:
Token Transfer
import { usePrivy, useSignMessage } from '@privy-io/react-auth';
function GaslessTransfer() {
const { user } = usePrivy();
const { signMessage } = useSignMessage();
const address = user?.wallet?.address;
async function send() {
// Step 1: Prepare (send EOA address, NOT private key)
const res = await fetch('/api/v2/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: address,
chainId: 137,
rpcUrl: 'https://polygon-rpc.com',
tokenAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
recipient: '0xRecipient',
amount: 5.0,
}),
});
const { data } = await res.json();
// Step 2: Sign with Privy embedded wallet
const signature = await signMessage(data.hashToSign);
// Step 3: Submit
await fetch('/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 137, userOperation: data.userOperation, signature }),
});
}
}
ERC-20 Approve (any contract call)
import { usePrivy, useSignMessage } from '@privy-io/react-auth';
function GaslessApprove() {
const { user } = usePrivy();
const { signMessage } = useSignMessage();
const address = user?.wallet?.address;
async function approveToken(spender, amount) {
// Use "calls" instead of tokenAddress/recipient/amount
const res = await fetch('/api/v2/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: address,
chainId: 1135,
rpcUrl: 'https://rpc.api.lisk.com',
calls: [{
to: '0xF242275d3a6527d877f2c927a82D9b057609cc71', // Bridged USDC on Lisk
abi: ['function approve(address spender, uint256 amount)'],
functionName: 'approve',
args: [spender, amount],
}],
paymasterToken: '0xF242275d3a6527d877f2c927a82D9b057609cc71',
}),
});
const { data } = await res.json();
// Sign with Privy
const signature = await signMessage(data.hashToSign);
// Submit
await fetch('/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 1135, userOperation: data.userOperation, signature }),
});
}
return (
<button onClick={() => approveToken('0xDEXRouter', '1000000')}>
Approve USDC
</button>
);
}
Cross-Chain Bridge
import { usePrivy, useSignMessage } from '@privy-io/react-auth';
function GaslessBridge() {
const { user } = usePrivy();
const { signMessage } = useSignMessage();
const address = user?.wallet?.address;
async function bridge() {
// Step 1: Prepare bridge UserOp
const res = await fetch('https://smart-wallet-519w.onrender.com/api/v2/bridge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: address,
sourceChainId: 137,
destChainId: 42161,
rpcUrl: 'https://polygon-rpc.com',
tokenAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
amount: 25.0,
}),
});
const { data } = await res.json();
// Step 2: Sign with Privy embedded wallet
const signature = await signMessage(data.hashToSign);
// Step 3: Submit
await fetch('https://smart-wallet-519w.onrender.com/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 137, userOperation: data.userOperation, signature }),
});
}
}
wagmi / RainbowKit / ConnectKit
Works with any wagmi-compatible wallet (MetaMask, WalletConnect, Coinbase Wallet, etc.):
import { useAccount, useSignMessage } from 'wagmi';
import { toBytes } from 'viem';
function GaslessAction() {
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
async function execute() {
// Prepare — works for transfers, approvals, any contract call
const res = await fetch('/api/v2/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: address,
chainId: 8453,
rpcUrl: 'https://mainnet.base.org',
calls: [{ to: '0xContract', data: '0x...' }],
paymasterToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
}),
});
const { data } = await res.json();
// Sign with connected wallet (MetaMask, WalletConnect, etc.)
const signature = await signMessageAsync({
message: { raw: toBytes(data.hashToSign) },
});
// Submit
await fetch('/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 8453, userOperation: data.userOperation, signature }),
});
}
}
Cross-Chain Bridge
import { useAccount, useSignMessage } from 'wagmi';
import { toBytes } from 'viem';
function GaslessBridge() {
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
async function bridge() {
// Step 1: Prepare bridge UserOp
const res = await fetch('https://smart-wallet-519w.onrender.com/api/v2/bridge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: address,
sourceChainId: 8453,
destChainId: 42161,
rpcUrl: 'https://mainnet.base.org',
tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
amount: 100.0,
}),
});
const { data } = await res.json();
// Step 2: Sign with connected wallet (MetaMask, WalletConnect, etc.)
const signature = await signMessageAsync({
message: { raw: toBytes(data.hashToSign) },
});
// Step 3: Submit
await fetch('https://smart-wallet-519w.onrender.com/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 8453, userOperation: data.userOperation, signature }),
});
}
}
Server-Side Signing (ethers.js)
For backend services, bots, or automation — generate or use a private key server-side:
import { ethers } from 'ethers';
// Generate a new wallet (or use existing key)
const wallet = ethers.Wallet.createRandom();
// Or: const wallet = new ethers.Wallet('0xYourPrivateKey');
console.log('EOA:', wallet.address); // save this
console.log('Key:', wallet.privateKey); // store securely!
// Prepare any transaction
const prepareRes = await fetch('https://smart-wallet-519w.onrender.com/api/v2/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: wallet.address,
chainId: 1135,
rpcUrl: 'https://rpc.api.lisk.com',
tokenAddress: '0xF242275d3a6527d877f2c927a82D9b057609cc71',
recipient: '0xRecipient',
amount: 10.0,
}),
});
const { data } = await prepareRes.json();
// Sign locally
const signature = await wallet.signMessage(ethers.utils.arrayify(data.hashToSign));
// Submit
const result = await fetch('https://smart-wallet-519w.onrender.com/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 1135, userOperation: data.userOperation, signature }),
}).then(r => r.json());
console.log('UserOp:', result.data.userOperationHash);
Cross-Chain Bridge
import { ethers } from 'ethers';
const wallet = new ethers.Wallet('0xYourPrivateKey');
// Step 1: Prepare bridge UserOp
const prepareRes = await fetch('https://smart-wallet-519w.onrender.com/api/v2/bridge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ownerAddress: wallet.address,
sourceChainId: 137,
destChainId: 42161,
rpcUrl: 'https://polygon-rpc.com',
tokenAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
amount: 50.0,
}),
});
const { data: bridgeData } = await prepareRes.json();
// Step 2: Sign locally with ethers.Wallet.signMessage
const bridgeSig = await wallet.signMessage(ethers.utils.arrayify(bridgeData.hashToSign));
// Step 3: Submit
const bridgeResult = await fetch('https://smart-wallet-519w.onrender.com/api/v2/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 137, userOperation: bridgeData.userOperation, signature: bridgeSig }),
}).then(r => r.json());
console.log('Bridge UserOp:', bridgeResult.data.userOperationHash);
MetaMask / Browser window.ethereum
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [hashToSign, ownerAddress],
});
viem (standalone)
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(privateKey);
const signature = await account.signMessage({ message: { raw: hashToSign } });
Examples
Token Transfer
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/prepare \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "0xYourEOA",
"chainId": 1135,
"rpcUrl": "https://rpc.api.lisk.com",
"tokenAddress": "0xF242275d3a6527d877f2c927a82D9b057609cc71",
"recipient": "0xRecipient",
"amount": 5.0
}'
ERC-20 Approve (any chain including Lisk)
Approve a contract to spend tokens on your behalf. Works on all chains:
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/prepare \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "0xYourEOA",
"chainId": 1135,
"rpcUrl": "https://rpc.api.lisk.com",
"calls": [
{
"to": "0xF242275d3a6527d877f2c927a82D9b057609cc71",
"abi": ["function approve(address spender, uint256 amount)"],
"functionName": "approve",
"args": ["0xSpenderContract", "1000000000"]
}
],
"paymasterToken": "0xF242275d3a6527d877f2c927a82D9b057609cc71"
}'
Multi-Call: Approve + Swap (single UserOp)
Execute multiple contract calls atomically in one transaction:
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/prepare \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "0xYourEOA",
"chainId": 8453,
"rpcUrl": "https://mainnet.base.org",
"calls": [
{
"to": "0xTokenAddress",
"abi": ["function approve(address,uint256)"],
"functionName": "approve",
"args": ["0xDEXRouter", "1000000"]
},
{
"to": "0xDEXRouter",
"data": "0x...encoded swap calldata..."
}
],
"paymasterToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
}'
Raw Calldata (any contract interaction)
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/prepare \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "0xYourEOA",
"chainId": 1135,
"rpcUrl": "https://rpc.api.lisk.com",
"calls": [
{
"to": "0xAnyContract",
"data": "0xabcdef...raw calldata...",
"value": "0"
}
],
"paymasterToken": "0xF242275d3a6527d877f2c927a82D9b057609cc71"
}'
Cross-Chain Bridge (Across Protocol)
Bridge USDC from Polygon to Arbitrum (client-side signing):
# Step 1: Prepare bridge UserOp
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/bridge \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "0xYourEOA",
"sourceChainId": 137,
"destChainId": 42161,
"rpcUrl": "https://polygon-rpc.com",
"tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"amount": 50.0
}'
# Step 2: Sign hashToSign locally with your wallet
# Step 3: Submit signed UserOp
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/submit \
-H "Content-Type: application/json" \
-d '{
"chainId": 137,
"userOperation": { "...from prepare response..." },
"signature": "0x...your signature..."
}'
Use with Claude / LLMs
You can give Claude (or any LLM) the API reference file so it can build transactions for you. Download the markdown file and include it in your prompt context:
How to use with Claude
# In your Claude prompt / system prompt, include:
You have access to the Light Infra API. Here is the API reference:
<api-reference>
{paste contents of api-reference.md}
</api-reference>
Use the /api/v2/prepare and /api/v2/submit endpoints to build
gasless transactions. The user will sign the hashToSign locally.
Example Claude interaction
User: Send 10 USDC to 0xBob on Polygon, my address is 0xAlice
Claude: I'll prepare a gasless USDC transfer for you.
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/prepare \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "0xAlice",
"chainId": 137,
"rpcUrl": "https://polygon-rpc.com",
"tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"recipient": "0xBob",
"amount": 10.0
}'
Sign the hashToSign from the response, then submit:
curl -X POST https://smart-wallet-519w.onrender.com/api/v2/submit ...
.md format works best for LLM context. You can also paste the file contents directly into the system prompt for tools like Claude Code, Cursor, or Windsurf.