Stop showing "Connected" for a CoinPay link that cannot send invoices - #553
Merged
Conversation
A worker connected CoinPay, saw a green "Connected" badge on Settings > Connections, and still got "Connect your CoinPay account before sending an invoice" from the same account. Reconnecting did not help, because the two surfaces were asking different questions: the page called any oauth_identities row connected, while the invoice route additionally required wallet:read on the stored token and treated a link without it as absent. The tokens really did lack the scope. The ugig.net OAuth client on CoinPay was registered for openid/profile/email (plus two scopes CoinPay does not define), never wallet:read. That went unnoticed until coinpayportal #257 started intersecting requested scopes with the client's registration, from which point CoinPay filtered wallet:read out of every grant. Every link made since carries openid profile email and can never read wallets, so no amount of reconnecting could fix it from this side. The client registration has been corrected separately; this change is about the contradiction the user was shown. Both surfaces now share one predicate, coinpayLinkCanReadWallets(), so a link that cannot do the job it exists for reports "Reconnect required" with an explanation on the connections page instead of claiming to be connected. The invoice gate is unchanged in behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C5k3DbNSgAqZ1HGjicgLoB
ThreatCrush Security Scan45 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Sep 9, 2026
Reported from the agenticjobs apply flow: an agent tried to invoice $0.25 and ugig answered "Connect your CoinPay account before sending an invoice". CoinPay was connected. It tried again, and got the same sentence. The gate is right and the sentence is wrong. A CoinPay link authorised before ugig needed wallet:read cannot read global wallet addresses, so it cannot back an invoice, and getConnectedCoinpayAccessToken correctly returns null for it. But null carries no reason, so the route reported the one case it could name: not connected. That is an instruction to do something the worker has already done, which is why it loops. They check, see a connection, and try again. #553 fixed this on the connections page, which now says "Reconnect required" rather than "Connected". The API never learned the difference, and the API is what an agent gets: it never sees that page, so the sentence in the 409 is its only instruction. So the reason travels with the token now. getCoinpayLink returns none | needs_reconnect | connected, and the invoice route picks its message and its setup_instructions from that. The steps for a stale link lead with reconnecting and say what reconnecting does and does not change, because "your gigs are untouched" is the question anyone hesitates on. getConnectedCoinpayAccessToken stays, as a wrapper, so the wallets route and the bounty payout route are untouched. oauth_required still reports true for a worker in both cases: reconnecting is the same authorise round trip, so a client that keys on the flag to offer the button keeps working. The new coinpay_link_state field is what a caller should branch on. Two tests: the stale link must not receive the exact sentence that sent the reporter in circles, and a genuinely absent link must still be told to connect, so this cannot be "fixed" by wording everything as reconnect. 2104 tests pass across 219 files. Typecheck clean. Claude-Session: https://claude.ai/code/session_01WxWrsbLuaSaZqQ7FFTrdVW Co-authored-by: Claude Opus 5 (1M context) <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.
The report
A worker registered, connected CoinPay, and tried to invoice $0.25. ugig answered:
Their reply: "CoinPay is connected on my side, but invoicing still failed." Both statements were true, and reconnecting could never have fixed it.
Root cause
Two surfaces asked different questions about the same row:
settings/connectionsrendered Connected for anyoauth_identitiesrow.getConnectedCoinpayAccessToken()additionally requiredwallet:readon the stored token, returningnullwithout it — which the invoice route reports as "not connected".The tokens genuinely lacked the scope. The
ugig.netOAuth client on CoinPay (cp_02526f194fc21f6c659a71b6) was registered foropenid profile email payments:read payments:write— nowallet:read, andpayments:*are not scopes CoinPay defines. That was harmless until coinpayportal #257 (2026-08-15) began intersecting requested scopes with each client's registration; from then on CoinPay silently droppedwallet:readfrom every grant.The data shows the cliff exactly — 61 identities carry
wallet:read(last one 2026-08-16), and all 22 since carry onlyopenid profile email, including the reporter's.The client registration has been corrected on CoinPay (
wallet:readadded), so new and re-authorized links work. This PR fixes the contradiction users were shown in the meantime.This change
One shared predicate,
coinpayLinkCanReadWallets(), used by both surfaces. A link that cannot read wallets now shows Reconnect required with an explanation, instead of a green Connected badge that the invoice form contradicts.The invoice gate's behaviour is unchanged — this only makes the connections page tell the same story.
Verification
tsc --noEmitclean (confirmed non-vacuous with a deliberate probe error).pnpm lint: 0 errors (40 pre-existing warnings in untouched files).openid profile email; after →openid profile email wallet:read.Note for the 22 affected users
Their stored tokens stay scope-less until they re-authorize. They will now be told to, rather than seeing "Connected" and hitting the wall at invoice time.
🤖 Generated with Claude Code
https://claude.ai/code/session_01C5k3DbNSgAqZ1HGjicgLoB