Integration Lab / bench 01 — auth & token lifecycle checking…
Bench 01

Auth & token lifecycle

Testing aim

Can an unattended poller hold third-party credentials for months without a mid-request expiry, a silent stop, or a human being asked to log in again?

success
ingestion survives token expiry indefinitely with no manual step
failure
any state the poller cannot recover from on its own
also testing
whether a revoked grant is distinguishable from a quiet data source
Pain point

A dead credential looks exactly like a quiet market

LatticePulse polls X and Reddit every few minutes. Both authenticate with OAuth 2.0, and both issue access tokens measured in hours. A process that runs unattended for months will therefore refresh its credentials thousands of times, and every one of those refreshes is an opportunity to stop working forever.

The dangerous part is what failure looks like. If the refresh token is revoked — a user changes a password, an app’s permissions are reviewed, a provider rotates something — ingestion from that source simply returns nothing. The heatmap keeps rendering. The scores keep computing. A source producing zero events is indistinguishable from a source where nothing is happening, and “nothing is happening” is a perfectly plausible reading of a quiet Sunday.

By the time someone notices that X has contributed no signals since Tuesday, the baseline has been polluted: every ticker’s attention velocity has been computed against a window that was missing a whole source. The scores aren’t just stale, they’re wrong in a way that survives the fix.

CryptoFundingWatch has the mirror-image problem. Exchange APIs authenticate with signed API keys rather than OAuth, so nothing expires on a timer — but nothing refreshes either. A rotated or rate-limit-suspended key fails the same silent way, and because there is no expiry event to hook into, there is nothing to monitor except the absence of data. The lesson transfers: the credential needs a heartbeat, not just a refresh.

SEC EDGAR, by contrast, needs no credential at all — just a declared User-Agent. Worth noting because it is the only one of the four sources that cannot fail this way, and it is not a coincidence that it is also the least fragile.

Solution architecture

Authorization code, with PKCE

Six steps. The two that matter for an unattended poller are the last two: refreshing ahead of expiry rather than reacting to a 401, and treating a revoked grant as an alertable state rather than an empty result.

CLIENT PROVIDER EDGE FUNCTION 1 · /api/oauth/start verifier = random(32) challenge = S256(verifier) 2 · consent screen holds the challenge user approves scope 3 · /api/oauth/callback receives code + state state compared constant-time 4 · token exchange code + verifier + secret → access + refresh token no verifier = no tokens 5 · signed session HttpOnly · Secure absolute expiry tracked nothing stored server-side 6 · refresh ahead at T-5min, not on 401 rotated token replaces old revoked → alert, not silence

Why PKCE. Without it the authorization code is a bearer token travelling through a redirect — a URL, which lands in browser history, server logs and Referer headers. Anyone who captures it can exchange it for real tokens, because the token endpoint asks only for the code and a client ID.

PKCE makes the code useless on its own. The client generates a random verifier, sends only its hash as a challenge, and must present the original verifier at exchange time. A stolen code without the verifier exchanges for nothing. Ten lines, specified in RFC 7636.

Live bench

Run the full lifecycle

A real authorization-code flow with PKCE against your own Google account — chosen because it is a provider anyone reading this can test with. Connect, watch the token age, force a refresh, then revoke it and see what the poller would experience.

Connection
Trigger a failure

These hit the deployed callback handler for real and render whatever it returns.

oauth · token lifecycle
idle
Expires in
—
Refreshes
0
Last latency
—
Implementation process

Holding a credential unattended

The flow itself is the easy part and every library does it. These are the steps that decide whether the poller is still running in six months.

Track absolute expiry, not elapsed time

Store the instant the token dies, computed once at issue. Anything based on “seconds since we got it” drifts across restarts, clock changes and queue delays — and drifts in the direction of using an expired token.

Refresh ahead of the deadline, never on a 401

Refresh when less than five minutes remain. Reacting to a 401 means the expiry lands inside a real request, and the retry has to be safe to repeat — a problem bench 03 exists to handle and this one should avoid creating.

Serialise refreshes per credential

Two workers refreshing the same token concurrently is how you get one valid token and one immediately-invalidated one, with a coin flip deciding which worker survives. One refresh in flight per credential, others wait for its result.

Treat invalid_grant on refresh as terminal

It means revoked or expired beyond recovery. Retrying it never succeeds, so retry logic here is a bug rather than resilience. Clear the session, raise an alert, and stop pretending the source is quiet.

Give every credential a heartbeat

The single most important step, and the one that comes from the pain point rather than the spec. Record the last successful fetch per source and alert on absence. A source that has produced nothing for three times its expected interval is broken until proven otherwise.

Request the narrowest scope that works

Scope is the blast radius of a leaked token. It also determines how alarming the consent screen looks, which matters when the person approving it is a user rather than you.

Use cases

Where this runs

System B · primary

LatticePulse

X and Reddit ingestion both run on OAuth 2.0 with tokens that expire in hours, polled every few minutes, unattended. The refresh path runs thousands of times between human interventions, and a revoked grant must raise an alert rather than quietly contributing zero events to the baseline.

Tests: long-running refresh, revocation detection
Contrast: SEC EDGAR needs no credential at all
System A · the mirror case

CryptoFundingWatch

Exchange APIs use signed keys, so nothing expires on a timer — and nothing refreshes either. There is no expiry event to hook into, which means the heartbeat is the only detection mechanism available when a key is rotated or suspended.

Tests: credential failure with no expiry signal
Finding: heartbeat > refresh logic
Findings

What the bench showed

The refresh logic is not the hard part; the monitoring is. Going in, this bench was about getting PKCE and rotation right. Those took an afternoon and every library does them already. The finding that actually changed how both systems are built is that a credential failure produces no error anywhere — it produces an absence, and absences don’t page anyone. Every source now carries a last-successful-fetch timestamp and alerts on staleness, which catches key rotation, revocation, rate-limit suspension and a silently changed endpoint with one mechanism.

Refreshing reactively creates a problem that didn’t need to exist. Waiting for a 401 puts the expiry inside a live request, which then has to be retried, which means the request has to be safe to repeat. Refreshing five minutes early removes the entire class of problem for the cost of one extra call per token lifetime.

Distinct failure branches are worth the extra code. The bench handles cancelled consent, state mismatch, replayed code and revoked grant as four separate outcomes rather than one 500. Only one of them — invalid_grant on refresh — is terminal and needs a human. Collapsing them means either paging someone for a user who clicked cancel, or retrying forever on something that will never succeed.

Not everything needs a credential, and that is an architectural option. SEC EDGAR is the least fragile of LatticePulse’s four sources purely because it has nothing to revoke. Where a public or key-free endpoint gives the same data, it is worth a meaningful loss of convenience.

Bench source: _shared.ts · start.ts · callback.ts · session.ts
Related: bench 03 — retry & dead letter

← All benches