Skip to content

Run an OpenAffiliate program, and join other merchants' programs from the same dashboard - #254

Merged
ralyodio merged 3 commits into
masterfrom
openaffiliate
Sep 13, 2026
Merged

ralyodio merged 3 commits into
masterfrom
openaffiliate

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

CrawlProof is the reference implementation of OpenAffiliate (spec: profullstack/logicsrc#166, logicsrc.com/docs/openaffiliate): one file a merchant serves about the commission it pays, and four calls that let a person or an agent earn it, with no network in the money.

The program we run

  • /.well-known/openaffiliate.json from one constants file, lib/affiliate/program.ts: 30% of a credits purchase within 30 days of a click, 30-day hold, USDC on Polygon weekly from $10, open approval, self-purchases refused. (Not the 60% the 2026-06 referral header promised: a credits pack pays publishers 1.4c of every 2c click, so 60% would be paid twice over.)
  • Click: proxy.ts sends a navigation carrying ?oa=<code> through /api/affiliate/v1/click, which records the click, sets the oa cookie and redirects back without the parameter. An image, frame, script or prefetch carrying it sets nothing (Sec-Fetch-Dest), which is the whole cookie-stuffing defence.
  • Attribution: sign-in (app/auth/callback) and invoice creation bind the user to the affiliate, last touch inside the window, never the affiliate's own account.
  • Conversion: completePurchase and the CoinPay webhook record it pending, idempotent on the purchase id, held_until 30 days out.
  • Cron (/api/cron/affiliate, hourly at :23): approve past the hold or reverse refunded ones with a reason (a reversal without a reason is a check constraint violation), deliver Ed25519-signed webhooks with backoff, pay on schedule, re-read stale merchants and ledgers.
  • Payout: affiliate_request_payout flips approved conversions to paid under a row lock, then createCryptoPayout; a failed send calls affiliate_fail_payout and puts them back. Probed live in a rolled-back transaction: pays only the approved rows, failure path restores them.

The affiliate side

Every user gets a membership on first use and an OpenProfile.md at /affiliate/u/<code>/openprofile.md, which is what they join other merchants with. /api/affiliate/v1/programs is a directory read from each merchant's own /.well-known/openaffiliate.json (verified above claimed), programs/join joins on the user's behalf and holds the merchant's token for them, joined shows every ledger on one page, events/<join> takes the merchant's webhooks.

Surfaces

/dashboard/affiliate, /affiliate, /affiliate/programs, /affiliate/terms, crawlproof affiliate link|ledger|pay|payout|programs|join|joined, seven MCP tools (affiliate_*), docs/affiliate.md. Three credentials reach the API: oa_ ledger token (the spec's), crp_ API token, session.

Migration

supabase/migrations/20260913120000_openaffiliate.sql: already applied to prod via the Supabase MCP (8 tables, 2 RPCs, cron crawlproof-affiliate). Before: 0 affiliate tables / functions / cron jobs. The cron 404s harmlessly until this deploys. Optional env OPENAFFILIATE_SIGNING_KEY (Ed25519 seed) signs webhooks; unset means unsigned, which the spec allows.

Gate

tsc --noEmit clean, vitest run 176 files / 2262 tests green (4 new files: spec parser, commission math, links, join parsing, OpenProfile reading, cookie + navigation check, webhook sign/verify, CLI), next build exit 0 with all 20 new routes in the tree.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CDEiDss9RWYibtmxSk5Gr2

… the same dashboard

CrawlProof is the reference implementation of OpenAffiliate
(logicsrc.com/docs/openaffiliate): one file a merchant serves about the
commission it pays, and four calls that let a person or an agent earn it
with no network in the money.

The program we run: /.well-known/openaffiliate.json from one constants file
(30% of a credits purchase within 30 days of a click, 30-day hold, USDC on
Polygon weekly from $10, open approval, self-purchases refused). A
navigation carrying ?oa=<code> goes through /api/affiliate/v1/click, which
records the click, sets the cookie and strips the parameter; an image,
frame or script carrying it sets nothing. Sign-in and invoice creation bind
the user to the affiliate; purchase completion records the conversion,
pending, idempotent on the purchase; the hourly cron approves past the
hold (or reverses refunded ones with a reason), delivers signed webhooks,
pays on schedule through CoinPay with debit-first/put-back-on-failure RPCs.

Join (public, with an OpenProfile.md), ledger (oa_ token, crp_ token or
session), me, token rotation, payout, jwks. Every one of our users gets a
membership on first use and an OpenProfile at /affiliate/u/<code>/openprofile.md,
which is what they join other merchants with: a directory of descriptors
read from each merchant's own origin, joins that hold the merchant's token
for the user, ledgers synced daily and on demand, inbound webhooks.

Surfaces: /dashboard/affiliate (link, balances, conversions, payouts,
joined programs, directory), /affiliate, /affiliate/programs,
/affiliate/terms, `crawlproof affiliate link|ledger|pay|payout|programs|join|joined`,
seven MCP tools, docs/affiliate.md. Migration 20260913120000_openaffiliate.sql.

The 2026-06 referral tables stay; nothing ever wrote a commission there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDEiDss9RWYibtmxSk5Gr2
Comment thread app/api/credits/create-invoice/route.ts Fixed
Comment thread app/auth/callback/route.ts Fixed
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

39 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread lib/affiliate/client.ts Fixed
Comment thread lib/affiliate/directory.ts Fixed
… CLI/MCP parity

CodeQL flagged the four things in this feature that were mine: fetching a
URL somebody typed (a merchant's descriptor, a profile, a join endpoint, a
ledger), one regex-in-a-message line, and two cookie readers that built a
RegExp from a variable. lib/affiliate/ssrf.ts now fronts every outbound
request: http(s) on a default port only, no loopback / link-local /
private names, every resolved address public, redirects followed by hand
with each hop re-checked and never on a POST. The cookie readers split
the header instead.

CLI gains `affiliate webhook` and `affiliate token --yes`; MCP gains
affiliate_set_webhook and affiliate_token, so every dashboard action has
an API, CLI and MCP twin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDEiDss9RWYibtmxSk5Gr2
Comment thread lib/affiliate/ssrf.ts Fixed
Comment thread lib/affiliate/ssrf.ts Dismissed
…ier regex

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDEiDss9RWYibtmxSk5Gr2
@ralyodio
ralyodio merged commit 60e8d25 into master Sep 13, 2026
10 checks passed
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