XWipe
Bulk deletion of a social account's history, built against a metered third-party API.

XWipe deletes posts, likes, retweets and bookmarks from an X account in bulk. It is at getxwipe.com, with an iOS app. We use X's public API and are not affiliated with X.
What follows is the engineering, not the pitch.
The constraint
Everything here is somebody else's rate limit.
The API is metered pay-as-you-go, so every deletion costs real money and an empty wallet surfaces as an HTTP 402 that looks exactly like a code fault in a log digest. It is not one, and knowing that saves an afternoon each time. The API also only reads back roughly the last 3,200 posts, so anyone wanting to clear a decade of history has to upload their account archive and we work from the identifiers in it.
The part that cost us the most is quieter. X's OAuth 2.0 PKCE flow uses single-use rotating refresh tokens. Each refresh consumes the stored token and issues a new one, invalidating the old one immediately.
The stack, and why
Next.js with Prisma over Postgres, Redis with BullMQ for the job queue,
NextAuth v5 for the OAuth handshake, and the X API v2. Everything runs on
Railway: a web service, a worker service switched by a WORKER_MODE
environment variable, Postgres and Redis in one project so the web app can use
internal hostnames.
The worker is a separate service rather than a background function because a deletion run is measured in hours, not seconds. It pauses on rate limits, waits, and resumes. There is nowhere to put that inside a request.
Three decisions
Exactly one code path may refresh a token. getFreshAccessToken(userId) is
it, guarded by a per-user Redis lock. A caller that arrives while the lock is
held waits and reuses the token the holder just produced, detected by the
expiry timestamp advancing past what it first read, rather than rotating again.
It degrades to an unlocked refresh if Redis is down. A verification script fires
eight concurrent callers at an expired token and asserts exactly one refresh.
The rule that goes with it is absolute: never call the underlying refresh
function directly, and never add a second path.
A dead session pauses a job instead of failing it. When authentication dies
mid-run, the job goes to paused with progress preserved. When the user
reconnects, the NextAuth sign-in callback re-enqueues paused jobs, claimed
atomically so a reconnect cannot start the same job twice, and the loop skips
items already processed. One reconnect, no lost progress, no re-burned quota.
That decision has a billing consequence we did not anticipate. A paused job is
billed at the pause and again at completion, so the original "have we credited
this job" boolean was wrong. It was replaced with a creditedCount column and a
delta calculation at every crediting site. Any design where a unit of work can
complete more than once needs delta accounting, not a flag.
A failure streak stops the job early. The circuit breaker trips after three consecutive failures, each already past its own retries, and resets on any success. It was ten. Three cuts the API spend on a doomed run by roughly seventy percent, at the cost of occasionally stopping a job that would have recovered. On a metered API that trade is easy.
What broke
The same 401, five times, each time from a different place.
Jobs stopped mid-run and then recovered on their own, which is the least
helpful failure signature there is. Logs showed ten consecutive
Token refresh failed: 401, the breaker marking the job failed, and then a
later run completing fine. The apparent correlation with the rate-limit window
filling up was a red herring: a long rate-limit wait triggered a staleness check
that collided with another refresher.
Three independent unlocked refresh paths existed. The lock fixed the collisions.
Then jobs still failed, because the deletion loop refreshed once per failing item, so a streak of 401s produced ten rotations in a few minutes and X's own abuse detection started rejecting the refresh endpoint. Fixed with a job-scoped five-minute cooldown, set deliberately longer than the time the breaker takes to trip, so a doomed run rotates exactly once and then fails cleanly.
Then returning users saw every job fail at 0 of 0 with a raw 401, because the token refresher was constructed before the loop began and threw unguarded, which also triggered three BullMQ retries hammering an already dead token. Wrapped, with a graceful end and a reconnect prompt.
Then three admin endpoints turned out to be rotating outside the lock, including the "Refresh X Token" rescue button. So clicking the button to fix a user could invalidate the token family and cause the exact reconnect it was meant to prevent.
The lesson is not about tokens. A rule enforced by convention has as many holes as it has call sites, and the last hole is always in the admin panel.
Where it is now
Live on the web with multiple locales, and on iOS. The iOS app sells credit packs as consumables rather than subscriptions, with the store transaction identifier used as the idempotency key so a redelivered receipt cannot double-credit an account.
Built by BestDid
We built XWipe and we run it. Plans and the free tier are at getxwipe.com.
Related: the Geotally case study covers orchestrating six third-party APIs at once, and the BDShield case study covers maintaining twelve products on one architecture. The rest is on our work page. To discuss a build, see services.