Opening IO Gateway

How-to guides

Codex reset-credit automation

Use when
Preserving expiring Codex reset credits
Storage
<auth_dir>/api-key-policy.sqlite3
Updated
September 21, 2026

Codex reset-credit automation is an opt-in, per-account safety net for an earned ChatGPT usage-limit reset credit that is nearing expiry. It is deliberately conservative: it never treats a dashboard cache, a renamed credential, or an ambiguous network result as authority to spend a credit.

What the policy does

  1. 1
    Read fresh App Server state.

    The worker reads Codex rate limits and reset-credit details through the mapped local App Server profile; the dashboard quota cache is not used to authorize redemption.

  2. 2
    Select one concrete credit.

    It chooses the earliest-expiring available Codex-rate-limits credit (the App Server form is codexRateLimits) with an explicit ID and a known expiry at least 30 minutes and no later than the configured upper window from the fresh read.

  3. 3
    Prove it is needed.

    Immediately before the POST, another fresh read must show a reached Codex limit, the same credit still available, and a natural reset that is not already near.

  4. 4
    Persist, redeem, verify.

    The exact credit ID and a gateway-generated idempotency key are durable before the POST. A successful or idempotent answer needs a final fresh read proving the limit cleared.

Use managed local App Server profiles

Reset-credit work uses the documented Codex App Server JSON-RPC account API, not upstream_base or the gateway's direct HTTP/Wham quota adapter. It launches a short-lived local codex app-server --stdio child for account/rateLimits/read and account/rateLimitResetCredit/consume. Codex App Server is experimental and unsupported for production workloads, so this feature remains off unless its configuration explicitly opts in; there is no direct-backend fallback.

{
  "codex_reset_credit_app_server": {
    "experimental_opt_in": true,
    "command": "/usr/local/bin/codex",
    "profile_root": "/var/lib/io-gateway/codex-app-server-profiles",
    "profiles": [{
      "account_key": "codex:account_id:org_123",
      "profile": "org_123",
      "expected_email": "[email protected]"
    }]
  }
}

The executable and profile root must be absolute existing paths. Each simple profile directory is private and uniquely bound to one stable account, and must already contain its own private auth.json from a Codex-managed ChatGPT login. IO Gateway verifies the managed account and its required configured email on launch, clears ambient environment values, supplies no gateway bearer credential to the child, and fixes a file-backed credential-store override. A profile config.toml may not set sqlite_home; an explicit cli_auth_credentials_store is allowed only when it is "file".

Before enabling it

  • Keep the dashboard private. This is an administrator-only operation on an upstream ChatGPT account.
  • Use a stable identity and profile. The account must be an enabled, uniquely resolved codex:account_id:<id> with a matching configured managed App Server profile. Labels, filenames, and anonymous manual-N identities are rejected for unattended use.
  • Expect fail-closed behavior. A count/details state that cannot prove the exact selected credit, unknown expiry, unavailable fresh state, or unknown natural reset time means no automatic spend; an existing automatic action is deferred for a later fresh check.

Default timing

SettingDefaultEffect
scan_interval_minutes30Per-account cadence for newly eligible credits; enabling or editing an enabled policy also schedules one immediate scan.
expiry_window_minutes60Upper selection bound: a new automatic action requires 30 minutes <= expires_at - now <= window.
final_attempt_minutes5Schedule a deferred action's final pre-expiry check this many minutes before expiry when possible.
min_natural_reset_remaining_minutes10Retain the credit when the ordinary reached window will reset at or within this time.

The worker wakes every minute only to honor durable action deadlines; it does not scan every account every minute. Policy scans remain on their configured cadence. With the default 30-minute cadence and 60-minute upper window, new credits are selected only in the intended 30–60 minute interval before expiry. An already selected durable action may be rechecked closer to expiry after a deferral, retry, or temporary upstream failure; that never makes a newly discovered credit under 30 minutes old eligible. expiry_window_minutes must be 30 through 10,080, and final_attempt_minutes must be 1 through one minute less than that window. A restart preserves scan and action deadlines, but an outage that lasts beyond credit expiry cannot be recovered automatically.

Enable and inspect

Read the eligible account keys, defaults, policies, and browser-safe action status:

curl -sS http://127.0.0.1:8319/admin/codex/reset-credit-automation \
  -b "$ADMIN_COOKIE"

Enable the default policy for an account marked eligible:

curl -sS -X POST http://127.0.0.1:8319/admin/codex/reset-credit-automation \
  -b "$ADMIN_COOKIE" \
  -H 'Content-Type: application/json' \
  --data '{
    "account_key":"codex:account_id:org_123",
    "enabled":true,
    "scan_interval_minutes":30,
    "expiry_window_minutes":60,
    "final_attempt_minutes":5,
    "min_natural_reset_remaining_minutes":10
  }'

Disabling the same stable account key stops future automatic work and remains allowed even after its credential is disabled or removed. Existing actions remain durable for audit and safe recovery; disabling does not make the worker choose another credit. The write endpoint rejects unknown fields and never returns its internal idempotency keys.

Read action state safely

StateMeaning
pendingA specific credit is waiting for its first durable attempt.
deferredThe credit is being retained for a later check: no reached limit, nearby/unknown natural reset, a disabled policy, or a safe retry time.
submittedThe exact credit and idempotency key were stored before a POST; any retry uses those same values.
verifiedA fresh post-consume read confirmed the reached rate limit cleared.
no_credit, expiredThe selected credit is no longer usable.
manual_reviewThe outcome is ambiguous, or a legacy direct-HTTP submission needs review. Automation stops rather than spending another credit.

Opaque credit IDs, timing, and safe outcome metadata are stored, but provider credentials, prompts, raw request bodies, raw upstream responses, and idempotency keys are not exposed through the browser endpoint.

Manual redemption shares the ledger

Manual reset requests require a concrete credit_id and the same matching managed App Server profile; the gateway generates and persists the idempotency key. The dashboard quota view is informational only: the endpoint re-reads the selected credit and rate-limit state through App Server before spending. Do not ask upstream to select the next credit or send a client idempotency key.

curl -sS -X POST http://127.0.0.1:8319/codex/rate-limit-reset-credit/consume \
  -b "$ADMIN_COOKIE" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'file_name=my-codex-account.json' \
  -d 'credit_id=credit_...'
Operate from durable evidence

Only verified proves that a reset completed. A timeout after submission retains the exact credit and idempotency key for recovery. Treat manual_review as an operator task, not a reason to send another redemption request with a different credit.

Legacy transport migration

Older direct-HTTP/Wham actions are migrated safely. A legacy action already marked submitted becomes manual_review; its old idempotency key is never replayed through App Server. A legacy pending or deferred action is retained and must pass fresh App Server checks before it can proceed.

Persistence and backup

Policies and actions live in <auth_dir>/api-key-policy.sqlite3 with managed API-key policy and quota accounting. Keep that directory on persistent local storage, retain its identity file, and use io-gateway --config /absolute/config.json --backup-policy /backups/new-policy-snapshot for a WAL-consistent policy snapshot. Do not share the live SQLite database across hosts or over NFS.