Refresh a wallet's token once when two jobs find it expired together - #92
Merged
Merged
Conversation
AnyAPI rotates the refresh token on every use and revokes the connection when an old one is presented again. One user's three projects scanned in the same minute, both scans found the hour-old access token expired, and both spent the stored refresh token. The connection was revoked, and every job for that user has failed with a 400 from the token endpoint since: 57 in a day. The refresh now runs under a row lock on the wallet connection, and whoever waited reads the token the first one stored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
One connected-wallet user (Chesspoly, We Want To Rent for tenants / for agents) has had every
scan,competitor_scanandprofile_reseedfail since 2026-09-19 18:16 withAnyAPI token request failed with status 400: 57 failures. They connected at 17:02; at 18:15, as the first access token expired, two of their scans ran in the same minute and both calledrefreshTokenswith the same stored refresh token. AnyAPI rotates refresh tokens, so the second use reads as replay and the connection is revoked. Every other wallet user has one project, or was lucky.What
walletAccessTokentakesselect … for updateon the user'swallet_connectionsrow before refreshing and re-checks freshness under the lock, so concurrent callers spend the refresh token once and the rest read the stored access token.Testing
tests/walletRefresh.test.tsruns twoclientForUsercalls together against Postgres: 2 refreshes before the change, 1 after. Typecheck and lint clean; the only suite failure is the existingtests/jobs.test.tstimeout.This does not revive the already-revoked connection; that user has to reconnect.
🤖 Generated with Claude Code