Integrating ClawPaymaster
This guide shows how to integrate ClawPaymaster into your dApp or agent infrastructure to enable gasless transactions for ClawWallet agent wallets.
Overview
On Abstract Chain (zkSync ERA), paymasters are a native protocol feature. When a transaction includes paymaster parameters, the paymaster contract pays the gas instead of the sender. ClawPaymaster manages the logic of which wallets get sponsored and tracks usage.
Architecture
┌──────────┐ ┌───────────────┐ ┌──────────────┐
│ Agent │────▶│ Transaction │────▶│ Abstract │
│ Wallet │ │ + Paymaster │ │ Sequencer │
└──────────┘ │ Params │ │ │
└───────────────┘ │ Calls │
│ Paymaster │
│ hooks │
│ │
┌───────────────┐ │ │
│ ClawPaymaster │◀────│ │
│ validates & │ │ │
│ pays gas │ └──────────────┘
└───────────────┘
Step 1: Check Eligibility
Before sending a gasless transaction, verify the wallet is eligible:
check-eligibility.ts
import { ethers } from 'ethers';
const PAYMASTER = '0x7BBBBbDaCE3EA19Fe317e620CbD89F1040F2ddAf';
const provider = new ethers.JsonRpcProvider('https://api.mainnet.abs.xyz');
const paymaster = new ethers.Contract(PAYMASTER, [
'function validateSponsorship(address wallet, uint256 gasAmount) external view returns (bool)',
'function getRemainingDailyGas(address wallet) external view returns (uint256)',
'function getBalance() external view returns (uint256)',
'function getWalletConfig(address wallet) external view returns (tuple(uint8 mode, uint256 dailyGasLimit, uint256 dailyGasUsed, uint256 lastResetDay, bool approved))',
], provider);
async function checkEligibility(walletAddress: string) {
// Check remaining daily gas
const remaining = await paymaster.getRemainingDailyGas(walletAddress);
console.log('Remaining daily gas:', ethers.formatEther(remaining), 'ETH');
// Check paymaster balance
const balance = await paymaster.getBalance();
console.log('Paymaster balance:', ethers.formatEther(balance), 'ETH');
// Validate specific amount
const estimatedGas = ethers.parseEther('0.001'); // Estimate
const canSponsor = await paymaster.validateSponsorship(walletAddress, estimatedGas);
console.log('Can sponsor:', canSponsor);
// Get wallet config
const config = await paymaster.getWalletConfig(walletAddress);
const modes = ['FREE_TIER', 'TOKEN_PAID', 'REVENUE_FUNDED'];
console.log('Mode:', modes[config.mode]);
console.log('Daily limit:', ethers.formatEther(config.dailyGasLimit));
console.log('Used today:', ethers.formatEther(config.dailyGasUsed));
return { remaining, balance, canSponsor, config };
}
Step 2: Fund the Paymaster
The paymaster needs ETH to sponsor gas. Fund it directly:
fund-paymaster.ts
const paymaster = new ethers.Contract(PAYMASTER, [
'function fund() external payable',
'function getBalance() external view returns (uint256)',
], wallet);
// Fund with 1 ETH
await paymaster.fund({ value: ethers.parseEther('1.0') });
// Or simply send ETH (has receive() function)
await wallet.sendTransaction({
to: PAYMASTER,
value: ethers.parseEther('1.0'),
});
const balance = await paymaster.getBalance();
console.log('Paymaster balance:', ethers.formatEther(balance), 'ETH');
Step 3: Configure Wallet Sponsorship
Set custom configurations for specific wallets:
configure-wallet.ts
// Only the paymaster owner can set wallet configs
const paymaster = new ethers.Contract(PAYMASTER, [
'function setWalletConfig(address wallet, uint8 mode, uint256 dailyGasLimit) external',
'function setDefaultDailyGasLimit(uint256 limit) external',
'function setDefaultMode(uint8 mode) external',
], paymasterOwner);
// Set a wallet to Revenue Funded mode with 1 ETH daily limit
await paymaster.setWalletConfig(
walletAddress,
2, // REVENUE_FUNDED
ethers.parseEther('1.0'),
);
// Update default settings for all wallets
await paymaster.setDefaultDailyGasLimit(ethers.parseEther('0.2'));
await paymaster.setDefaultMode(0); // FREE_TIER
Step 4: Build a Gas Dashboard
Create a monitoring dashboard for your paymaster:
dashboard.ts
async function getPaymasterStats() {
const paymaster = new ethers.Contract(PAYMASTER, [
'function getBalance() external view returns (uint256)',
'function totalSponsored() external view returns (uint256)',
'function defaultMode() external view returns (uint8)',
'function defaultDailyGasLimit() external view returns (uint256)',
], provider);
const [balance, totalSponsored, defaultMode, dailyLimit] = await Promise.all([
paymaster.getBalance(),
paymaster.totalSponsored(),
paymaster.defaultMode(),
paymaster.defaultDailyGasLimit(),
]);
const modes = ['FREE_TIER', 'TOKEN_PAID', 'REVENUE_FUNDED'];
return {
balance: ethers.formatEther(balance),
totalSponsored: ethers.formatEther(totalSponsored),
defaultMode: modes[defaultMode],
dailyLimit: ethers.formatEther(dailyLimit),
};
}
Step 5: Monitor Sponsorship Events
Listen for real-time sponsorship events:
monitor.ts
const paymaster = new ethers.Contract(PAYMASTER, [
'event WalletSponsored(address indexed wallet, uint256 gasUsed, uint8 mode)',
'event PaymasterFunded(address indexed funder, uint256 amount)',
'event PaymasterDefunded(address indexed to, uint256 amount)',
], provider);
// Monitor sponsorships
paymaster.on('WalletSponsored', (wallet, gasUsed, mode) => {
const modes = ['FREE_TIER', 'TOKEN_PAID', 'REVENUE_FUNDED'];
console.log(`Sponsored ${wallet}: ${ethers.formatEther(gasUsed)} ETH (${modes[mode]})`);
});
// Monitor funding
paymaster.on('PaymasterFunded', (funder, amount) => {
console.log(`Funded by ${funder}: ${ethers.formatEther(amount)} ETH`);
});
Rate Limiting Strategy
The paymaster implements daily rate limiting per wallet:
Daily gas reset: midnight UTC (block.timestamp / 86400)
For each transaction:
1. If new day: reset dailyGasUsed to 0
2. Check: dailyGasUsed + gasAmount <= dailyGasLimit
3. If approved: dailyGasUsed += gasAmount
Recommended Limits
| Wallet Type | Daily Gas Limit | Mode |
|---|---|---|
| New agents | 0.1 ETH | FREE_TIER |
| Active agents | 0.5 ETH | FREE_TIER |
| Revenue-generating | 1.0+ ETH | REVENUE_FUNDED |
| Premium | Unlimited | REVENUE_FUNDED |
Error Handling
try {
const canSponsor = await paymaster.validateSponsorship(wallet, gasAmount);
if (!canSponsor) {
// Check why
const remaining = await paymaster.getRemainingDailyGas(wallet);
const balance = await paymaster.getBalance();
if (remaining === 0n) {
console.error('Daily gas limit reached. Resets at midnight UTC.');
} else if (balance < gasAmount) {
console.error('Paymaster needs funding.');
}
}
} catch (err) {
console.error('Sponsorship check failed:', err);
}
Complete Integration Example
gasless-agent.ts
import { ethers } from 'ethers';
const PAYMASTER = '0x7BBBBbDaCE3EA19Fe317e620CbD89F1040F2ddAf';
const FACTORY = '0xf6B945dBf532D376A475E31be32F51972915B1cc';
async function setupGaslessAgent(agentAddress: string) {
const provider = new ethers.JsonRpcProvider('https://api.mainnet.abs.xyz');
const owner = new ethers.Wallet(process.env.OWNER_KEY!, provider);
// 1. Get agent's wallet
const factory = new ethers.Contract(FACTORY, [
'function agentToWallet(address) view returns (address)',
], provider);
const walletAddress = await factory.agentToWallet(agentAddress);
// 2. Check paymaster eligibility
const paymaster = new ethers.Contract(PAYMASTER, [
'function validateSponsorship(address, uint256) view returns (bool)',
'function getRemainingDailyGas(address) view returns (uint256)',
], provider);
const remaining = await paymaster.getRemainingDailyGas(walletAddress);
console.log(`Agent wallet ${walletAddress}`);
console.log(`Remaining daily gas: ${ethers.formatEther(remaining)} ETH`);
if (remaining > 0n) {
console.log('✅ Agent can submit gasless transactions');
} else {
console.log('⚠️ Daily limit reached, wait until midnight UTC');
}
return { walletAddress, remainingGas: remaining };
}