Windsurf
Windsurf — prompt credits, flex credits.
Reverse-engineered from app bundle, extension.js, and language server binary. May change without notice.
Windsurf and Antigravity share the same Codeium language server binary and Connect-RPC protocol. The discovery, port probing, and RPC endpoints are virtually identical — the key differences are authentication (Windsurf requires an API key) and the usage model (credits vs fractions).
Variants
The plugin supports two Windsurf variants, probed in this order:
| Variant | App | Bundle ID | --ide_name | App Support dir |
|---|---|---|---|---|
| Windsurf | Windsurf.app | com.exafunction.windsurf | windsurf | ~/Library/Application Support/Windsurf/ |
| Windsurf Next | Windsurf - Next.app | com.exafunction.windsurfNext | windsurf-next | ~/Library/Application Support/Windsurf - Next/ |
Both use the same Codeium language server binary (language_server_macos_arm), same Connect-RPC service, same CSRF auth, and same GetUserStatus RPC. They differ only in:
- Process marker:
windsurfvswindsurf-next(matched via--ide_nameexact value) - SQLite path:
Windsurf/User/globalStorage/state.vscdbvsWindsurf - Next/User/globalStorage/state.vscdb - ideName in metadata:
windsurfvswindsurf-next
Overview
- Vendor: Codeium (Windsurf)
- Protocol: Connect RPC v1 (JSON over HTTP) on local language server
- Service:
exa.language_server_pb.LanguageServerService - Auth: API key (
sk-ws-01-...) from SQLite + CSRF token from process args - Usage model: credit-based (prompt + flex credits, stored in hundredths)
- Billing cycle: monthly (
planStart/planEnd) - Timestamps: ISO 8601
- Requires: Windsurf IDE running (language server is a child process), or signed-in credentials in SQLite (cloud fallback)
Discovery
Same process as Antigravity — same binary, same flags. The distinguishing marker is the --ide_name flag value in the process args.
# 1. Find process and extract CSRF token + version
ps -ax -o pid=,command= | grep 'language_server_macos'
# Windsurf: --ide_name windsurf
# Windsurf Next: --ide_name windsurf-next
# Extract: --csrf_token <token>
# Extract: --windsurf_version <version>
# Extract: --extension_server_port <port> (HTTP fallback)
# 2. Find listening ports
lsof -nP -iTCP -sTCP:LISTEN -a -p <pid>
# 3. Probe each port to find the Connect-RPC endpoint
POST https://127.0.0.1:<port>/.../GetUnleashData → any response wins
# 4. Get API key from SQLite (path depends on variant)
# Windsurf:
sqlite3 ~/Library/Application\ Support/Windsurf/User/globalStorage/state.vscdb \
"SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'"
# Windsurf Next:
sqlite3 ~/Library/Application\ Support/Windsurf\ -\ Next/User/globalStorage/state.vscdb \
"SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'"
# → JSON: { apiKey: "sk-ws-01-...", ... }Port and CSRF token change on every IDE restart. The LS may use HTTPS with a self-signed cert.
Headers (all local requests)
| Header | Required | Value |
|---|---|---|
| Content-Type | yes | application/json |
| Connect-Protocol-Version | yes | 1 |
| x-codeium-csrf-token | yes | <csrf_token> (from process args) |
Endpoints
GetUserStatus (primary)
Returns plan info with credit-based usage for the current billing cycle. Same RPC as Antigravity, but requires metadata.apiKey.
POST http://127.0.0.1:{port}/exa.language_server_pb.LanguageServerService/GetUserStatusRequest
{
"metadata": {
"apiKey": "sk-ws-01-...",
"ideName": "windsurf",
"ideVersion": "<version>",
"extensionName": "windsurf",
"extensionVersion": "<version>",
"locale": "en"
}
}For Windsurf Next, use "ideName": "windsurf-next" and "extensionName": "windsurf-next".
Unlike Antigravity, Windsurf requires metadata.apiKey. The LS uses it to authenticate with the cloud backend internally. Antigravity authenticates via the Google OAuth session instead.
Response (~167KB, mostly model configs)
{
"userStatus": {
"planStatus": {
"planInfo": {
"planName": "Teams", // "Free" | "Pro" | "Teams" | "Free Trial"
"monthlyPromptCredits": 50000,
"monthlyFlexCreditPurchaseAmount": 100000
},
"planStart": "2026-01-18T09:07:17Z",
"planEnd": "2026-02-18T09:07:17Z",
"availablePromptCredits": 50000, // total pool (in hundredths)
"usedPromptCredits": 4700, // consumed (omitted when 0)
"availableFlexCredits": 2679300,
"usedFlexCredits": 175550
}
}
}Field mapping
| Response field | Display | Notes |
|---|---|---|
availablePromptCredits | Total credit pool | Divide by 100 for display (50000 → 500) |
usedPromptCredits | Credits consumed | Divide by 100 |
planStart / planEnd | Billing cycle | ISO 8601, used for pacing |
negative available* | Unlimited | Skip that credit line |
Differences from Antigravity
| Windsurf | Antigravity | |
|---|---|---|
| Auth | API key (sk-ws-01-) required in metadata | No API key needed (CSRF only) |
| Usage model | Credit-based (prompt + flex) | Fraction-based per model (0.0–1.0) |
| Credit units | Stored in hundredths (÷100 for display) | N/A |
| Billing cycle | Monthly (planStart/planEnd) | 5-hour rolling windows per model |
| Version flag | --windsurf_version | N/A |
| Token location | SQLite windsurfAuthStatus → apiKey | Not needed |
| Models shown | Not used (credits are the metric) | Per-model quota bars |
Both use the same Codeium language server binary, same Connect-RPC service, same CSRF auth, same discovery process (ps + lsof + port probe), and same GetUserStatus RPC.
Token Location
SQLite database — path depends on variant:
| Variant | Path |
|---|---|
| Windsurf | ~/Library/Application Support/Windsurf/User/globalStorage/state.vscdb |
| Windsurf Next | ~/Library/Application Support/Windsurf - Next/User/globalStorage/state.vscdb |
| Key | Value |
|---|---|
windsurfAuthStatus | JSON: { apiKey: "sk-ws-01-...", ... } |
Cloud API (fallback)
When the language server is not running, the plugin falls back to Codeium's cloud API using the API key from SQLite.
GetUserStatus (cloud)
POST https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatusHeaders
| Header | Required | Value |
|---|---|---|
| Content-Type | yes | application/json |
| Connect-Protocol-Version | yes | 1 |
Request
{
"metadata": {
"apiKey": "sk-ws-01-...",
"ideName": "windsurf",
"ideVersion": "0.0.0",
"extensionName": "windsurf",
"extensionVersion": "0.0.0",
"locale": "en"
}
}Same metadata shape as the local LS endpoint. Required fields: apiKey, ideName, ideVersion, extensionName, and extensionVersion. Use semver versions (for example "0.0.0"). No CSRF token is required for cloud requests.
Response
Same userStatus.planStatus shape as the local LS response — credit fields in hundredths.
Cloud fallback uses the same API key from SQLite (windsurfAuthStatus).
Plugin Strategy
- Try each variant in order: Windsurf → Windsurf Next
- Discover LS process via
ctx.host.ls.discover()(ps + lsof) with variant-specific marker - Read API key from SQLite (
windsurfAuthStatus) at variant-specific path - Probe ports with
GetUnleashDatato find the Connect-RPC endpoint - Call local
GetUserStatuswithapiKeyand variant-specificideNamein metadata - Build prompt + flex credit lines with billing cycle pacing (÷100 for display)
- If LS probe fails for all variants, call cloud
GetUserStatusatserver.codeium.comwith API key in metadata - If both local and cloud paths fail: error "Start Windsurf or sign in and try again."