A production-oriented, fully self-hosted ERC-4337 (EntryPoint v0.7) account-abstraction platform: a verifying paymaster you own end to end, a sponsorship backend that decides and signs, and a self-hosted bundler. No dependency on any hosted paymaster or bundler service.
Wallet ──▶ SDK ──▶ Bundler (rundler) ──▶ RPC router ──▶ EntryPoint ──▶ EVM chain
└──▶ Paymaster API ──▶ (policy + signature) ──▶ paymasterAndData
- VerifyingPaymaster (Solidity) — sponsors a UserOperation when an authorised backend signer has attested to it, bound by EIP-712 to one chain and one deployment. Rotatable signer set, emergency pause, two-step ownership.
- Sponsorship backend (TypeScript / NestJS / viem) — a policy engine that decides whether to sponsor and a signature engine that produces the on-chain attestation, behind an authenticated HTTP API. PostgreSQL for durable state, Redis for cross-replica quotas.
- Bundler — rundler (Alchemy's open-source Rust bundler, Apache-2.0/MIT), run on our own infrastructure. Not a hosted service.
- SDK (TypeScript) — framework-agnostic client that drives both the paymaster and the bundler in one call.
Every layer is tested against real infrastructure, not mocks:
- the signature engine's digest is asserted equal to the deployed EntryPoint's own
getHash; maxCostis bracketed against a real EntryPoint's prefund requirement to the wei;- quota atomicity is proven against a real Redis, and the schema against a real PostgreSQL;
- the paymaster is accepted — and an unstaked one rejected — by a real bundler running full trace validation;
- the SDK drives the real backend and real bundler to land an operation on-chain.
Where a test could pass while the system is wrong, that gap is closed by mutation: the load-bearing tests have each been shown to fail when the code they guard is broken. See docs/ARCHITECTURE.md and docs/SECURITY.md.
| Path | What |
|---|---|
| contracts/ | Foundry project: VerifyingPaymaster.sol, TenantPaymaster.sol, deploy script |
| backend/ | NestJS sponsorship + admin API, policy/signature engines, DB, Redis |
| sdk/ | Framework-agnostic TypeScript SDK + runnable example |
| web/ | The public site and the CUSTOMER dashboard (Next.js), on :3000. / explains the product; /dashboard is the signed-in account |
| frontend/ | The OPERATOR console (Next.js), on :3003: live metrics, chains, funding, alerts |
| deploy/ | Multi-chain deploy + verification, the RPC router, an endpoint checker, Helm chart, monitoring config, k6 load test |
| docker-compose.yml | Stack: postgres, redis, rpc-router, bundler (Sepolia), backend |
| docs/ | Indexed at docs/README.md: integration, architecture, security, deployment, operations, runbooks, DR, monitoring, development |
Two Next apps, a bundler and a Grafana all want a port in the low 3000s, so they are assigned rather than left to collide:
| Port | What |
|---|---|
| 3000 | web/ — the public site and /dashboard. It owns 3000 because that is the address a person types |
| 3001 | Bundler (rundler) |
| 3002 | Grafana |
| 3003 | frontend/ — the operator console |
| 3100 | Backend API on the HOST. Inside its container it still listens on 3000, so nothing in Kubernetes changed |
Rundler validates in safe mode, which enforces the ERC-7562 storage rules — the rules that make the
paymaster's stake load-bearing. That requires debug_traceCall with a custom JavaScript tracer,
and that one requirement narrows the field sharply:
| Provider | debug_traceCall |
Custom JS tracer |
|---|---|---|
| QuickNode, Chainstack, self-hosted geth/reth | yes | yes |
| Alchemy, Infura | yes, built-in tracers only | no — -32600 invalid tracer value |
| Free public endpoints | no — -32601 |
no |
The providers that qualify tend to meter hardest. QuickNode's free tier allows 15 requests/second, and rundler issues far more than that — measured at 369 ordinary reads per sponsored operation against 3 trace calls. The endpoint that can validate gets exhausted by traffic that has nothing to do with validating.
deploy/rpc-router/ splits the traffic by method: debug_* and trace_* go to
the tracing provider, everything else to a second endpoint that never needs debug support. Both can
be free tiers. Configure with RPC_URL (tracing) and ALCHEMY_RPC_URL (everything else) in
contracts/.env.
Before committing to a provider, check it:
./deploy/check-rpc.sh https://your-endpointTwo more free-tier settings live in docker-compose.yml and are documented
there: USER_OPERATION_EVENT_BLOCK_DISTANCE (rundler's default searches block 0→latest, which
every provider rejects) and RUST_LOG (rundler logs nothing by default, which makes every
submission failure opaque).
web/ and frontend/ are separate deployments on purpose. The operator console's server holds
PAYMASTER_ADMIN_KEY, which reads and writes every tenant; the customer app authenticates as the
signed-in customer and never has that credential in its process at all.
./start.shThat is the whole thing: it checks the toolchain, installs what is missing, brings up the chain,
deploys the contracts onto it, writes the backend's .env, starts the stack, and starts both web
apps. It is idempotent — a second run redeploys nothing that is still on chain — and it leaves any
port it finds already served alone rather than fighting whoever owns it.
./start.sh status # what is up, and where
./start.sh stop # stop everything it started (data volumes are kept)
./start.sh --skip-checks # do not read Sepolia at boot (offline, or the RPC is rate limiting)
./start.sh --monitoring # add Prometheus, Grafana and the OTel collector
./start.sh --no-ui # backend stack onlyThe stack runs against Ethereum Sepolia, not a local chain. start.sh does not deploy: it
reads the paymaster out of contracts/.env (or the broadcast receipt), checks on chain that it has
code, a deposit and enough stake for the bundler's floor, and generates CHAINS from what it found
rather than from anything transcribed.
Postgres and Redis are remapped automatically if their host ports are taken, because nothing in the stack reaches them that way — the backend uses the compose network. A conflict on the bundler's 3001 is reported instead: that one IS addressed by name from the host.
start.sh deliberately will not do this for you: it spends real ETH, and a boot script should not.
Deploy once, and every later boot reads the result back.
cp contracts/.env.example contracts/.env
$EDITOR contracts/.env # DEPLOYER_KEY (funded), PAYMASTER_OWNER, PAYMASTER_SIGNER
cd contracts
set -a && source .env && set +a
forge script script/DeployPaymaster.s.sol:DeployPaymaster \
--rpc-url "$RPC_URL" --broadcast --verify --private-key "$DEPLOYER_KEY"Budget roughly STAKE_WEI + DEPOSIT_WEI + ~0.007 ETH of gas. The deploy funds and stakes in the
same broadcast, because a paymaster is non-functional without both.
Requires Foundry, Node ≥ 22, PostgreSQL 16, Redis, and the rundler binary.
# 1. Install deps and fetch the pinned, checksum-verified bundler binary
npm install
npm run bundler:fetch --workspace @paymaster/backend
# 2. Point the stack at Sepolia. Both are required — compose refuses to start without them.
# BUNDLER_SIGNER_KEY is an EOA that PAYS for the bundles it submits, in real Sepolia ETH.
export SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
export BUNDLER_SIGNER_KEY=0x...
# 3. Start the bundler and the backend, then run the end-to-end example against Sepolia:
cd sdk && SMART_ACCOUNT=0x... ACCOUNT_OWNER_KEY=0x... API_KEY=... npx tsx examples/sponsor-and-send.ts
# -> success: true; account balance (unchanged, it paid nothing): 0 weiThe smart account is yours to provide — on Sepolia nothing deploys one for you. It need not hold a balance (that is the point of the paymaster), but it must already be deployed or validation fails with AA20.
# CHAINS names the paymaster, so it must exist on chain before the backend starts — the backend
# validates CHAINS at boot and refuses a chain whose EntryPoint has no code.
docker compose up -d # postgres, redis, bundler, backend
cd sdk && npx tsx examples/sponsor-and-send.ts./start.sh writes .env for you; by hand, it needs CHAINS, SPONSORSHIP_SIGNER_KEY,
BOOTSTRAP_API_KEY, SEPOLIA_RPC_URL, BUNDLER_SIGNER_KEY and MIN_STAKE_VALUE.
Add --profile monitoring for Prometheus, Grafana (:3002) and an OTel collector. If the host
already runs Postgres or Redis, set POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env — nothing
inside the stack uses those host ports.
# Contracts (Foundry): 35 tests, incl. a full EntryPoint + SimpleAccount flow and the deploy script.
# 100% line/statement/branch/function coverage, enforced in CI.
cd contracts && forge test
# Backend + SDK: 474 tests, incl. real Postgres, Redis, EntryPoint, a real bundler, 200-way
# concurrency against real Redis, 2,000-case property tests, and a fork of Ethereum mainnet.
# Integration suites self-skip when their infra (rundler binary, postgres, redis) is absent.
npm testEthereum, BNB Smart Chain, Polygon, Arbitrum, Base, Optimism — and any other EVM chain that has the
canonical v0.7 EntryPoint (0x0000000071727De22E5E9d8BAf0edAc6f37da032, identical on every chain).
Adding a chain is configuration only; there is no per-chain code.
This is a substantial, working implementation with an important set of caveats. It is not yet something to point at mainnet without the hardening listed below.
Implemented and verified against real infrastructure:
| Area | State |
|---|---|
| VerifyingPaymaster contract | ✅ 35 tests, mutation-checked, 100% covered |
| Signature engine | ✅ differential vs deployed EntryPoint |
| Policy engine (allow/block/quota/spend caps, hot reload) | ✅ |
| Chain adapter + config-only onboarding | ✅ |
| Authenticated API (API keys + RBAC) | ✅ |
| PostgreSQL persistence + migrations | ✅ vs real Postgres |
| Redis cross-replica quotas | ✅ vs real Redis |
| Admin API + hot reload | ✅ |
| Self-hosted bundler integration | ✅ vs real rundler |
| TypeScript SDK + example | ✅ full-stack e2e |
| Deploy script + local devnet | ✅ runs end to end |
| CI (contracts + backend + SDK) | ✅ |
| KMS-backed signer (key never in process) | ✅ |
| JWT admin auth, request signing, circuit breakers, IP throttling | ✅ |
| Deposit/stake monitor + spend-cap reconciliation | ✅ |
| Metrics, alert rules, Grafana, OTLP tracing, pager sink | ✅ see docs/MONITORING.md |
| Kubernetes / Helm chart | ✅ linted + rendered, with value validation |
| Multi-chain deploy + explorer verification | ✅ exercised against a live node |
| Cross-replica policy propagation + leader lock | ✅ vs real Redis |
| Load, property-based and forked-chain tests | ✅ incl. mainnet fork vs the real EntryPoint |
| Documentation set | ✅ see docs/ |
| Operations console (Next.js) | ✅ reads the real /metrics, /health and admin API |
| Docker Compose stack | ✅ booted end to end, incl. the monitoring profile |
Coverage is measured against the full suite — Postgres, Redis, a test-spawned anvil, rundler and a mainnet fork included — not estimated:
| Lines | Statements | Branches | Functions | |
|---|---|---|---|---|
| Contracts | 100% | 100% | 100% | 100% |
| Backend | 82.86% | 82.86% | 89.14% | 83.85% |
Not built — a different product shape: this is a SINGLE-TENANT paymaster (one operator, one shared deposit per chain, keys minted by that operator), which is what td.md and td2.md specify. The self-service SaaS shape — signup, per-tenant API keys, per-tenant funded balances, billing — needs a tenant model and a credit ledger that do not exist yet. Scoped in docs/REMAINING.md.
Not yet done — deployment decisions rather than missing work:
- Alertmanager routing is not configured; routing, silencing and escalation are per-deployment.
- Two alert-rule thresholds ship as
TUNEplaceholders that need real traffic to set.
Full detail, including what was deliberately not built and why, is in docs/REMAINING.md.
Start at docs/README.md — it routes by audience, because all but one of these are written for the person RUNNING the platform rather than the person integrating with it.
| docs/README.md | Index, and the five things that surprise people |
| INTEGRATION.md | For customers: sign up, fund, mint a key, call the SDK |
| ARCHITECTURE.md | Structure, and the reasoning behind it |
| SECURITY.md | Security guide and threat model |
| DEPLOYMENT.md | Production deployment, in order, with a checklist |
| OPERATIONS.md | Policies, keys, chains, funding, maintenance, upgrades |
| RUNBOOKS.md | Incident procedures |
| DISASTER-RECOVERY.md | What survives what, and what does not |
| MONITORING.md | Metrics, alerts, tracing, paging |
| DEVELOPMENT.md | Working on the codebase |
| openapi.yaml | API reference |
MIT (this project's own code). One licensing consideration to resolve before commercial
distribution: VerifyingPaymaster inherits BasePaymaster from
eth-infinitism/account-abstraction, which
is GPL-3.0, so the deployed contract is a derivative work under that license. This is common
across ERC-4337 paymasters but is a real obligation, not a footnote — get counsel before shipping.
rundler (Apache-2.0) is run as a separate process, not linked, so it does not carry this concern.