Seneschal Paymaster LIVE
Pay ERC-4337 transaction gas in stablecoins on Base mainnet. No ETH required in your smart account; the paymaster fronts the gas and pulls USDC/EURC/USDT/DAI/cbBTC from you at Chainlink-quoted rates.
exact_match). Integration questions:
GitHub issues
or @OrknetP on Telegram.
Live address
| Paymaster | 0xb6E8d189285003cF0000388b01BA0C3433ee9f14 |
|---|---|
| Chain | Base mainnet (chainId 8453) |
| EntryPoint | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 (ERC-4337 v0.7) |
| Source | Rotwang9000/seneschal-paymaster |
| Verification | BaseScan · Sourcify |
| Smoke-tested via | Pimlico (api.pimlico.io/v2/base/rpc) |
The earlier v1 paymaster at
0xC5642B37000F9559b4628D7BA228051010150017
deprecated is undeployed (deposit
withdrawn). v1 under-charged for its own postOp gas; v2 fixes that with
an explicit overhead inside _postOp and a validUntil
window inside _validatePaymasterUserOp.
Supported tokens
| Symbol | Address | Decimals | Markup over Chainlink spot |
|---|---|---|---|
USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 | 7% |
EURC | 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42 | 6 | 7% |
USDT | 0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2 | 6 | 8% |
DAI | 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb | 18 | 8% |
cbBTC | 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf | 8 | 10% |
Prices feed from Chainlink. Stablecoins use the canonical token/USD feeds with a 25h heartbeat tolerance; cbBTC uses a 2h tolerance.
paymasterAndData layout
[ 0..20) paymaster address (set by EntryPoint)
[20..36) paymasterVerificationGasLimit (recommended ≥ 200 000)
[36..52) paymasterPostOpGasLimit (recommended ≥ 120 000)
[52..72) token address (one of the supported tokens)
[72..104) maxTokenCost (uint256) (max token-native units you'll spend)
maxTokenCost is a slippage guard. Quote off-chain with
paymaster.quote(token, expectedGasWei) and supply 110-120% of
that to absorb a baseFee uptick between simulation and inclusion.
Gas budget rationale
Validation reads two Chainlink feeds (ETH/USD + token/USD),
executes a transferFrom from the sender, and emits the prefund
context. Empirical gas: ~140–160k. PostOp reads the token feed again,
executes a transfer of the refund, and emits an event.
Empirical gas: ~55–75k.
Setting paymasterVerificationGasLimit below 150 000 or
paymasterPostOpGasLimit below 90 000 will cause
Pimlico/Alchemy/Stackup to drop the userOp with AA33 reverted 0x
(out-of-gas inside the paymaster). The contract refunds unused gas via
the postOp refund mechanism, so generous budgets are essentially free.
validationData
_validatePaymasterUserOp returns a validUntil of
block.timestamp + 5 minutes. Bundlers will therefore drop any
userOp that doesn't get included within that window — important because
the Chainlink-read prices in validation could otherwise drift. If you
simulate a userOp and then sit on it for >5 minutes before submitting,
re-simulate.
What the smart account must have
Before sending the userOp:
- Token balance: at least
maxTokenCostof the chosen token in the smart account. - Token allowance: the paymaster needs
approve(paymaster, maxTokenCost)(or more —MaxUint256once is fine). The allowance can be granted either:- in a prior userOp the user signs (one-off "approve paymaster for unlimited USDC"), or
- in a batched call within the same userOp (if your
account supports batched calls and your bundler allows
transferFrom-from-sender during validation, which Pimlico does).
If the allowance or balance is insufficient, the bundler will drop the userOp during simulation. There's no on-chain failure path.
Live example — viem + permissionless
import { createPublicClient, http, encodePacked, parseAbi } from 'viem';
import { base } from 'viem/chains';
import { createBundlerClient, entryPoint07Address } from 'viem/account-abstraction';
const PAYMASTER = '0xb6E8d189285003cF0000388b01BA0C3433ee9f14';
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const pub = createPublicClient({
chain: base,
transport: http('https://base-rpc.publicnode.com')
});
const quote = await pub.readContract({
address: PAYMASTER,
abi: parseAbi(['function quote(address,uint256) view returns (uint256)']),
functionName: 'quote',
args: [USDC, 900_000_000_000_000n] // 0.0009 ETH ≈ 300k gas @ 3 gwei
});
const maxTokenCost = (quote * 12n) / 10n; // +20% slippage buffer
const paymasterData = encodePacked(['address','uint256'], [USDC, maxTokenCost]);
const bundler = createBundlerClient({
chain: base,
transport: http('https://api.pimlico.io/v2/base/rpc?apikey=' + process.env.PIMLICO_KEY),
paymaster: {
async getPaymasterData() {
return {
paymaster: PAYMASTER,
paymasterData,
paymasterVerificationGasLimit: 200_000n,
paymasterPostOpGasLimit: 120_000n
};
}
}
});
// then bundler.sendUserOperation({ account, calls, … }) as usual.
Live example — ethers.js v6
import { ethers } from 'ethers';
const PAYMASTER = '0xb6E8d189285003cF0000388b01BA0C3433ee9f14';
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const provider = new ethers.JsonRpcProvider('https://base-rpc.publicnode.com');
const paymaster = new ethers.Contract(
PAYMASTER,
[ 'function quote(address token, uint256 ethWei) view returns (uint256)' ],
provider
);
// 1. Estimate the userOp's total gas (callGasLimit + verificationGasLimit
// + preVerificationGas + paymasterVerificationGasLimit + paymasterPostOpGasLimit)
// multiplied by maxFeePerGas. Most ERC-4337 SDKs expose this directly.
const expectedGasWei = 300_000n * 3_000_000_000n; // 300k gas × 3 gwei
// 2. Quote the USDC cost.
const quote = await paymaster.quote(USDC, expectedGasWei);
const maxTokenCost = (quote * 12n) / 10n; // 20% slippage buffer
// 3. Build the paymasterAndData suffix (SDK / bundler prepends the
// address + gas limits; you only contribute the trailing 52 bytes).
const paymasterData = ethers.solidityPacked(
[ 'address', 'uint256' ],
[ USDC, maxTokenCost ]
);
// 4. In your bundler client (Pimlico, Stackup, Alchemy AA, biconomy SDK):
// paymaster: PAYMASTER,
// paymasterData: paymasterData,
// paymasterVerificationGasLimit: 200_000,
// paymasterPostOpGasLimit: 120_000,
Validation rules (bundler will check)
The paymaster passes ERC-7562 storage-access rules:
- Reads/writes only its own storage (
tokenConfigs,paused,supportedTokens). - Calls
transferFromon the user's chosen ERC-20, which is sender-associated storage (balances[sender],allowances[sender][paymaster]) — explicitly whitelisted by 4337. - Calls Chainlink price feeds via
staticcallonly — read-only.
If a bundler reports a simulation failure not listed below, open a GitHub issue with the bundler trace.
Common errors
| Error | Cause | Fix |
|---|---|---|
AA33 reverted: TokenNotSupported | Token addr not configured | Use one from the table above |
AA33 reverted: MaxTokenCostExceeded | Your maxTokenCost was below the live quote | Re-quote and bump by ≥20% |
AA33 reverted: BadPaymasterDataLength | paymasterAndData suffix isn't exactly 52 bytes | Re-pack with encodePacked(['address','uint256'], …) |
AA33 reverted: StalePrice | Chainlink feed hasn't updated within the staleness window | Almost always a Chainlink outage — retry in a few minutes |
AA33 reverted: EnforcedPause | Paymaster is paused | Use a different paymaster until ops un-pauses |
AA34 ERC20InsufficientAllowance | User didn't approve the paymaster | Send an approve(paymaster, MAX) userOp first |
AA33 reverted 0x (empty data) | Validation OOG (two Chainlink reads + transferFrom won't fit) | Set paymasterVerificationGasLimit to ≥ 200 000 |
AA40 over verificationGasLimit | Validation gas limit too low | Set paymasterVerificationGasLimit to ≥ 200 000 |
AA50 paymaster postOp ran OOG | postOp gas limit too low | Set paymasterPostOpGasLimit to ≥ 120 000 |
Quoting at scale
If you're embedding the paymaster in an SDK, batch-quote at startup and
refresh every ~5 blocks. The on-chain quote() takes ~70k gas
to read all 5 token configs + Chainlink feeds — fine for one-off,
expensive for per-userOp. Cache the markup constants client-side and only
refresh the Chainlink prices.
We do not throttle reads of quote() — feel free to hammer it.
Trust model
You're trusting:
- The Chainlink ETH/USD + token/USD feeds on Base. Industry standard.
- Us (the paymaster owner) not to pause the contract maliciously while your userOp is in flight. The 1-block pause window between a bundler accepting and submitting your userOp is the only realistic risk; impact is your userOp drops, no funds lost.
- The contract's source matches the deployed bytecode. Both Sourcify
(
exact_match) and BaseScan verification confirm this — open the BaseScan link above and you can read the source there.
You're not trusting us with:
- Your private keys (you sign the userOp).
- Your ETH (we only ever hold the EntryPoint float, never user ETH).
- Your tokens beyond
maxTokenCost(validation reverts if our quote exceeds the bound you signed).
Why this exists
Seneschal runs an Ethereum mainnet block builder and a liquidation searcher. The paymaster is a side-project of the same operator, offered as a public good on Base: a small markup (7–10%) over the Chainlink spot price covers the operator's gas float and Chainlink read costs, with anything beyond that going toward keeping the builder running.
Open-source, MIT-licensed, no SDK lock-in. The contract is roughly 350 lines of Solidity; you can read every line before integrating.
Support & status
- Issues / questions: github.com/Rotwang9000/seneschal-paymaster/issues
- Builder + liquidation stats: stats.seneschal.space (paymaster KPIs land here once v2 has a few hundred userOps of history)
- Operator address:
0x46Ba634261566CF242c853d1f49511f9268ba674. ETH sent here funds the EntryPoint working float — no obligation, but appreciated if our paymaster saved you bundler grief.