Skip to content

Commit ef94e9d

Browse files
jahoomaclaude
andcommitted
Add Carbon (BuySellAds) ad provider for waiting room
Introduces a pluggable ad-provider abstraction (gravity + carbon) on the server so adding ZeroClick later is a single-file drop-in. The Freebuff waiting room now fetches Carbon ads; in-chat ads continue to use Gravity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1c92cf9 commit ef94e9d

14 files changed

Lines changed: 3833 additions & 253 deletions

File tree

cli/src/components/waiting-room-screen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
7373
// Always enable ads in the waiting room — this is where monetization lives.
7474
// forceStart bypasses the "wait for first user message" gate inside the hook,
7575
// which would otherwise block ads here since no conversation exists yet.
76+
// Uses Carbon (BuySellAds); in-chat ads still use the Gravity default.
7677
const { ad, adData, recordImpression } = useGravityAd({
7778
enabled: true,
7879
forceStart: true,
80+
provider: 'carbon',
7981
})
8082

8183
useFreebuffCtrlCExit()

cli/src/hooks/use-gravity-ad.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then p
1616
const ACTIVITY_THRESHOLD_MS = 30_000 // 30 seconds idle threshold for fetching new ads
1717
const MAX_AD_CACHE_SIZE = 50 // Maximum number of ads to keep in cache
1818

19-
// Ad response type (matches Gravity API response, credits added after impression)
19+
// Ad response type (normalized shape across providers; credits added after impression)
2020
export type AdResponse = {
2121
adText: string
2222
title: string
@@ -30,6 +30,12 @@ export type AdResponse = {
3030

3131
export type AdVariant = 'banner' | 'choice'
3232

33+
/**
34+
* Which upstream ad network to query. The server maps each provider onto the
35+
* same normalized response shape, so the rest of the hook is provider-agnostic.
36+
*/
37+
export type AdProvider = 'gravity' | 'carbon'
38+
3339
export type AdData =
3440
| { variant: 'banner'; ad: AdResponse }
3541
| { variant: 'choice'; ads: AdResponse[] }
@@ -102,9 +108,12 @@ export const useGravityAd = (options?: {
102108
/** Skip the "wait for first user message" gate. Used by the freebuff
103109
* waiting room, which has no conversation but still needs ads. */
104110
forceStart?: boolean
111+
/** Which ad network to query. Defaults to Gravity. */
112+
provider?: AdProvider
105113
}): GravityAdState => {
106114
const enabled = options?.enabled ?? true
107115
const forceStart = options?.forceStart ?? false
116+
const provider: AdProvider = options?.provider ?? 'gravity'
108117
const [ad, setAd] = useState<AdResponse | null>(null)
109118
const [adData, setAdData] = useState<AdData | null>(null)
110119
const [isLoading, setIsLoading] = useState(false)
@@ -159,7 +168,7 @@ export const useGravityAd = (options?: {
159168

160169
const authToken = getAuthToken()
161170
if (!authToken) {
162-
logger.warn('[gravity] No auth token, skipping impression recording')
171+
logger.warn('[ads] No auth token, skipping impression recording')
163172
return
164173
}
165174

@@ -179,7 +188,7 @@ export const useGravityAd = (options?: {
179188
if (data.creditsGranted > 0) {
180189
logger.info(
181190
{ creditsGranted: data.creditsGranted },
182-
'[gravity] Ad impression credits granted',
191+
'[ads] Ad impression credits granted',
183192
)
184193
setAd((cur) =>
185194
cur?.impUrl === impUrl
@@ -205,7 +214,7 @@ export const useGravityAd = (options?: {
205214
}
206215
})
207216
.catch((err) => {
208-
logger.debug({ err }, '[gravity] Failed to record ad impression')
217+
logger.debug({ err }, '[ads] Failed to record ad impression')
209218
})
210219
}
211220

@@ -235,7 +244,7 @@ export const useGravityAd = (options?: {
235244

236245
const authToken = getAuthToken()
237246
if (!authToken) {
238-
logger.warn('[gravity] No auth token available')
247+
logger.warn('[ads] No auth token available')
239248
return null
240249
}
241250

@@ -277,16 +286,21 @@ export const useGravityAd = (options?: {
277286
Authorization: `Bearer ${authToken}`,
278287
},
279288
body: JSON.stringify({
289+
provider,
280290
messages: adMessages,
281291
sessionId: useChatStore.getState().chatSessionId,
282292
device: getDeviceInfo(),
293+
// Carbon requires a real browser-ish useragent for targeting/fraud
294+
// detection. Gravity ignores it. We source one centrally so every
295+
// provider that needs it sees the same value.
296+
userAgent: getAdUserAgent(),
283297
}),
284298
})
285299

286300
if (!response.ok) {
287301
logger.warn(
288-
{ status: response.status, response: await response.json() },
289-
'[gravity] Web API returned error',
302+
{ provider, status: response.status, response: await response.json() },
303+
'[ads] Web API returned error',
290304
)
291305
return null
292306
}
@@ -304,7 +318,7 @@ export const useGravityAd = (options?: {
304318

305319
return null
306320
} catch (err) {
307-
logger.error({ err }, '[gravity] Failed to fetch ad')
321+
logger.error({ err }, '[ads] Failed to fetch ad')
308322
return null
309323
}
310324
}
@@ -465,3 +479,22 @@ function getDeviceInfo(): DeviceInfo {
465479

466480
return { os, timezone, locale }
467481
}
482+
483+
/**
484+
* Useragent string passed to ad providers. Carbon (BuySellAds) requires a
485+
* plausible browser useragent for targeting and fraud screening. We send a
486+
* stable desktop Chrome-on-{os} UA per platform so targeting is consistent
487+
* across users on the same platform without sharing anything identifying.
488+
*
489+
* Chrome version needs bumping periodically — stale UAs look bot-ish to ad
490+
* networks. Last bumped: 2026-04-21. Revisit roughly every 6 months.
491+
*/
492+
const AD_CHROME_VERSION = '124.0.0.0'
493+
function getAdUserAgent(): string {
494+
const osUA: Record<string, string> = {
495+
darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${AD_CHROME_VERSION} Safari/537.36`,
496+
win32: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${AD_CHROME_VERSION} Safari/537.36`,
497+
linux: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${AD_CHROME_VERSION} Safari/537.36`,
498+
}
499+
return osUA[process.platform] ?? osUA.linux
500+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE "ad_impression" ALTER COLUMN "payout" DROP NOT NULL;--> statement-breakpoint
2+
ALTER TABLE "ad_impression" ADD COLUMN "provider" text DEFAULT 'gravity' NOT NULL;--> statement-breakpoint
3+
ALTER TABLE "ad_impression" ADD COLUMN "extra_pixels" text[];

0 commit comments

Comments
 (0)