Skip to content

Commit b76b977

Browse files
author
S. K. Rotwang MMMMMMMMM
committed
docs: add paymaster integration page
Standalone /paymaster.html (sibling of index.html) covering the live ERC-4337 v0.7 paymaster at 0xb6E8d189285003cF0000388b01BA0C3433ee9f14 on Base — paymasterAndData layout, supported tokens table, gas budget rationale, viem + ethers code samples, ERC-7562 validation rules, error catalogue, and trust model. Cross-linked from index.html, landing.html (apex), and stats.html nav.
1 parent 9beea02 commit b76b977

4 files changed

Lines changed: 368 additions & 0 deletions

File tree

docs/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ <h1>Seneschal Data API</h1>
6060
to see the data without writing a single line.
6161
</div>
6262

63+
<div class="callout callout-info">
64+
<strong>Looking for the ERC-4337 paymaster?</strong>
65+
Pay gas in USDC / EURC / USDT / DAI / cbBTC on Base mainnet via
66+
the Seneschal paymaster &mdash; verified on BaseScan + Sourcify,
67+
smoke-tested via Pimlico, no SDK lock-in.
68+
<a href="https://docs.seneschal.space/paymaster.html"><strong>Paymaster integration docs &raquo;</strong></a>
69+
</div>
70+
6371
<h2>Why this exists</h2>
6472
<p>
6573
Seneschal runs an Ethereum block builder and a vertically-integrated

docs/landing.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ <h2>For everyone else: free liquidation &amp; builder data</h2>
4747
<a href="https://mcp.seneschal.space/health">MCP <code>/health</code></a>
4848
</div>
4949

50+
<h2>For smart accounts: pay gas in stablecoins on Base</h2>
51+
<p>
52+
ERC-4337 v0.7 paymaster on Base mainnet. Stop holding ETH in every
53+
smart account &mdash; pay gas in <code>USDC</code>, <code>EURC</code>,
54+
<code>USDT</code>, <code>DAI</code> or <code>cbBTC</code> at Chainlink-quoted
55+
rates plus a small markup. Verified on BaseScan + Sourcify,
56+
smoke-tested via Pimlico, MIT-licensed.
57+
</p>
58+
<div class="row">
59+
<a href="https://docs.seneschal.space/paymaster.html" style="background:#e7f1ff;border-color:#1462b3;color:#1462b3"><strong>Paymaster integration docs &raquo;</strong></a>
60+
<a href="https://basescan.org/address/0xb6E8d189285003cF0000388b01BA0C3433ee9f14#code">BaseScan source</a>
61+
<a href="https://github.com/Rotwang9000/seneschal-paymaster">GitHub</a>
62+
</div>
63+
5064
<h2>For agents: premium feed via x402</h2>
5165
<p>
5266
A small per-call USDC fee on Base unlocks

docs/paymaster.html

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1">
6+
<title>Seneschal Paymaster — pay gas in stablecoins on Base</title>
7+
<meta name="description" content="ERC-4337 v0.7 paymaster on Base mainnet. Pay transaction gas in USDC, EURC, USDT, DAI or cbBTC instead of ETH. Chainlink-priced, verified on BaseScan + Sourcify, smoke-tested via Pimlico.">
8+
<style>
9+
:root {
10+
--fg: #1a1a1a;
11+
--muted: #6b6b6b;
12+
--accent: #1462b3;
13+
--bg: #fdfdfc;
14+
--code-bg: #f4f4f4;
15+
--border: #e4e4e2;
16+
--good: #1f7a3a;
17+
--warn: #b06900;
18+
}
19+
* { box-sizing: border-box }
20+
html, body { background: var(--bg); color: var(--fg); margin: 0 }
21+
body { font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; padding: 1em }
22+
main { max-width: 52em; margin: 0 auto; padding: 2em 1em 4em }
23+
h1 { font-weight: 600; font-size: 2em; margin: 0 0 .1em }
24+
h2 { font-weight: 600; font-size: 1.3em; margin-top: 2em; padding-bottom: .15em; border-bottom: 1px solid var(--border) }
25+
h3 { font-weight: 600; font-size: 1.05em; margin-top: 1.5em }
26+
p { margin: .6em 0 }
27+
a { color: var(--accent) }
28+
.muted { color: var(--muted); font-size: 0.92em }
29+
code, pre { font-family: ui-monospace, "JetBrains Mono", Consolas, "Liberation Mono", monospace; font-size: 0.92em }
30+
code { background: var(--code-bg); padding: 1px 5px; border-radius: 3px; word-break: break-word }
31+
pre { background: var(--code-bg); padding: 0.8em 1em; border-radius: 4px; overflow-x: auto; line-height: 1.45 }
32+
pre code { background: transparent; padding: 0 }
33+
.kv { background: #fff; border: 1px solid var(--border); border-radius: 5px; padding: 0.6em 1em; margin: 0.6em 0 }
34+
.kv table { margin: 0 }
35+
table { border-collapse: collapse; width: 100%; margin: 0.8em 0; font-size: 0.92em }
36+
th, td { padding: 0.5em 0.7em; border: 1px solid var(--border); text-align: left; vertical-align: top }
37+
th { background: #f7f7f5 }
38+
.callout { background: #fff8e7; border-left: 3px solid #e0b400; padding: 0.6em 1em; margin: 1em 0; border-radius: 0 4px 4px 0 }
39+
.callout-info { background: #eaf3fc; border-left-color: #1462b3 }
40+
.callout-good { background: #ecf6ef; border-left-color: #1f7a3a }
41+
.pill { display: inline-block; font-size: 0.78em; font-weight: 600; padding: 1px 7px; border-radius: 3px; vertical-align: 2px }
42+
.pill.live { background: #ecf6ef; color: var(--good); border: 1px solid #c2e2ce }
43+
.pill.deprecated { background: #fdecec; color: #a23131; border: 1px solid #f0c1c1 }
44+
nav.crumbs { font-size: 0.9em; color: var(--muted); margin-bottom: 0.5em }
45+
nav.crumbs a { color: var(--muted) }
46+
footer { text-align: center; margin-top: 4em; padding-top: 2em; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.9em }
47+
</style>
48+
</head>
49+
<body>
50+
<main>
51+
52+
<nav class="crumbs">
53+
<a href="https://seneschal.space/">Seneschal</a>
54+
&nbsp;·&nbsp; <a href="https://docs.seneschal.space/">Docs</a>
55+
&nbsp;·&nbsp; Paymaster
56+
</nav>
57+
58+
<h1>Seneschal Paymaster <span class="pill live">LIVE</span></h1>
59+
<p class="muted">
60+
Pay ERC-4337 transaction gas in stablecoins on Base mainnet. No ETH
61+
required in your smart account; the paymaster fronts the gas and pulls
62+
USDC/EURC/USDT/DAI/cbBTC from you at Chainlink-quoted rates.
63+
</p>
64+
65+
<div class="callout callout-info">
66+
<strong>Status:</strong> v2 deployed, verified, profitable.
67+
Smoke-tested end-to-end via Pimlico.
68+
Source verified on <a href="https://basescan.org/address/0xb6E8d189285003cF0000388b01BA0C3433ee9f14#code">BaseScan</a>
69+
and <a href="https://sourcify.dev/#/lookup/0xb6E8d189285003cF0000388b01BA0C3433ee9f14">Sourcify</a>
70+
(<code>exact_match</code>). Integration questions:
71+
<a href="https://github.com/Rotwang9000/seneschal-paymaster/issues">GitHub issues</a>
72+
or <code>@Rotwang9000</code> on Discord.
73+
</div>
74+
75+
<h2>Live address</h2>
76+
<div class="kv">
77+
<table>
78+
<tr><th>Paymaster</th><td><code>0xb6E8d189285003cF0000388b01BA0C3433ee9f14</code></td></tr>
79+
<tr><th>Chain</th><td>Base mainnet (chainId 8453)</td></tr>
80+
<tr><th>EntryPoint</th><td><code>0x0000000071727De22E5E9d8BAf0edAc6f37da032</code> (ERC-4337 v0.7)</td></tr>
81+
<tr><th>Source</th><td><a href="https://github.com/Rotwang9000/seneschal-paymaster">Rotwang9000/seneschal-paymaster</a></td></tr>
82+
<tr><th>Verification</th><td><a href="https://basescan.org/address/0xb6E8d189285003cF0000388b01BA0C3433ee9f14#code">BaseScan</a> · <a href="https://sourcify.dev/#/lookup/0xb6E8d189285003cF0000388b01BA0C3433ee9f14">Sourcify</a></td></tr>
83+
<tr><th>Smoke-tested via</th><td>Pimlico (<code>api.pimlico.io/v2/base/rpc</code>)</td></tr>
84+
</table>
85+
</div>
86+
87+
<p class="muted">
88+
The earlier v1 paymaster at
89+
<code>0xC5642B37000F9559b4628D7BA228051010150017</code>
90+
<span class="pill deprecated">deprecated</span> is undeployed (deposit
91+
withdrawn). v1 under-charged for its own postOp gas; v2 fixes that with
92+
an explicit overhead inside <code>_postOp</code> and a <code>validUntil</code>
93+
window inside <code>_validatePaymasterUserOp</code>.
94+
</p>
95+
96+
<h2>Supported tokens</h2>
97+
<table>
98+
<tr><th>Symbol</th><th>Address</th><th>Decimals</th><th>Markup over Chainlink spot</th></tr>
99+
<tr><td><code>USDC</code></td><td><code>0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913</code></td><td>6</td><td>7%</td></tr>
100+
<tr><td><code>EURC</code></td><td><code>0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42</code></td><td>6</td><td>7%</td></tr>
101+
<tr><td><code>USDT</code></td><td><code>0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2</code></td><td>6</td><td>8%</td></tr>
102+
<tr><td><code>DAI</code></td><td><code>0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb</code></td><td>18</td><td>8%</td></tr>
103+
<tr><td><code>cbBTC</code></td><td><code>0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf</code></td><td>8</td><td>10%</td></tr>
104+
</table>
105+
<p>
106+
Prices feed from Chainlink. Stablecoins use the canonical token/USD
107+
feeds with a 25h heartbeat tolerance; cbBTC uses a 2h tolerance.
108+
</p>
109+
110+
<h2>paymasterAndData layout</h2>
111+
<pre><code>[ 0..20) paymaster address (set by EntryPoint)
112+
[20..36) paymasterVerificationGasLimit (recommended &ge; 200 000)
113+
[36..52) paymasterPostOpGasLimit (recommended &ge; 120 000)
114+
[52..72) token address (one of the supported tokens)
115+
[72..104) maxTokenCost (uint256) (max token-native units you'll spend)</code></pre>
116+
117+
<p>
118+
<code>maxTokenCost</code> is a slippage guard. Quote off-chain with
119+
<code>paymaster.quote(token, expectedGasWei)</code> and supply 110-120% of
120+
that to absorb a baseFee uptick between simulation and inclusion.
121+
</p>
122+
123+
<h3>Gas budget rationale</h3>
124+
<p>
125+
Validation reads <strong>two</strong> Chainlink feeds (ETH/USD + token/USD),
126+
executes a <code>transferFrom</code> from the sender, and emits the prefund
127+
context. Empirical gas: ~140–160k. PostOp reads the token feed again,
128+
executes a <code>transfer</code> of the refund, and emits an event.
129+
Empirical gas: ~55–75k.
130+
</p>
131+
<p>
132+
Setting <code>paymasterVerificationGasLimit</code> below 150 000 or
133+
<code>paymasterPostOpGasLimit</code> below 90 000 will cause
134+
Pimlico/Alchemy/Stackup to drop the userOp with <code>AA33 reverted 0x</code>
135+
(out-of-gas inside the paymaster). The contract refunds unused gas via
136+
the postOp refund mechanism, so generous budgets are essentially free.
137+
</p>
138+
139+
<h3>validationData</h3>
140+
<p>
141+
<code>_validatePaymasterUserOp</code> returns a <code>validUntil</code> of
142+
<code>block.timestamp + 5 minutes</code>. Bundlers will therefore drop any
143+
userOp that doesn't get included within that window — important because
144+
the Chainlink-read prices in validation could otherwise drift. If you
145+
simulate a userOp and then sit on it for &gt;5 minutes before submitting,
146+
re-simulate.
147+
</p>
148+
149+
<h2>What the smart account must have</h2>
150+
<p>Before sending the userOp:</p>
151+
<ol>
152+
<li><strong>Token balance</strong>: at least <code>maxTokenCost</code> of the
153+
chosen token in the smart account.</li>
154+
<li><strong>Token allowance</strong>: the paymaster needs
155+
<code>approve(paymaster, maxTokenCost)</code> (or more —
156+
<code>MaxUint256</code> once is fine). The allowance can be granted either:
157+
<ul>
158+
<li>in a <strong>prior userOp</strong> the user signs (one-off "approve
159+
paymaster for unlimited USDC"), or</li>
160+
<li>in a <strong>batched call</strong> within the same userOp (if your
161+
account supports batched calls and your bundler allows
162+
<code>transferFrom</code>-from-sender during validation, which Pimlico
163+
does).</li>
164+
</ul>
165+
</li>
166+
</ol>
167+
<p>
168+
If the allowance or balance is insufficient, the bundler will drop the
169+
userOp during simulation. There's no on-chain failure path.
170+
</p>
171+
172+
<h2>Live example — viem + permissionless</h2>
173+
<pre><code>import { createPublicClient, http, encodePacked, parseAbi } from 'viem';
174+
import { base } from 'viem/chains';
175+
import { createBundlerClient, entryPoint07Address } from 'viem/account-abstraction';
176+
177+
const PAYMASTER = '0xb6E8d189285003cF0000388b01BA0C3433ee9f14';
178+
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
179+
180+
const pub = createPublicClient({
181+
chain: base,
182+
transport: http('https://base-rpc.publicnode.com')
183+
});
184+
185+
const quote = await pub.readContract({
186+
address: PAYMASTER,
187+
abi: parseAbi(['function quote(address,uint256) view returns (uint256)']),
188+
functionName: 'quote',
189+
args: [USDC, 900_000_000_000_000n] // 0.0009 ETH ≈ 300k gas @ 3 gwei
190+
});
191+
const maxTokenCost = (quote * 12n) / 10n; // +20% slippage buffer
192+
const paymasterData = encodePacked(['address','uint256'], [USDC, maxTokenCost]);
193+
194+
const bundler = createBundlerClient({
195+
chain: base,
196+
transport: http('https://api.pimlico.io/v2/base/rpc?apikey=' + process.env.PIMLICO_KEY),
197+
paymaster: {
198+
async getPaymasterData() {
199+
return {
200+
paymaster: PAYMASTER,
201+
paymasterData,
202+
paymasterVerificationGasLimit: 200_000n,
203+
paymasterPostOpGasLimit: 120_000n
204+
};
205+
}
206+
}
207+
});
208+
// then bundler.sendUserOperation({ account, calls, … }) as usual.</code></pre>
209+
210+
<h2>Live example — ethers.js v6</h2>
211+
<pre><code>import { ethers } from 'ethers';
212+
213+
const PAYMASTER = '0xb6E8d189285003cF0000388b01BA0C3433ee9f14';
214+
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
215+
const provider = new ethers.JsonRpcProvider('https://base-rpc.publicnode.com');
216+
217+
const paymaster = new ethers.Contract(
218+
PAYMASTER,
219+
[ 'function quote(address token, uint256 ethWei) view returns (uint256)' ],
220+
provider
221+
);
222+
223+
// 1. Estimate the userOp's total gas (callGasLimit + verificationGasLimit
224+
// + preVerificationGas + paymasterVerificationGasLimit + paymasterPostOpGasLimit)
225+
// multiplied by maxFeePerGas. Most ERC-4337 SDKs expose this directly.
226+
const expectedGasWei = 300_000n * 3_000_000_000n; // 300k gas × 3 gwei
227+
228+
// 2. Quote the USDC cost.
229+
const quote = await paymaster.quote(USDC, expectedGasWei);
230+
const maxTokenCost = (quote * 12n) / 10n; // 20% slippage buffer
231+
232+
// 3. Build the paymasterAndData suffix (SDK / bundler prepends the
233+
// address + gas limits; you only contribute the trailing 52 bytes).
234+
const paymasterData = ethers.solidityPacked(
235+
[ 'address', 'uint256' ],
236+
[ USDC, maxTokenCost ]
237+
);
238+
239+
// 4. In your bundler client (Pimlico, Stackup, Alchemy AA, biconomy SDK):
240+
// paymaster: PAYMASTER,
241+
// paymasterData: paymasterData,
242+
// paymasterVerificationGasLimit: 200_000,
243+
// paymasterPostOpGasLimit: 120_000,</code></pre>
244+
245+
<h2>Validation rules (bundler will check)</h2>
246+
<p>
247+
The paymaster passes
248+
<a href="https://eips.ethereum.org/EIPS/eip-7562">ERC-7562</a>
249+
storage-access rules:
250+
</p>
251+
<ul>
252+
<li>Reads/writes only its own storage (<code>tokenConfigs</code>,
253+
<code>paused</code>, <code>supportedTokens</code>).</li>
254+
<li>Calls <code>transferFrom</code> on the user's chosen ERC-20, which is
255+
sender-associated storage
256+
(<code>balances[sender]</code>,
257+
<code>allowances[sender][paymaster]</code>) — explicitly whitelisted by
258+
4337.</li>
259+
<li>Calls Chainlink price feeds via <code>staticcall</code> only — read-only.</li>
260+
</ul>
261+
<p>
262+
If a bundler reports a simulation failure not listed below, open a
263+
<a href="https://github.com/Rotwang9000/seneschal-paymaster/issues">GitHub
264+
issue</a> with the bundler trace.
265+
</p>
266+
267+
<h2>Common errors</h2>
268+
<table>
269+
<tr><th>Error</th><th>Cause</th><th>Fix</th></tr>
270+
<tr><td><code>AA33 reverted: TokenNotSupported</code></td><td>Token addr not configured</td><td>Use one from the table above</td></tr>
271+
<tr><td><code>AA33 reverted: MaxTokenCostExceeded</code></td><td>Your <code>maxTokenCost</code> was below the live quote</td><td>Re-quote and bump by &ge;20%</td></tr>
272+
<tr><td><code>AA33 reverted: BadPaymasterDataLength</code></td><td><code>paymasterAndData</code> suffix isn't exactly 52 bytes</td><td>Re-pack with <code>encodePacked(['address','uint256'], …)</code></td></tr>
273+
<tr><td><code>AA33 reverted: StalePrice</code></td><td>Chainlink feed hasn't updated within the staleness window</td><td>Almost always a Chainlink outage — retry in a few minutes</td></tr>
274+
<tr><td><code>AA33 reverted: EnforcedPause</code></td><td>Paymaster is paused</td><td>Use a different paymaster until ops un-pauses</td></tr>
275+
<tr><td><code>AA34 ERC20InsufficientAllowance</code></td><td>User didn't approve the paymaster</td><td>Send an <code>approve(paymaster, MAX)</code> userOp first</td></tr>
276+
<tr><td><code>AA33 reverted 0x</code> (empty data)</td><td>Validation OOG (two Chainlink reads + <code>transferFrom</code> won't fit)</td><td>Set <code>paymasterVerificationGasLimit</code> to &ge; 200 000</td></tr>
277+
<tr><td><code>AA40 over verificationGasLimit</code></td><td>Validation gas limit too low</td><td>Set <code>paymasterVerificationGasLimit</code> to &ge; 200 000</td></tr>
278+
<tr><td><code>AA50 paymaster postOp ran OOG</code></td><td>postOp gas limit too low</td><td>Set <code>paymasterPostOpGasLimit</code> to &ge; 120 000</td></tr>
279+
</table>
280+
281+
<h2>Quoting at scale</h2>
282+
<p>
283+
If you're embedding the paymaster in an SDK, batch-quote at startup and
284+
refresh every ~5 blocks. The on-chain <code>quote()</code> takes ~70k gas
285+
to read all 5 token configs + Chainlink feeds — fine for one-off,
286+
expensive for per-userOp. Cache the markup constants client-side and only
287+
refresh the Chainlink prices.
288+
</p>
289+
<p>We do not throttle reads of <code>quote()</code> — feel free to hammer it.</p>
290+
291+
<h2>Trust model</h2>
292+
<p>You're trusting:</p>
293+
<ol>
294+
<li>The Chainlink ETH/USD + token/USD feeds on Base. Industry standard.</li>
295+
<li>Us (the paymaster owner) not to pause the contract maliciously while
296+
your userOp is in flight. The 1-block pause window between a bundler
297+
accepting and submitting your userOp is the only realistic risk; impact
298+
is your userOp drops, no funds lost.</li>
299+
<li>The contract's source matches the deployed bytecode. Both Sourcify
300+
(<code>exact_match</code>) and BaseScan verification confirm this — open
301+
the BaseScan link above and you can read the source there.</li>
302+
</ol>
303+
304+
<p>You're <strong>not</strong> trusting us with:</p>
305+
<ul>
306+
<li>Your private keys (you sign the userOp).</li>
307+
<li>Your ETH (we only ever hold the EntryPoint float, never user ETH).</li>
308+
<li>Your tokens beyond <code>maxTokenCost</code> (validation reverts if our
309+
quote exceeds the bound you signed).</li>
310+
</ul>
311+
312+
<h2>Why this exists</h2>
313+
<p>
314+
Seneschal runs an Ethereum mainnet block builder and a liquidation
315+
searcher. The paymaster is a side-project of the same operator,
316+
offered as a public good on Base: a small markup (7–10%) over the
317+
Chainlink spot price covers the operator's gas float and Chainlink
318+
read costs, with anything beyond that going toward keeping the builder
319+
running.
320+
</p>
321+
<p>
322+
Open-source, MIT-licensed, no SDK lock-in. The contract is roughly
323+
350 lines of Solidity; you can read every line before integrating.
324+
</p>
325+
326+
<h2>Support &amp; status</h2>
327+
<ul>
328+
<li><strong>Issues / questions</strong>:
329+
<a href="https://github.com/Rotwang9000/seneschal-paymaster/issues">github.com/Rotwang9000/seneschal-paymaster/issues</a></li>
330+
<li><strong>Builder + liquidation stats</strong>:
331+
<a href="https://stats.seneschal.space">stats.seneschal.space</a>
332+
(paymaster KPIs land here once v2 has a few hundred userOps of history)</li>
333+
<li><strong>Operator address</strong>:
334+
<code>0x46Ba634261566CF242c853d1f49511f9268ba674</code>.
335+
ETH sent here funds the EntryPoint working float — no obligation, but
336+
appreciated if our paymaster saved you bundler grief.</li>
337+
</ul>
338+
339+
<footer>
340+
<p>Seneschal &middot; <a href="https://seneschal.space/">seneschal.space</a> &middot; <a href="https://docs.seneschal.space/">data API docs</a> &middot; <a href="https://stats.seneschal.space/">live stats</a></p>
341+
</footer>
342+
343+
</main>
344+
</body>
345+
</html>

docs/stats.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ <h1>Seneschal — Public Stats</h1>
136136
<nav>
137137
<a href="https://seneschal.space/">Home</a>
138138
<a href="https://docs.seneschal.space/">Docs</a>
139+
<a href="https://docs.seneschal.space/paymaster.html">Paymaster</a>
139140
<a href="https://api.seneschal.space/v1/stats/overview">Raw JSON</a>
140141
</nav>
141142
</header>

0 commit comments

Comments
 (0)