Python client + interactive CLIs for a sub-pool server — a
credential broker for N Claude and OpenAI Codex subscription
accounts.
You get three things in one package:
sp-claude— aclaudeCLI wrapper. Leases a Claude account from the pool, runs the realclaudebinary against a per-sessionCLAUDE_CONFIG_DIR, rotates the access_token in the background, and swaps accounts transparently if the leased one cools mid-run.sp-codex— same flow for OpenAI Codex. Leases a Codexauth.json, drops it into an isolatedCODEX_HOME, spawns the realcodexbinary, and rotates the token in the background.PooledClient/PooledCodexClient— Python SDK classes for programs and services.PooledClientis a drop-in subclass ofClaudeSDKClientfrom the officialclaude-agent-sdk;PooledCodexClientis a thin async wrapper aroundcodex exec.
The agent loop — tool execution, filesystem I/O, hooks, MCP servers, the codex subprocess — all stays on the consumer's machine. The pool only sees lease lifecycles.
pip install sub-pool-client
# or
uv pip install sub-pool-clientLatest release tracks main. To install from source instead:
pip install git+https://github.com/subs-pool/sub-pool-clientBoth the CLIs and the SDK read:
export SUB_POOL_URL=http://your-pool-host:8787
export SUB_POOL_KEY=cp-... # admin issues this from the sub-pool UISUB_POOL_KEY's strategy decides which accounts you can reach.
sp-claude --setup # one-time interactive config wizard
sp-claude # start the claude REPL on a leased account
sp-claude "explain this" # single-prompt mode
sp-claude --account alice # pin to a specific account
sp-claude --status # show effective config and exitsp-claude --setup writes pool URL + API key to ~/.sub-pool/cli.toml
at 0o600. Subsequent invocations exec claude against a per-session
config dir that holds the leased .credentials.json. The persistent
home (~/.sub-pool/claude-home/) keeps your conversation history
across sessions and is isolated from ~/.claude/ — sp-claude never
reads or writes your real claude config.
If the leased account starts cooling (Anthropic rate-limit or quota
window), a background watcher swaps to a different account
transparently. Your running session sees one continuous claude
process; only the access_token under the hood changes.
sp-codex --setup
sp-codex # interactive REPL on a leased Codex account
sp-codex exec "summarize ." # one-shot
sp-codex --account codex1Shares ~/.sub-pool/cli.toml with sp-claude. Persistent
CODEX_HOME lives at ~/.sub-pool/codex-home/.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from sub_pool_client import PooledClient
async def main():
options = ClaudeAgentOptions(system_prompt="Be terse.")
async with PooledClient(options=options) as client:
print(f"leased {client.account} lease_id={client.lease_id}")
await client.query("List three prime numbers.")
async for msg in client.receive_response():
print(type(msg).__name__, "->", msg)
asyncio.run(main())PooledClient subclasses ClaudeSDKClient, so every option the SDK
accepts (hooks, MCP servers, cwd, allowed_tools,
permission_mode) works unchanged.
from claude_agent_sdk import ClaudeSDKError
async with PooledClient(options=options) as client:
try:
await client.query("...")
async for msg in client.receive_response():
...
except ClaudeSDKError as e:
if "429" in str(e) or "rate" in str(e).lower():
await client.report_error("RateLimit", str(e))
raisereport_error posts to /credentials/lease/{id}/report-error; the
pool marks the account COOLING so the next lease skips it.
Multiple async with PooledClient() calls in the same process — or
across sibling processes — with identical routing inputs (user_id,
required_model) share a single lease. Coordination happens
through ~/.sub-pool/client/<hash>/ (flock + holder list). First
arriver leases; later arrivers refcount; last out releases. So N
parallel agents on one account work up to Anthropic's per-account
concurrency limit. Vary user_id (with the sticky_user strategy)
to force different accounts in parallel.
import asyncio
from sub_pool_client import PooledCodexClient
async def main():
async with PooledCodexClient() as codex:
proc = await codex.exec(
"summarize this repository",
cwd=".",
model="gpt-5.5",
)
stdout, stderr = await proc.communicate()
print(stdout.decode())
asyncio.run(main())PooledCodexClient writes the leased (sanitized) auth.json into an
isolated CODEX_HOME and spawns codex exec against it. A
background poll task rotates the on-disk auth.json before the
access_token expires. On a backend error, call
await codex.report_error("RateLimit", "...") so the pool cools the
account.
- The pool exclusively owns the
refresh_tokenchain (both Anthropic and OpenAI). Leases hand out only the access_token; therefreshTokenfield in the leased credentials file is blank. - Token rotation happens through
POST /credentials/lease/{lease_id}/token— a consumer crash can never leak the pool's refresh state. account_names come from the pool's admin config. The pool decides which account fulfills each lease based on the API key's strategy.
The sub-pool server (admin UI, strategy reference, account
lifecycle) is hosted separately — point this client at whatever pool
URL your admin gave you via SUB_POOL_URL above.
~/.sub-pool/
├── cli.toml # sp-claude / sp-codex shared config (0o600)
├── claude-home/ # sp-claude persistent CLAUDE_CONFIG_DIR
├── codex-home/ # sp-codex persistent CODEX_HOME
└── client/<hash>/ # PooledClient / PooledCodexClient
# cross-process lease coordination
Override the SDK shared-dir root with SUB_POOL_CLIENT_DIR.
examples/hello.py—PooledClientend-to-end with a retry loop on 429.examples/codex_hello.py—PooledCodexClientrunningcodex exec.
(TBD — defer to upstream)