koala73/worldmonitor · warning

buildAuthHeaders: free-tier context has no credentials — a…

Error message

buildAuthHeaders: free-tier context has no credentials — a free-tier tool must not call a credentialed downstream

What it means

requireSignedInUserId(action) is the billing service's shared guard: it reads getCurrentClerkUser()?.id and throws `Sign in to ${action}.` (e.g., 'Sign in to claim Pro activation.') when no Clerk session exists. The action string names the exact billing operation, so the message tells the developer which gated flow fired. No billing mutation or Convex call is attempted.

Solutions

  1. Sign in and retry the billing action
  2. Ensure Clerk is fully loaded before invoking billing flows
  3. Gate billing buttons on auth state so the call never fires signed-out
  4. Check ClerkProvider/publishable key configuration if the user should be signed in

Example fix

// before
const status = await claimProActivationPresentation(activationKey, claimNonce);
// 'Sign in to claim Pro activation.'

// after
if (!getCurrentClerkUser()?.id) {
  openSignIn();
  return;
}
const status = await claimProActivationPresentation(activationKey, claimNonce);
Defensive patterns

Strategy: validation

Validate before calling

if (!getCurrentClerkUser()?.id) { openSignIn(); return; }
const outcome = await claimProActivationPresentation(activationKey, claimNonce);

Type guard

const hasActiveClerkUser = (u: { id: string } | null | undefined): u is { id: string } =>
  typeof u?.id === 'string' && u.id.length > 0;

Try / catch

try {
  await billingOperation();
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Sign in to ')) openSignIn();
  else throw e;
}

Prevention

When it happens

Trigger: Invoking any billing operation routed through requireSignedInUserId (such as claimProActivationPresentation) while signed out, after session expiry, before Clerk loads, or with ClerkProvider misconfigured.

Common situations: Billing UI rendered from cached state after logout; expired sessions on long-lived dashboards; dev builds without Clerk env vars.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21). Data as JSON: /api/errors/d3a74afb70c64ba2. Report an issue: GitHub.

Appendix: source

Thrown at api/mcp/auth.ts:138

  mcpProMinRatelimits.set(perMinute, limiter);
  return limiter;
}

function getMcpAnonRatelimit(): Ratelimit | null {
  if (mcpAnonRatelimit) return mcpAnonRatelimit;
  const url = process.env.UPSTASH_REDIS_REST_URL;
  const token = process.env.UPSTASH_REDIS_REST_TOKEN;
  if (!url || !token) return null;
  mcpAnonRatelimit = new Ratelimit({
    redis: new Redis({ url, token, retry: false }),
    limiter: Ratelimit.slidingWindow(60, '60 s'),
    prefix: 'rl:mcp:anon',
    analytics: false,
  });
  return mcpAnonRatelimit;
}

/**
 * Build the Authorization header set for a downstream `_execute` fetch.
 *
 *   - env_key → `X-WorldMonitor-Key: <apiKey>` (legacy operator keys).
 *   - pro / user_key → `X-WM-MCP-Internal: <ts>.<sig>` + `X-WM-MCP-User-Id`.
 *               Signature binds method+pathname+queryHash+bodyHash+userId.
 *               Identity-resolved dashboard keys use this path so the
 *               gateway does not increment the shared daily account meter
 *               a second time after MCP already reserved the tool weight.
 *
 * `body` MUST be the EXACT bytes the caller passes to `fetch()` so the
 * signed payload matches the wire bytes. For JSON, pre-stringify on the
 * caller side and pass the same string here.
 */
export async function buildAuthHeaders(
  context: McpAuthContext,
  method: string,
  url: string,
  body: BodyInit | null | undefined,

View on GitHub (pinned to 7d06c8633d)