Pooled Identities & Routing
A pool holds one or more GitHub identities. The relay picks one identity per request and performs the GitHub call with it. Routing prefers healthy identities with available rate budget and avoids piling many callers onto the same identity.
Source: src/db.ts (loadIdentities), src/github.ts (token minting), src/pool-coordinator.ts (selection), src/index.ts (upsertIdentity).
#Identity kinds
#pat
A user/service GitHub Personal Access Token, stored as a Cloudflare Worker secret. The identity's secret_ref is the binding name; the Worker reads the token at request time and never logs or returns it.
#github_app
A GitHub App installation. The identity carries an installation_id and a secret_ref that points at the App's PKCS#8 private key secret. The Worker:
- Mints a short-lived RS256 JWT for the App (
OCTOPOOL_GITHUB_APP_ID) using WebCrypto. - Exchanges it for an installation access token via
POST /app/installations/{id}/access_tokens. - Caches the installation token in memory and refreshes it ~60s before expiry.
The private key must be BEGIN PRIVATE KEY (PKCS#8) PEM — BEGIN RSA PRIVATE KEY (PKCS#1) is rejected with 503 github_app_key_format, because WebCrypto only imports PKCS#8. For v1 the octopool-cache App is installed on selected repositories only (openclaw/openclaw); no private-repo installations.
#Scopes
Each identity has one or more identity_scopes rows (owner, optional repo, allow_private). When a request targets owner/repo, only identities scoped to that owner (with a matching repo or an owner-wide NULL repo) are candidates. Routes with no owner (e.g. /rate_limit) consider all active identities in the pool.
allow_private exists in the schema but the shared relay is public-repository-only in v1; the public-repo guard blocks private routes regardless.
#Selection (PoolCoordinator)
Identity selection runs in a Durable Object partitioned per pool (pool:<pool_id>). It keeps three SQLite tables in DO storage:
leases— sticky route→identity binding, 10s TTL.rate_states— last seenremaining/reset_atper identity and resource bucket.cooldowns— per identity, scoped to*,resource:<r>, or a route key.
selectIdentity logic:
- If a live lease for the route key points at a candidate that is not cooling down and not quota-exhausted, reuse it (
reason: sticky). - Otherwise score each non-cooling candidate by
remaining + weight(unknown rate assumes a fresh 5000 budget; an exhausted-but-unreset identity is skipped) and take the best (reason: highest_remaining, orfallback). - If every candidate is cooling down, the Worker returns
503 identities_cooling_down.
The winning route gets a fresh 10s lease so concurrent callers stick to the same identity briefly instead of stampeding.
#Health feedback (cooldowns)
After each GitHub call, recordResult updates rate_states from the response's x-ratelimit-* headers and, on a 401/403/429, writes a cooldown:
401→ global*cooldown (Retry-After, else 120s).- a
Retry-Afteron any error → global*cooldown for that duration. 403with budget remaining (secondary/abuse limit) → global*cooldown, 120s.429→resource:<resource>cooldown, 120s.- otherwise → route-key cooldown, 120s.
Cooling-down identities are skipped by selection until their cooldown expires.
#Registration
Identities are created/updated by admins via POST /v1/admin/pools/:pool/identities or octopool admin identity. See Admin & provisioning. weight (default 100) biases selection between otherwise-equal identities.