pi-antigravity-rotator
v1.10.1
Published
Multi-account rotation proxy for Google Antigravity with per-model routing, real-time quota tracking, and infringement detection
Maintainers
Readme
Pi Antigravity Rotator
Multi-account rotation proxy for Google Antigravity. Distributes API usage across multiple Google accounts with per-model routing, real-time quota tracking, automatic token management, and infringement detection.
Features
- Per-model routing -- Each model (Gemini Pro, Flash, Claude) routes to its own active account independently. Multiple agents using different models won't interfere with each other.
- Real-time quota monitoring -- Polls Google's quota API every 5 minutes to track remaining usage per model per account
- Per-model timer tracking -- Timer classification (
fresh/7d/5h) is evaluated per model using each model's actualresetTimefrom the quota API, not a per-account estimate - Smart rotation -- Rotates only the specific model whose quota dropped, leaving other models on their current accounts
- Infringement detection -- On 403 with infringement/abuse/suspension keywords, the account is immediately flagged and excluded from routing
- Safer 429 handling -- On provider
429, stops the current request and avoids cascade-burning sibling accounts - Concurrency guardrails -- Limits each account to one in-flight request by default to avoid bursty pressure
- Operator fresh-window controls -- You can block new
freshwindow starts globally, then selectively allow specific accounts to override that policy - Protective pause -- Pauses all routing for several hours after serious ToS/abuse-style flags so the rest of the pool is not burned
- Token auto-refresh -- Tokens are refreshed automatically before expiry; no manual management
- Endpoint cascade -- Tries daily, autopush, and prod API endpoints for resilience
- Pro Family Advisor -- Scans your account pool and alerts you if there are major imbalances (like some accounts never getting used because of routing bias), giving you actionable steps to optimize token distribution
- Advanced Telemetry & Statistics -- Track exactly how much USD you save compared to a paid API plan, predict quota depletion with the Forecast grid, view Latency tracking (p50/p95), and explore 60-day historical usage heatmaps
- Web dashboard -- Real-time view of model routing table, per-account quota bars with per-model timers, and flagged account alerts
- Auto-update notifications -- The dashboard checks npm for new releases every 30 minutes and shows a banner with one-click update when a newer version is available
- Broadcast notifications -- Operator-controlled announcements and alerts delivered directly to the dashboard
- State persistence -- Survives restarts; routing assignments, per-model request counters, cooldowns, and flags are saved to disk
Quick Start
Option A: Install from npm
npm install -g pi-antigravity-rotator
# Add your first account
pi-antigravity-rotator login
# Start the proxy
pi-antigravity-rotator startOption B: Clone from source
git clone https://github.com/tuxevil/pi-antigravity-rotator.git
cd pi-antigravity-rotator
npm install
# Add your first account
npm run login
# Start the proxy
npm startAdding Accounts
Run npm run login once per Google account:
- A Google OAuth URL is printed to the terminal -- open it in your browser
- Complete the sign-in and grant permissions
- The browser redirects to a
localhostURL that won't load -- this is expected - Copy the full URL from the browser's address bar and paste it into the terminal
- If project discovery fails, open that same account in Antigravity IDE, send one message, then rerun login
The tool automatically:
- Creates or updates
accounts.jsonwith the account credentials - Configures
~/.pi/agent/models.jsonto pointgoogle-antigravityat the proxy - Configures
~/.pi/agent/auth.jsonwith proxy-managed credentials
Re-running with the same email updates the existing entry.
Activation rule
Some accounts do not expose a discoverable projectId until they are used once in the Antigravity IDE.
If login fails at project discovery:
- Open that exact Google account in Antigravity IDE.
- Send one message.
- Rerun
npm run login.
Dashboard
After starting the proxy, open http://localhost:51200/dashboard or http://<your-server-ip>:51200/dashboard from any machine on the same network (the proxy binds to 0.0.0.0).
The dashboard shows:
- Top Status & Controls -- Real-time routing state, uptime, requests, and PII masking toggle.
- Pro Family Advisor & Dual-Window Tracking -- Advanced logic that tracks and compares both Pro and Free quota windows simultaneously. The Advisor analyzes cumulative quota to suggest mathematical upgrades/downgrades.
- Token Usage & Savings -- Interactive chart (
1h,2h,4h,8h,12h,1d,7d,1m) showing token consumption by model, with estimated USD savings andCSV/JSONexport options. - Activity Heatmap -- 60-day responsive GitHub-style contribution grid showing request intensity hour by hour.
- Latency (p50/p95) -- Real-time median and 95th percentile tracking for Time-to-First-Byte (TTFB) and Total Duration per model.
- Quota Forecast -- Predictive modeling showing when each model's quota will run out based on the current requests/hour burn rate.
- Searchable Request Log -- Live feed of the last 200 requests with exact timestamps, models, masked accounts, status codes, and latency.
- Account Cards -- Sorted by total quota. Shows status (
active,ready,cooldown,flagged,disabled), dual-window trackers, quota bars with timers, and precise error messages. - Operator Panels -- "Attention Needed" summaries for quarantined accounts and a real-time event feed of rotator actions.

How It Works
Proxying
Pi Agent 1 (Gemini Pro) ---> localhost:51200 ---> Account A
Pi Agent 2 (Claude) ---> localhost:51200 ---> Account C
Pi Agent 3 (Flash) ---> localhost:51200 ---> Account A
(this proxy) (per-model routing)- Pi sends a request to
localhost:51200with a model name in the body - The proxy resolves the model to a quota key (e.g.,
gemini-3.1-pro) - The best available account for that specific model is selected
- The
Authorizationheader andprojectfield are swapped with real credentials - The request is forwarded (trying daily, autopush, then prod endpoints)
- The SSE response streams back to pi transparently
Per-Model Account Selection
Each model maintains its own active account. When the proxy needs to rotate a model, it picks the next account using a priority system:
| Priority | Badge | Condition | Rationale |
|----------|-------|-----------|-----------|
| 1 (first) | 5h | Short reset window is already active for this model | Drain short-window quota before it recharges |
| 2 | 7d | Long reset window is already active for this model | Already ticking, so it is still worth using |
| 3 (last) | fresh | No active reset window is known for this model yet | Save untouched quota for later if other timed pools exist |
Within the same priority tier, the account with the most remaining quota for that model wins. If multiple accounts tie on priority and quota, rotation advances circularly from the current account so equal candidates share traffic instead of always favoring the first configured match.
Timer meanings:
fresh-- no futureresetTimeis currently reported for that model on that account. In practice, this means no active reset window is visible in quota polling yet. The dashboard labels this asidleto avoid implying that it is automatically safe to start.5h--resetTimeis less than 6 hours away.7d--resetTimeis 6 hours or more away.
Rotation Triggers
Three mechanisms trigger rotation, scoped to the specific model:
Quota-based (primary) -- Polls the Google quota API every 5 minutes. When a model's remaining quota drops by
rotateOnQuotaDroppercentage points (default: 20%), that model rotates to the next account. Other models stay on their current accounts.Request-count (fallback) -- Before forwarding a request, the rotator checks how many requests the current account has already served for that specific model and rotates once it reaches
requestsPerRotation(default: 5). Per-model counters are persisted so restarts do not reset the threshold. By default this fallback is only used when quota data for that model is still unknown; setuseRequestCountRotationWhenQuotaUnknownOnlytofalseto keep request-count rotation active even when quota telemetry exists. If the threshold is reached but every replacement account is cooling down, flagged, disabled, busy, blocked by fresh-window policy, or out of quota for that model, the rotator stays on the current healthy account instead of returning503.429 containment (reactive) -- On provider rate limit, the account is marked exhausted with a parsed retry cooldown and the current request stops. Repeated unique-account
429s trip project/model and model-wide circuit breakers so retries cannot burn through the pool.
Fresh Windows
The quota polling API only exposes one visible quotaInfo block per model. If a model has no visible resetTime, the rotator classifies it as fresh internally and the dashboard shows it as idle.
Operationally, idle means:
- no timer window is currently visible for that model in quota polling
- starting that account may open a new quota window
- because the provider does not expose all parallel buckets explicitly, the rotator cannot guarantee ahead of time whether that new visible window will behave like a short
5hopportunity or a longer7drunway
For that reason, the rotator has two operator controls:
- a global fresh-window toggle that blocks opening new
idlewindows by default - a per-account override that allows specific accounts to ignore the global block when you intentionally want them available
When fresh-window starts are blocked:
- visible
5htimers still have highest priority - visible
7dtimers are still used normally idleaccounts are held back unless you explicitly enable their per-account override
Account Protection
The proxy detects blocked/suspended accounts at three levels:
Quota API check (initial poll + every poll) -- If the quota API returns
401or403, the account is immediately flagged.API 401 (on request) -- If the prod endpoint rejects the token with
401 UNAUTHENTICATED, the account is flagged.API 403 (on request) -- If the response body contains enforcement keywords such as
infring,suspend,abus,terminat,violat,banned,policy,forbidden, orverif, the account is flagged.
Flagged accounts are immediately excluded from all model routing. If the reason looks serious enough (for example ToS, abuse, infringement, suspension, or ban language), the rotator also enables a global protective pause that stops all routing for protectivePauseMs (default: 6 hours). The dashboard shows a red FLAGGED badge with the error message and quarantine guidance. Flagged accounts are intentionally kept out of rotation until the provider explicitly restores access.
Cooldown Management
- Cooldowns are capped at 30 minutes max
- Stale cooldowns from previous sessions are capped on startup
- When every non-flagged account is cooling down, the routing state becomes
cooldown_wait - The dashboard shows why routing is waiting, how long until the next retry window, and which accounts are cooling down
- Quota-based rotation only triggers if a healthy account is available; the proxy won't rotate away from a working account if there's no better alternative
Error Handling
- 429 (rate limit) -- account is marked exhausted with cooldown; request stops and returns
429/Retry-Afterto force client backoff - 401 -- account is flagged and excluded from routing
- 403 with enforcement keywords -- account is flagged and may trigger protective pause
- 503 (no capacity) -- returned directly to the agent when all healthy accounts are cooling down, busy, flagged, or disabled
- 5xx (other server errors) -- account error counter incremented, rotates to next
Dashboard Visibility
The dashboard is intended to replace day-to-day journalctl digging for normal operations. The top status panel shows:
- The current routing state (
healthy,cooldown_wait,busy,paused,stopped) - The exact stop or wait reason
- The next retry window when cooldowns are active
- Protective pause remaining time and the provider signal that triggered it
- The global fresh-window policy and a button to block or allow new
idlewindow starts - Pool counts for available, ready, active, cooldown, busy, flagged, disabled, and error accounts
- An
Attention Neededsection summarizing flagged, cooling, disabled, and error accounts - A recent event feed with the latest rotator/proxy incidents that led to the current state
Configuration
Config files (accounts.json, state.json) are stored in ~/.pi-antigravity-rotator/ by default. Override with:
# Environment variables
export PI_ROTATOR_DIR=/path/to/config
export PI_ROTATOR_QUOTA_USER_AGENT="antigravity/1.107.0 darwin/arm64"
# Optional: require this token for dashboard/API access. If unset, legacy open access is preserved.
export PI_ROTATOR_ADMIN_TOKEN="change-me"
# Optional: max accepted proxy request body size in bytes. Default: 26214400 (25 MiB).
export PI_ROTATOR_MAX_BODY_BYTES=26214400
# Optional: log verbosity. One of debug, info, warn, error, silent. Default: info.
export PI_ROTATOR_LOG_LEVEL=info
# Optional override for Antigravity UA version used by quota fetches
export PI_AI_ANTIGRAVITY_VERSION=1.107.0
# Or CLI flag
pi-antigravity-rotator start --config-dir /path/to/configaccounts.json is created automatically by the login command.
Login now fails if Google does not return a project ID. No shared fallback.
{
"proxyPort": 51200,
"requestsPerRotation": 5,
"rotateOnQuotaDrop": 20,
"quotaPollIntervalMs": 300000,
"maxConcurrentRequestsPerAccount": 1,
"maxConcurrentRequestsPerProjectModel": 1,
"projectCircuitBreaker429Threshold": 3,
"projectCircuitBreakerWindowMs": 600000,
"projectCircuitBreakerCooldownMs": 3600000,
"modelCircuitBreaker429Threshold": 3,
"modelCircuitBreakerCooldownMs": 21600000,
"dailyAccountSlowRequests": 250,
"dailyAccountStopRequests": 350,
"dailyProjectSlowRequests": 900,
"dailyProjectStopRequests": 1200,
"slowModeJitterMinMs": 8000,
"slowModeJitterMaxMs": 25000,
"protectivePauseMs": 21600000,
"useRequestCountRotationWhenQuotaUnknownOnly": true,
"accounts": [
{
"email": "[email protected]",
"refreshToken": "1//...",
"projectId": "project-abc123",
"label": "user"
}
]
}Options
| Field | Default | Description |
|-------|---------|-------------|
| proxyPort | 51200 | Port the proxy listens on |
| requestsPerRotation | 5 | Max per-model requests before attempting request-count rotation |
| rotateOnQuotaDrop | 20 | Rotate when a model's quota drops this many %. Set to 0 to disable |
| quotaPollIntervalMs | 300000 | Quota poll interval in ms (5 minutes) |
| maxConcurrentRequestsPerAccount | 1 | Max simultaneous requests allowed per account |
| maxConcurrentRequestsPerProjectModel | 1 | Max simultaneous requests allowed across accounts sharing the same projectId for the same quota model |
| projectCircuitBreaker429Threshold | 3 | Unique accounts from the same projectId that must hit provider 429 before pausing that project/model |
| projectCircuitBreakerWindowMs | 600000 | Rolling window for the project/model 429 circuit breaker |
| projectCircuitBreakerCooldownMs | 3600000 | Minimum project/model pause after the circuit breaker trips |
| modelCircuitBreaker429Threshold | 3 | Unique accounts across all projects that must hit provider 429 for the same quota model before pausing that model globally |
| modelCircuitBreakerCooldownMs | 21600000 | Minimum model-wide pause after the global model circuit breaker trips |
| dailyAccountSlowRequests | 250 | Daily upstream attempts per account before slow-mode jitter starts |
| dailyAccountStopRequests | 350 | Daily upstream attempts per account before routing stops for that account until the next UTC day |
| dailyProjectSlowRequests | 900 | Daily upstream attempts per projectId before slow-mode jitter starts |
| dailyProjectStopRequests | 1200 | Daily upstream attempts per projectId before routing stops for that project until the next UTC day |
| slowModeJitterMinMs | 8000 | Minimum slow-mode delay before upstream request |
| slowModeJitterMaxMs | 25000 | Maximum slow-mode delay before upstream request |
| protectivePauseMs | 21600000 | Global routing pause after a serious provider enforcement signal |
| useRequestCountRotationWhenQuotaUnknownOnly | true | Use request-count rotation only until quota telemetry exists for the request's model. Set to false to keep rotating by request count even with known quotas |
Account Fields
| Field | Description |
|-------|-------------|
| email | Google account email (auto-filled by login) |
| refreshToken | OAuth refresh token (auto-filled by login) |
| projectId | Cloud project ID discovered from Google during login |
| projectSource | Optional metadata: google when discovered from Google, manual if edited by hand |
| label | Display name on the dashboard (auto-filled, defaults to email username) |
API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | /dashboard | Web dashboard |
| GET | /login | Hosted account-link landing page |
| GET | /api/status | JSON status: accounts, quotas, model routing, flags |
| POST | /api/enable/<email> | Re-enable a disabled account after its underlying issue is fixed |
| POST | /api/settings/fresh-window-starts/on | Allow opening new idle/fresh windows globally |
| POST | /api/settings/fresh-window-starts/off | Block opening new idle/fresh windows globally |
| POST | /api/account-fresh-window-starts/<email>/on | Allow one account to override the global fresh-window block |
| POST | /api/account-fresh-window-starts/<email>/off | Return one account to the global fresh-window policy |
| POST | /api/self-update | Trigger npm self-update to latest version (admin-only) |
| POST | /v1internal:streamGenerateContent | Native Antigravity proxy endpoint (used by pi) |
| GET | /v1/models | OpenAI-compatible model list |
| POST | /v1/chat/completions | OpenAI-compatible non-streaming chat adapter |
| POST | /v1/messages | Anthropic-compatible non-streaming messages adapter |
If PI_ROTATOR_ADMIN_TOKEN is set, dashboard/API requests must include either Authorization: Bearer <token>, X-Rotator-Admin-Token: <token>, or ?token=<token> for browser dashboard access. The native pi proxy endpoint and compatibility adapters remain unauthenticated so existing clients keep working. Put this service behind a trusted local boundary if exposing beyond localhost/LAN.
Compatibility Adapters
The compatibility adapters are additive. They do not change the native /v1internal:streamGenerateContent route used by pi.
OpenAI-compatible example:
curl http://localhost:51200/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "gemini-3-flash",
"messages": [{"role": "user", "content": "Say pong"}],
"stream": false
}'Anthropic-compatible example:
curl http://localhost:51200/v1/messages \
-H 'Content-Type: application/json' \
-d '{
"model": "claude-sonnet-4-6",
"system": "Be terse.",
"messages": [{"role": "user", "content": "Say pong"}],
"max_tokens": 128,
"stream": false
}'Current adapter scope:
- Text chat/messages.
- Streaming mode is supported as compatibility SSE. The adapter buffers the upstream Antigravity stream, then emits one OpenAI/Anthropic-compatible final delta. Native token-by-token pass-through is not implemented yet.
- Image input is supported when sent as base64 data URL (
OpenAI image_url.url = data:image/...;base64,...) or Anthropic base64 source (type=image,source.type=base64). - Tool/function calling is explicitly rejected with
400until a safe contract mapper is implemented.
Development Checks
npm run typecheck
npm test
npm run checkThe test suite covers model resolution contracts and dashboard render/syntax smoke checks.
Running as a Service
# Using nohup
nohup npm start > rotator.log 2>&1 &
# Or with systemd (create /etc/systemd/system/pi-antigravity-rotator.service)
[Unit]
Description=Pi Antigravity Rotator
After=network.target
[Service]
Type=simple
WorkingDirectory=/path/to/pi-antigravity-rotator
ExecStart=/usr/bin/npm start
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetTroubleshooting
Account shows flagged status
Google detected potential abuse or enforcement. Review the error message on the dashboard and resolve the provider-side block first. Flagged accounts are quarantined and are not re-enabled through /api/enable/<email> until the underlying provider issue is cleared by replacing or restoring the account.
Account keeps getting disabled after 5 errors
Check the error message. Common causes: revoked OAuth consent, expired refresh token (re-run npm run login), or Google account suspension.
Quota bars not showing Quota data appears after the first poll cycle (up to 5 minutes). Ensure accounts have valid tokens.
All accounts exhausted
If the outage is temporary (cooldown, model breaker, or protective pause), the proxy returns 429 with Retry-After and retryAfterMs; clients must back off. The proxy returns terminal 503 only when there is no known retry time, for example all accounts are disabled/flagged.
Multiple agents on different models This is fully supported. Each model routes independently. Agent 1 using Gemini Pro and Agent 2 using Claude will each have their own active account and won't interfere with each other's rotation.
Telemetry
pi-antigravity-rotator collects anonymous usage telemetry to help understand how the tool is used and — most importantly — to improve the anti-flag algorithm that protects your accounts.
What is collected
Heartbeat (at boot, every 1h, at shutdown):
- Random install ID (UUID — not tied to any account or person)
- Rotator version, Node.js version, OS, architecture
- Account count, models in use, total request count, uptime
- Routing health state (healthy/paused/stopped)
- Flagged/disabled/pro/free account counts
- Per-model token usage (input/output tokens + request count per model)
- Feature usage flags (dashboard opened, login used, etc. — booleans only)
Flag events (sent immediately when Google flags an account):
- HTTP status that triggered the flag (401 or 403)
- Which known patterns matched (e.g.
infring,abus,suspend— from a fixed allowlist) - Model being requested, quota timer type, quota percentage
- Account request velocity (requests/hour), concurrent requests, lifetime requests
- Pool state: size, healthy count, whether protective pause triggered
- Time since previous flag
The dashboard shows both raw Flag Events and deduped Unique Flag Incidents so repeated identical incidents don't inflate the signal.
Flag data is the most valuable signal. It lets us study what behavior patterns lead to flags and improve the rotation algorithm to avoid them — benefiting everyone.
What is NEVER collected
- ❌ Email addresses
- ❌ OAuth tokens or API keys
- ❌ IP addresses
- ❌ Google project IDs
- ❌ Request/response bodies
- ❌ Error message text (only which known keywords matched)
Endpoint
Telemetry posts to:
http://telemetry.dragont.ec:3800/v1/eventsOpt out
Set the environment variable before starting:
export PI_ROTATOR_TELEMETRY=offOr use any of: PI_ROTATOR_TELEMETRY=false, PI_ROTATOR_TELEMETRY=0.
Support Me
If this tool has helped you optimize your API usage and save costs, consider supporting its development!
