This article was originally published on Jo4 Blog.
A brand creates an affiliate campaign on your platform. A publisher starts promoting it. Neither of them has acknowledged FTC disclosure requirements. Congratulations, you're now potentially liable.
The FTC requires that affiliate relationships be disclosed. Not "should be." Required. And if your platform makes it easy to skip that disclosure, you're part of the problem.
We added FTC disclosure gates to jo4 in 235 lines. Here's what we did and why the implementation details matter more than you'd think.
TL;DR
Campaign activation now requires FTC acknowledgment. Try to activate without it, you get a 400. Acknowledge first, then activate. Two API calls, one legal obligation met. Server-side enforcement, not a client-side checkbox.
The Flow
Three API calls, in order:
POST /campaigns/{slug}/activate
→ 400 Bad Request: "FTC disclosure acknowledgment required"
POST /campaigns/{slug}/ftc-acknowledge
→ 200 OK
POST /campaigns/{slug}/activate
→ 200 OK (status: ACTIVE)
The first activate call fails because ftcAcknowledged is false. The brand must explicitly call the acknowledgment endpoint. Then — and only then — can the campaign go active.
Why Not a Client-Side Checkbox?
The obvious implementation: add a checkbox to the campaign creation form. "I acknowledge FTC disclosure requirements." Required field. Done, right?
No. Here's why:
1. A checkbox can be bypassed.
Any browser extension, DOM manipulation, or automated script can check a box. The form submits with ftcAcknowledged: true without the user ever seeing the checkbox. You have zero proof of informed acknowledgment.
2. It doesn't protect API-only integrations.
Brands using Zapier, Pipedream, or direct API calls to create and activate campaigns never see your UI. If the gate is in the frontend, they skip it entirely. This isn't a hypothetical — we already have users creating campaigns via the jo4 API.
3. It doesn't survive SPA refactors.
You redesign the campaign form. A developer removes the checkbox because "we'll add it back in the new design." Or the checkbox component's state gets reset on a re-render. Or a feature flag accidentally hides it. Client-side gates are fragile because they depend on UI code that changes constantly.
4. It's not auditable.
With a server-side gate, we have a database record: who acknowledged, when, from which IP. With a checkbox, we have a form field that may or may not have been rendered when the user submitted.
The Implementation
The campaign entity gets a new field:
@Column(nullable = false)
private boolean ftcAcknowledged = false;
The activation service checks it:
public CampaignDTO activate(String slug, Long userId) {
CampaignEntity campaign = findCampaignBySlugAndOwner(slug, userId);
if (!campaign.isFtcAcknowledged()) {
throw new BadRequestException("FTC disclosure acknowledgment required");
}
campaign.setStatus(CampaignStatus.ACTIVE);
return mapper.toDTO(campaignRepository.save(campaign));
}
The acknowledgment endpoint:
public CampaignDTO acknowledgeFtc(String slug, Long userId) {
CampaignEntity campaign = findCampaignBySlugAndOwner(slug, userId);
campaign.setFtcAcknowledged(true);
campaign.setFtcAcknowledgedAt(Instant.now());
return mapper.toDTO(campaignRepository.save(campaign));
}
That's it. The gate is three lines in the activation method. The acknowledgment is four lines. The rest is tests.
The Tests: GAP-N Audit Mapping
Our E2E tests for this feature use a specific comment format:
// GAP-1: Campaign cannot be activated without FTC acknowledgment
@Test
void activate_without_ftc_returns_400() {
// Create campaign, attempt activation
var response = activateCampaign(campaignSlug);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// GAP-2: FTC acknowledgment endpoint returns 200
@Test
void ftc_acknowledge_returns_200() {
var response = acknowledgeFtc(campaignSlug);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
// GAP-3: Campaign activates after FTC acknowledgment
@Test
void activate_after_ftc_acknowledge_succeeds() {
acknowledgeFtc(campaignSlug);
var response = activateCampaign(campaignSlug);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getStatus()).isEqualTo("ACTIVE");
}
// GAP-4: FTC acknowledgment persists across sessions
@Test
void ftc_acknowledged_survives_re_fetch() {
acknowledgeFtc(campaignSlug);
var campaign = getCampaign(campaignSlug);
assertThat(campaign.isFtcAcknowledged()).isTrue();
}
The GAP-N prefix maps directly to our compliance audit document. When an auditor asks "how do you enforce FTC disclosure?", we point them to GAP-1 through GAP-4 and their corresponding test results. Each gap identified in the audit maps to a passing test.
This is a pattern I'd recommend for any compliance feature: name your tests after the audit finding they address. It creates a traceable chain from "regulatory requirement" to "passing CI check."
235 Lines. That's It.
The full diff:
- 1 new boolean column on
campaignstable - 1 new timestamp column (
ftcAcknowledgedAt) - 1 new endpoint (
POST /ftc-acknowledge) - 3 lines added to the activation method
- 4 E2E tests
- Migration script
235 lines of changes. The smallest feature we shipped that quarter, and the one with the highest legal impact.
The Lesson
Compliance features feel like checkbox exercises. Literally — most people implement them as checkboxes. But the implementation details determine whether you're actually compliant or just performing compliance.
Server-side gates are:
- Enforceable: works regardless of client (web, mobile, API, automation)
- Auditable: database record with timestamp
- Durable: survives UI refactors and redesigns
- Testable: E2E tests prove the gate works, mapped to audit findings
A client-side checkbox is none of these things.
If your platform has any compliance requirement — FTC disclosure, GDPR consent, age verification — implement it as a server-side gate. It's the same amount of code. It's infinitely more defensible.
How do you handle compliance gates in your platform? Server-side, client-side, or something else? I'm curious how other indie devs approach this.
Building jo4.io — affiliate infrastructure that takes compliance seriously, even at 235 lines.
Top comments (0)