| name | refetch-quota-credentials |
| description | Refresh AI Usage Dashboard provider credentials (Kimi, Claude, Cursor, ChatGPT, MiniMax, Grok) via DevTools or browser automation. Use when quota cards show stale/expired/401, or the user asks to refetch tokens or cookies. |
Refetch Quota Credentials
Credentials live in ~/.quota-watch/ (preferred) or ./credentials/.
Never commit real secrets — only *.template files belong in git.
After updating files, trigger a refresh:
curl -s -X POST http://localhost:3847/api/refresh
Or restart npm start.
Credential files
| Provider | File | Fields |
|---|
| Kimi | kimi.json (preferred) | accessToken, refreshToken (both JWTs) |
| Kimi | kimi-token.txt (legacy) | access JWT only — cannot auto-refresh; expires ~15 min |
| Claude | claude.json | cookies, orgId |
| Cursor | cursor.json | cookie = WorkosCursorSessionToken=... |
| ChatGPT | chatgpt.json | bearer, deviceId, optional sessionCookie |
| MiniMax | minimax.json | token (JWT from _token), groupId |
| Grok | grok.json | sso, optional plan |
chmod 600 credential files after writing.
Manual harvest (always works)
- Sign in to the provider site in a normal browser.
- DevTools → Network.
- Reload or open the usage/billing page.
- Pick an authenticated API request and copy:
Faster agent method (WebBridge, verified 2026-08-10): once on https://www.kimi.com (any app page; membership/quota works) and logged in, skip network sniffing:
JSON.stringify({
accessToken: localStorage.getItem("access_token"),
refreshToken: localStorage.getItem("refresh_token"),
})
Write kimi.json (mode 600). Confirm only HTTP status + non-secret fields (usage %). Decode JWT typ: access must be "access", refresh "refresh".
Kimi pitfalls (do not waste time on these):
| Dead end | Why | Do instead |
|---|
Saving only access_token / kimi-token.txt | Access JWT lifetime is exactly ~15 min (exp - iat = 900s) | Always save refresh_token in kimi.json |
WebBridge network detail on MembershipService | Returns response body only — no Authorization headers | localStorage.access_token + refresh_token |
Decrypt Dia/Chrome cookie kimi-auth | Cookie often holds an already-expired access JWT while UI still looks logged in | localStorage, not the cookie DB |
document.cookie / kimi-auth | Not exposed to page JS | localStorage |
| Using refresh JWT as Bearer on membership API | 401 token type mismatch: got "refresh", want "access" | Bearer = access only; refresh only via AuthService/RefreshToken |
| Cookie-only API call without Bearer | 401 REASON_INVALID_AUTH_TOKEN | Authorization: Bearer <access_token> |
WebBridge bootstrap: if status has running: false, run kimi-webbridge start. If extension_connected: false, wait for the user’s browser with the extension open (poll status) — do not invent Playwright login.
- Any
claude.ai API request → full Cookie header → cookies
- URL like
/api/organizations/{orgId}/usage → orgId
- Request to
/api/usage-summary
- Cookie
WorkosCursorSessionToken=... → entire cookie string value in JSON
(WorkosCursorSessionToken is HttpOnly — copy from Network request headers, not document.cookie)
- Request to
/backend-api/…
Authorization: Bearer <jwt> → bearer
oai-device-id → deviceId
- Optional
__Secure-next-auth.session-token=... → sessionCookie
Faster agent method (WebBridge, verified 2026-08): once on chatgpt.com and logged in,
skip network sniffing entirely:
evaluate: (await (await fetch("/api/auth/session")).json()).accessToken → bearer
evaluate: document.cookie.match(/oai-did=([^;]+)/)[1] → deviceId
- Keep the existing
sessionCookie from the old chatgpt.json if present.
- Verify with
curl https://chatgpt.com/backend-api/wham/usage + Authorization: Bearer …
oai-device-id: … — expect 200. (Note: /api/auth/session and /backend-api/settings/user
return 403 to curl even with a valid token — only trust the wham/usage check.)
- Cookie
_token=<jwt> → token (jwt only)
- Header
x-group-id → groupId
- Find
grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig in Network.
- Copy only the value of the
sso cookie into grok.json; analytics, Stripe, and Cloudflare cookies are not needed.
- The request is gRPC-Web protobuf with an empty message. Verify via
POST /api/refresh/grok and check the Grok card.
Automated harvest (agent)
Prefer the user's already logged-in browser. Fresh Playwright profiles usually fail (2FA/CAPTCHA).
Option A — Kimi WebBridge (real browser session)
- Health check:
~/.kimi-webbridge/bin/kimi-webbridge status
Need running: true and extension_connected: true.
running: false → ~/.kimi-webbridge/bin/kimi-webbridge start
extension_connected: false → user must open the browser with the WebBridge extension; poll status (do not loop forever — ask the user).
- Navigate (new tab) to the provider usage page, session name per site (e.g.
"session":"kimi-token").
- Confirm login via
list_tabs / snapshot (not a sign-in URL). Logged-in Kimi shows app chrome (New Chat, sidebar), not a login form.
- Harvest credentials — prefer site-specific evaluate (see ChatGPT / Kimi sections above) over network capture.
- Network capture is a fallback only:
network start → reload / hit usage API → network list → network detail.
- WebBridge
network detail often omits request headers (Authorization / Cookie). If detail has only body/status, stop and use evaluate / Option B.
- Cursor’s
WorkosCursorSessionToken is HttpOnly — evaluate/cookie JS will fail; use Option B.
- Write
~/.quota-watch/..., chmod 600, call /api/refresh.
close_session when done. Wipe any temp files that held JWTs (/tmp/...).
If the tab is on a sign-in page, ask the user to sign in, then continue.
Option B — Decrypt Chromium cookie DB (HttpOnly)
Used successfully for Cursor in Dia (WorkosCursorSessionToken).
For Kimi, cookie decrypt of kimi-auth is unreliable as a refresh path: the cookie JWT can already be expired while localStorage.access_token is still valid (or freshly rotated). Prefer WebBridge evaluate. Only decrypt Kimi cookies if WebBridge is unavailable and you immediately check JWT exp.
- Locate the browser profile that is logged in (WebBridge extension path often reveals it), e.g.
~/Library/Application Support/Dia/User Data/Default/Cookies
- Copy the SQLite DB to a temp file (browser may lock the original).
- Read
encrypted_value for the target cookie name.
- On macOS, get the safe-storage password from Keychain, e.g.
security find-generic-password -s "Dia Safe Storage" -w
(Chrome uses "Chrome Safe Storage".)
- Derive key: PBKDF2-HMAC-SHA1, password, salt
saltysalt, 1003 iterations, 16-byte key.
- AES-128-CBC decrypt
encrypted_value[3:] when prefix is v10, IV = 16 spaces.
- Strip PKCS#7 padding, then strip the leading 32-byte Chromium host-hash prefix.
- Remaining UTF-8 is the cookie value. Prefix
WorkosCursorSessionToken= for cursor.json.
- Verify with a live request before declaring success (and for JWTs, decode
exp first):
Do not print tokens/cookies in chat logs. Confirm only status codes and non-secret fields (plan name, percentages).
Option C — Playwright
Only if WebBridge is unavailable. Use a persistent context / existing user-data-dir for a profile that is already logged in. Do not attempt password login unless the user explicitly provides credentials and 2FA is handled by them.
Validation checklist
After writing credentials:
POST /api/refresh
GET /api/status → provider status is ok (or ChatGPT tokenExpired banner path)
- Dashboard live-dot shows a time, not
stale / expired
Safety
- Never commit
~/.quota-watch/* or credentials/*.{json,txt} (non-template).
- Never echo full JWTs or cookie strings into the conversation.
- Treat harvested sessions as passwords.