Developer guide

RPC providers and chain infrastructure

4 min read

Integration incidents whose root cause sits in a third-party RPC endpoint, node provider, indexer or other chain infrastructure ChatterPay B2B does not operate. An incident is grouped here only once its root cause is confirmed to be outside our codebase; if the root cause turns out to be ours, the fix is a normal code change and gets no entry.

Every entry states, in order: Symptom (the externally observable failure), How it was detected (concrete reproduction or evidence — a transaction hash, a log line, a test run), Root cause (the exact third-party mechanism), Relation to our platform (the file or function exposed to that behavior), Third-party reference (a citable official source) and Fix or mitigation (what changed on our side, and why).

KB-001 — Read-after-write lag on the Base Sepolia public RPC causes AA33 / AccountNotSponsored

Symptom handleOps on the ERC-4337 Paymaster reverted with AA33 reverted (or OOG) on Base Sepolia immediately after setAccountSponsorship(account, true) had been confirmed, even though Paymaster ownership, deposit, and the sponsoredAccounts allowlist were all correctly configured. The same flow succeeded reliably on Arbitrum Sepolia with identical code and configuration, which was the first signal that the cause was environmental rather than logical.

How it was detected The EntryPoint's FailedOpWithRevert(opIndex, reason, inner) event was being logged with only the generic reason string; the inner field, which carries the Paymaster's own custom error, was being discarded. Decoding inner against the Paymaster ABI turned the opaque AA33 reverted (or OOG) into AA33 reverted: AccountNotSponsored(0x984E5b8826fF3288fddB3f3Fd3671b55F78f5806) — a specific, actionable error instead of a generic wrapper.

A live reproduction against the real Base Sepolia network, not a local simulator or a mock, confirmed the sequence: a fresh wallet's first handleOps call failed with the decoded AccountNotSponsored, even though a direct sponsoredAccounts(...) read moments later already returned true. Re-running the identical operation seconds later, with no code or configuration change, succeeded. Base Sepolia's gas price at test time (0.006 gwei) was also well below Arbitrum Sepolia's (0.02 gwei) and far under the Paymaster's fixed cost cap (~16.7 gwei), which ruled out a gas-cap explanation. Transaction evidence: 0x66a7da8e4a2ff4d93839cf6ad17a89035f4ef0673df0e3e83131d7f641a389af on Base Sepolia.

Root cause A read-after-write consistency lag on the shared, public Base Sepolia RPC endpoint (sepolia.base.org): the node that served the handleOps call moments after setAccountSponsorship had not yet replicated the state that the node serving waitForTransactionReceipt already treated as confirmed. The Paymaster and EntryPoint contracts were correctly configured throughout; no OP-stack-specific gas mechanic (blob gas, L1 data fee) was involved, since validatePaymasterUserOp computes maxCost purely from L2 gas units and never reads an L1 fee input.

Relation to our platform chatterpay-b2b-api, src/infrastructure/blockchain/chain-adapter.ts: the ensureSponsored() helper that calls setAccountSponsorship, and the subsequent handleOps call, both assumed a single confirmed receipt from one RPC call was enough to guarantee the same state would be visible to the next call against the same public endpoint.

Third-party reference Base's official documentation states that its public RPC endpoints, including sepolia.base.org, are "rate-limited and not suitable for production traffic" and recommends a dedicated node provider for production workloads — confirming these are shared, best-effort, multi-node endpoints rather than a single consistent node: Connecting to Base — Base Documentation. Base does not publish a read-after-write consistency SLA for the public endpoint; the specific timing of the lag was established by our own reproduction (transaction evidence above), not by a third-party claim.

Fix or mitigation

  • ensureSponsored() now polls (waitUntilConsistent, 5 attempts / 500 ms) and re-reads on-chain state (sponsoredAccounts / getDeposit) until it matches the just-confirmed write, instead of trusting a single receipt that may have come from a different node than the one serving the next call.
  • decodePaymasterRevert decodes the EntryPoint's inner field against the four Paymaster custom errors (MaxCostExceeded, AccountNotSponsored, SponsorshipDisabled, SponsorshipCapExceeded) instead of surfacing the generic AA33 wrapper.
  • classifyAaReason marks the decoded AccountNotSponsored cause under AA33 as retryable, since it is transient by design, while the other three decoded causes remain non-retryable, because they reflect real state or policy that a retry would not change.
  • Covered by test/paymaster-revert-decoding.test.ts (7 tests): decoding of the four Paymaster causes, the inner-decode fallback, the no-inner FailedOp case, and retryability classification.