Why We Killed Hold Windows in Our Affiliate Marketplace
Source: Dev.to
Overview
We spent weeks building a settlement system for our affiliate marketplace—hold windows, clawbacks, carry‑forwards, commission auto‑approval schedulers, the works. Then we deleted it all.
The idea was straightforward: when an affiliate drives a conversion, don’t pay them immediately. Hold the commission for X days. If the customer refunds, claw back the commission. If there’s a remainder below the payout threshold, carry it forward to the next month. Sounds reasonable, right? Every major affiliate network does something like this.
Implementation we built
- Hold windows — configurable per campaign (7, 14, 30 days)
- Clawback logic — refunds during the hold period reduce the affiliate’s balance
- Carry‑forwards — sub‑threshold amounts roll to the next settlement period
- Auto‑approval scheduler — commissions move from HELD → APPROVED after the hold window
Why we scrapped it
Legal review flagged the feature as a regulatory risk:
- Money‑transmission concerns – holding and releasing funds on a schedule can be considered money transmission in some jurisdictions.
- Dispute‑resolution requirements – clawbacks need a formal dispute process, not just an automatic deduction.
- Accounting complexity – carry‑forwards create accrued liabilities that require proper bookkeeping.
- Tax reporting – uncertainty about when the income is earned (conversion, hold expiry, or payout).
We’re a URL shortener that added an affiliate marketplace, not a payment processor. Building the compliance infrastructure for hold windows would have cost more than the feature was worth.
Deleted assets
// CommissionAutoApprovalScheduler.java (deleted)
holdWindowDaysfield (removed from campaigns)clawbackAmount,previousCarryForward(removed from settlements)- HELD commission status (removed)
What we replaced it with
- Firm offers – brands mark campaigns as non‑negotiable; publishers accept the commission as‑is.
- Immediate settlement – conversions are confirmed by Stripe webhooks. When Stripe says the charge succeeded, the commission is earned.
- Monthly payouts – simple monthly settlement with no holds. If there’s a refund, the brand absorbs it (they can adjust their commission rates accordingly).
The simplification freed up time for features that actually matter:
- Partnership lifecycle – pause, resume, terminate partnerships with full event tracking.
- Multi‑channel notifications – email, in‑app, and push notifications for partnership events.
- Campaign budgets and expiry – brands set a maximum spend and end date; campaigns auto‑pause when limits are hit.
- Firm offers – skip the negotiation dance when the brand knows what it wants to pay.
Metrics
| Metric | Before | After |
|---|---|---|
| Settlement‑related DB tables | 5 | 2 |
| Commission statuses | 6 (PENDING, HELD, APPROVED, CLAWED_BACK, PAID, FAILED) | 3 (PENDING, APPROVED, PAID) |
| Settlement logic (lines) | ~800 | ~200 |
| Legal questions | Many | Few |
Takeaways
- Legal review before building – ask “can we hold affiliate funds?” before writing a single line of code.
- Complexity is a liability – every line of settlement logic is a potential bug, legal issue, and support ticket. Less code = less risk.
- Copy the leader carefully – “Amazon Associates holds windows” doesn’t mean you should; Amazon has a legal team.
- Simpler products attract more users – publishers want to drive traffic and get paid, not learn about hold windows and carry‑forwards.
- KISS isn’t lazy, it’s strategic – deleting working code can be the highest‑ROI engineering decision.
Ever deleted a feature you spent weeks building? What was the hardest “kill your darlings” moment in your product? Share below.
Building jo4.io – a URL shortener with an affiliate marketplace that pays publishers without the complexity.