Skip to content

Read r/couponcodes into stores and coupons every five minutes - #59

Merged
ralyodio merged 1 commit into
masterfrom
worktree-reddit-couponcodes-source
Sep 13, 2026
Merged

ralyodio merged 1 commit into
masterfrom
worktree-reddit-couponcodes-source

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

c0upons now reads r/couponcodes (https://www.reddit.com/r/couponcodes/new.rss) into stores and coupons, every five minutes, for new listings. Until now the only source was nichedb.dev's deals collection.

How

  • apps/web/lib/reddit-sync.ts: fetches the Atom feed, maps each post that carries a code or a link to the store into a coupon under a store it names, and upserts on (source 'reddit', source_id 'couponcodes:<post id>'). Requests for a code are declined, and so is any post linking back to c0upons.com (our own coupons get reposted there, so this stops the echo). A null code never overwrites a code the browser reveal found.
  • Reddit blocks datacenter fetches (429, empty body, from Railway and from the dev box alike). The run tries Reddit directly and falls back to the Obscura relay's fetch_page with format: original; the result says via: direct | relay. OBSCURA_RELAY_URL overrides the relay, REDDIT_COUPONCODES_FEED the feed.
  • Store naming: from the title ("$10 off at Wonder", "Sideline Swap Coupon Code", "Netcup Voucher Codes"), else from the linked host when the title is a sentence; the linked host wins when the title merely contains it, so "Deutscher Starlink" and "Starlink" are one store. Codes: "code: X" prose, "use code X", long numeric promo codes, or a link's promoCode / applyCode / auto_applied_promo_code parameter.
  • Every five minutes: apps/web/instrumentation.ts starts an in-process poller (lib/reddit-poller.ts) at server boot. REDDIT_SYNC_INTERVAL_MINUTES sets the cadence, 0 disables. GitHub's scheduler is too coarse and unreliable for this, and the app has no worker.
  • POST /api/sync/reddit: keyless trigger, throttled to one run per four minutes. Added to the keepalive workflow as a backstop, to both CLIs' sync, to the docs page, and to the MCP server as sync_reddit (descriptor updated).
  • Version 1.7.0 in all four places the version test checks.

Verified

  • node --test test/*.test.mjs: 33 pass. test/reddit-sync.test.mjs runs the real migration against a local libSQL file, a fake Reddit that answers 429, and a fake relay; it covers parsing, mapping, the echo skip, the relay fallback, the throttle, idempotent re-runs and code preservation.
  • tsc --noEmit clean, eslint clean on the touched files, next build succeeds.
  • Dry run of the mapper over the live feed captured 2026-09-13: 21 of 25 posts taken, 4 declined as intended (our own echo, a request for a code, a bare code with no store, a title with neither code nor link).

After merge

Railway deploys; the poller starts on boot and the first read happens 20 seconds later. Check with:

curl -X POST https://c0upons.com/api/sync/reddit

A skipped answer means the poller just ran. New stores appear at /stores; rows carry source = 'reddit'.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R63WmerrXfwcHkFRc61J2B

c0upons only filled itself from nichedb.dev's deals collection. The
subreddit where people paste the code they were just handed was not read
at all, so its listings never reached the site. This adds a second source.

lib/reddit-sync.ts reads the subreddit's Atom feed (new.rss, 25 posts),
maps each post that carries a code or a link to the store into a coupon
row under a store it names, and upserts on (source 'reddit', source_id
'couponcodes:<post id>'). Requests for a code are declined, and so is any
post that links back to c0upons.com, or the site would echo itself. A null
code never overwrites one the browser reveal already found.

Reddit answers 429 to a plain fetch from a datacenter address, so the
feed is fetched directly first and through the Obscura relay's fetch_page
(format original) when Reddit refuses; the run reports which path
answered. The store comes from the title ("$10 off at Wonder", "Sideline
Swap Coupon Code") or from the linked host when the title is a sentence,
and the linked host wins when the title merely contains it, so "Deutscher
Starlink" and "Starlink" are one store. Codes come from "code: X" prose,
long numeric promo codes, or a link's promoCode/applyCode style parameter.

Five minutes is finer than GitHub's scheduler keeps, and the app has no
worker, so instrumentation.ts starts an in-process poller
(lib/reddit-poller.ts, REDDIT_SYNC_INTERVAL_MINUTES, 0 disables) at boot.
POST /api/sync/reddit is the keyless trigger, throttled to one run per
four minutes; the keepalive workflow calls it as a backstop, both CLIs'
sync commands call it, and the MCP server gains sync_reddit.

Verified: 33 tests (test/reddit-sync.test.mjs runs the real migration
against a local libSQL file with a fake Reddit answering 429 and a fake
relay), tsc, eslint on the touched files, next build, and a dry run of the
mapper over the live feed: 21 of 25 posts taken, 4 declined as intended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R63WmerrXfwcHkFRc61J2B
/* ------------------------------------------------------------------------ */

async function getState(db: SqlDb, key: string): Promise<string | null> {
const rows = await db.sql`SELECT value FROM sync_state WHERE key = ${key} LIMIT 1`;
website = COALESCE(stores.website, excluded.website),
logo_url = COALESCE(stores.logo_url, excluded.logo_url)
`;
const rows = await db.sql`SELECT id FROM stores WHERE slug = ${store.slug} LIMIT 1`;

/** Insert or update one row; says which. A found code is never replaced by a null one. */
async function upsertCoupon(db: SqlDb, storeId: number, row: RedditCouponRow): Promise<'inserted' | 'updated'> {
const existing = await db.sql`SELECT id FROM coupons WHERE source = ${row.source} AND source_id = ${row.source_id} LIMIT 1`;
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

27 finding(s)

MEDIUM: 25 | LOW: 2

Severity Rule Location
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:18
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:34
MEDIUM sql-template-interpolation apps/web/app/api/bounties/[id]/claim/route.ts:43
MEDIUM sql-template-interpolation apps/web/app/api/bounties/route.ts:81
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:68
MEDIUM sql-template-interpolation apps/web/app/api/coupons/route.ts:75
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:31
MEDIUM sql-template-interpolation apps/web/app/api/coupons/vote/route.ts:33
MEDIUM sql-template-interpolation apps/web/app/api/stores/[slug]/route.ts:9
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:83
MEDIUM sql-template-interpolation apps/web/app/api/webhooks/crawlproof/route.ts:90
MEDIUM js-open-redirect apps/web/app/bounties/new/page.tsx:57
MEDIUM js-unescaped-html-sink apps/web/app/layout.tsx:77
MEDIUM sql-template-interpolation apps/web/app/stores/[slug]/page.tsx:12
MEDIUM sql-template-interpolation apps/web/lib/mcp-tools.ts:79
MEDIUM sql-template-interpolation apps/web/lib/nichedb-sync.ts:186
MEDIUM sql-template-interpolation apps/web/lib/nichedb-sync.ts:206
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:336
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:355
MEDIUM sql-template-interpolation apps/web/lib/reddit-sync.ts:361
MEDIUM sql-template-interpolation apps/web/lib/reveal-coupon.ts:115
MEDIUM sql-template-interpolation apps/web/lib/reveal-coupon.ts:169
MEDIUM sh-predictable-temp-path apps/web/public/install.sh:37
MEDIUM sh-predictable-temp-path apps/web/public/install.sh:39
MEDIUM sql-template-interpolation apps/web/scripts/migrate.mjs:186
LOW sql-template-interpolation test/reveal-sweep.test.mjs:69
LOW sql-template-interpolation test/reveal-sweep.test.mjs:112

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 9fb59ea into master Sep 13, 2026
6 checks passed
@ralyodio
ralyodio deleted the worktree-reddit-couponcodes-source branch September 13, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants