Skip to content

fix(solana): fail fast when the payer has no USDC token account#23

Merged
VickyXAI merged 2 commits into
mainfrom
fix/fail-fast-on-invalid-account-data
Jul 15, 2026
Merged

fix(solana): fail fast when the payer has no USDC token account#23
VickyXAI merged 2 commits into
mainfrom
fix/fail-fast-on-invalid-account-data

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

What

Client half of the x402 verify retry-storm fix. Gateway half: BlockRunAI/blockrun-sol#48 — that one must land and deploy first, since it's what starts returning the field this PR reads.

Why

A wallet whose USDC token account was never created fails simulation with InvalidAccountData, which the gateway's coarse invalidReason collapses to transaction_simulation_failed. That pattern is deliberately absent from _UNRECOVERABLE_PAYMENT_PATTERNS because it usually is recoverable under concurrent load — so the retry built for load spikes fired on a wallet that could never pay, burning all 5 attempts.

Each of those cost the gateway its own 4 verify retries: one request became up to 20 facilitator calls, and one broke wallet looked like a ~260-attempt client-side storm to our facilitator partner. It was ~13 real requests.

How

  • build_payment_rejected_error folds invalidMessage into the error message (the classifiers only ever see str(exc)) and keeps it on .response. Bounded to 256 chars, matching the existing details handling — same provenance: a facilitator error string, not upstream text.
  • _is_unrecoverable_payment_error also matches normalized invalidMessage patterns, so InvalidAccountData, invalid account data and invalid_account_data all hit one pattern.

The asymmetry is load-bearing

Blockhash messages stay OUT of the unrecoverable list on purpose. The gateway fails fast on BlockhashNotFound because retrying the same dead header is futile. Here the opposite holds: re-signing with a fresh blockhash is exactly what this retry does, and it fixes it.

The two repos classify blockhash errors oppositely and both are correct. Anyone later "harmonizing" the two lists will either reintroduce the storm or break recovery — hence the comment pointing at the other repo.

Not yet confirmed

The facilitator's exact spelling of invalidMessage is unverified — normalization covers the plausible variants, and the gateway's retry log (#48) now prints message= even when unclassified so the real string surfaces in prod. Worth confirming from logs before trusting the patterns.

Impact

With both halves, verify calls per doomed request: 20 → 1.

Test

  • 10 new tests in tests/unit/test_invalid_message_fail_fast.py, written first — 3 failed for exactly the reasons being fixed; the 7 that passed pin the asymmetry and back-compat that already held.
  • Full suite: 310 passed / 15 skipped. ruff clean.

Follow-up

Releasing this needs a blockrun-litellm dependency floor bump.

1bcMax added 2 commits July 15, 2026 11:32
Companion to blockrun-sol's invalidMessage classification. The gateway
now returns the x402 facilitator's `invalidMessage` next to the coarse
`invalidReason`; read it and stop retrying payments that can never pass.

A wallet whose USDC token account was never created fails simulation
with InvalidAccountData, which invalidReason collapses to
transaction_simulation_failed. That pattern is deliberately absent from
_UNRECOVERABLE_PAYMENT_PATTERNS because it usually IS recoverable under
concurrent load — so the retry meant for load spikes fired on a wallet
with no funds, burning all 5 attempts. Each of those cost the gateway
its own 4 verify retries: one request became up to 20 facilitator calls,
and one broke wallet looked like a ~260-attempt client-side storm.

build_payment_rejected_error folds invalidMessage into the error message
(the classifiers only ever see str(exc)) and keeps it on .response.
Bounded to 256 chars, matching the existing `details` handling — same
provenance, a facilitator error string rather than upstream text.

Matching normalizes away non-alphanumerics so InvalidAccountData,
"invalid account data" and invalid_account_data all hit one pattern; the
facilitator's exact spelling is not yet confirmed.

Blockhash messages stay OUT of the unrecoverable list on purpose. The
gateway fails fast on them because retrying the SAME dead header is
futile; here the opposite holds — re-signing with a fresh blockhash is
what this retry does, and it fixes them.

With both halves, verify calls per doomed request go 20 → 1.
@VickyXAI
VickyXAI force-pushed the fix/fail-fast-on-invalid-account-data branch from dfb1c89 to ccd6fc0 Compare July 15, 2026 19:01
@VickyXAI
VickyXAI merged commit 09f5c6c into main Jul 15, 2026
6 checks passed
VickyXAI added a commit to BlockRunAI/blockrun-litellm that referenced this pull request Jul 15, 2026
)

Picks up BlockRunAI/blockrun-llm#23, which stops the SDK retrying payments
a wallet can never make. Below this floor an unfunded payer burns all 5
payment attempts, each costing the gateway 4 verify retries — 20
facilitator calls per doomed request, which is how one wallet with no USDC
token account surfaced as a ~260-call storm.

A floor rather than a preference: a pinned older SDK reintroduces the
amplification in full. Base-chain users are unaffected, so the base
blockrun-llm floor stays put.

The fix also needs the gateway half (BlockRunAI/blockrun-sol#48, deployed)
to return the invalidMessage the SDK classifies on; against an older
gateway the SDK degrades to the previous retry behaviour rather than
misbehaving.

Unblocked: blockrun-llm 1.6.1 published to PyPI, so blockrun-llm[solana]>=1.6.1
now resolves (verified: blockrun-llm-1.6.1 with solana-0.39.0, x402-2.15.0).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant