Auth & token lifecycle
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
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.
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.
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.
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.
Runs a real authorization-code flow with PKCE.
drive.metadata.readonly) — filenames, nothing inside them. Tokens live in a signed, HttpOnly cookie for one hour and are never written to storage. Disconnecting revokes the grant at the provider, not just here.- scope
- —
- token
- —
- refresh
- —
These hit the deployed callback handler for real and render whatever it returns.
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.
Where this runs
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.
Contrast: SEC EDGAR needs no credential at all
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.
Finding: heartbeat > refresh logic
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