OAuth troubleshooting
"invalid_redirect_uri"
The redirect_uri you sent to /authorize doesn't match any entry in the agent's oauthClient.redirect_uris (which is mirrored from agents.redirectUris).
Check:
curl https://api.fleapo.ai/agents/by-slug/<slug> | jq '.data.redirectUris'
The string must match exactly — including protocol, host, port, and path. https://app.example.com/cb ≠ https://app.example.com/cb/.
Update via POST /agents/:id with the corrected list.
"code_challenge missing" / "PKCE required"
You're not sending PKCE. The marketplace requires code_challenge (S256). Better Auth sends it automatically; if you're rolling your own:
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(sha256(codeVerifier));
// include code_challenge + code_challenge_method=S256 in /authorize
// include code_verifier in /token
"invalid_grant" at /token
The most common causes:
- Reusing an already-used
code(codes are single-use). - Mismatched
code_verifier(PKCE didn't match). - The code expired (codes are short-lived — minutes, not hours).
Solution: restart the OAuth flow from /authorize.
"Refresh token reuse detected"
You used an old refresh token after it was rotated. better-auth invalidates the entire chain. The user must sign in fresh.
Why this might happen:
- Two processes are sharing the same refresh token and both refreshed concurrently.
- A snapshot/restore brought back stale tokens.
- An attacker actually got your token.
Investigate audit_logs / PostHog for the signature.
"Callback succeeds but session doesn't persist"
The agent backend exchanged the code, got tokens, hit userinfo — but the user lands on / and appears unauthenticated.
Almost always a cookie issue:
- Cross-origin agent (
api.my-agent.com↔app.my-agent.com) needsSameSite=None; Secure. - Browser silently drops cookies that don't match SameSite policy.
- Open DevTools → Application → Cookies → see if the cookie is set on the agent backend's domain.
"JWT verification fails with 'no signing key'"
Your JWKS cache is stale. The marketplace rotates keys; if you cached a static set you'll miss new ones.
Use createRemoteJWKSet(new URL(jwksUrl)) from jose. It auto-refreshes.
"Token has expired"
Your access_token is past 10h TTL. Refresh:
POST /api/auth/oauth2/token
grant_type=refresh_token
refresh_token=...
The response includes a new refresh token. Save it and discard the old.
"marketplace:agentTiers claim is wrong"
The claim is a hint, not source of truth. It's frozen at token issue time. If the user upgraded after token issue, the claim is stale.
Always call GET /me/subscriptions/:agentId for live state.
"User can't sign out of the marketplace"
POST /api/auth/sign-out from any UI that has a session cookie. The browser must include credentials: 'include'.
For RP-initiated logout from an agent:
GET ${MP}/api/auth/oauth2/endsession?id_token_hint=<token>&post_logout_redirect_uri=<your-page>
The user is bounced to the marketplace, signed out, then back to the redirect URI.
"Consent screen shows up unexpectedly"
For first-party agents the OAuth client has skipConsent: true. If you're seeing /consent, it's because:
- The client was created without
skipConsent: true(audit the row). - A new scope was requested that the user hasn't granted before.
Re-provision the client with the desired skipConsent setting.