This document provides background context for AI assistants and developers working on this project.
Ceed Ads is a contextual advertising platform designed specifically for AI chat applications. Unlike traditional display advertising, it analyzes conversation context to serve relevant, non-intrusive ads that enhance rather than disrupt the user experience.
- Provide monetization for AI chatbot developers
- Deliver value to users through relevant, contextual ads
- Enable advertisers to reach users at moments of high intent
┌─────────────────┐ ┌─────────────────────────────────┐
│ Chat App │ │ Ceed Ads Backend (Next.js) │
│ (SDK Client) │────▶│ │
│ │ │ ┌────────────────────────────┐ │
│ @ceedhq/ │ │ │ /api/requests │ │
│ ads-web-sdk │ │ │ - Language detection │ │
│ │ │ │ - Keyword matching │ │
└─────────────────┘ │ │ - Cooldown control │ │
│ └────────────────────────────┘ │
│ │
│ ┌────────────────────────────┐ │
│ │ /api/events │ │
│ │ - Impression tracking │ │
│ │ - Click tracking │ │
│ └────────────────────────────┘ │
│ │
│ ┌────────────────────────────┐ │
│ │ Firebase Firestore │ │
│ │ - ads, advertisers │ │
│ │ - requests, events │ │
│ └────────────────────────────┘ │
└─────────────────────────────────┘
- Ad Format:
action_card- A text-based promotional card with CTA button - Languages: English (
eng) and Japanese (jpn) - Targeting: Keyword-based matching using ad tags
- Tracking: Impression and click events
- Frequency Control: 60-second cooldown per conversation
| Format | Status | Description |
|---|---|---|
action_card |
Live | Text card with title, description, CTA button |
lead_gen |
Live | Email capture form with success message |
static |
Live | Display ad below text input field (page load targeting) |
followup |
Live | Tappable sponsored question card with expand/redirect action |
Decision: Use franc for language detection on the server, not the client.
Why:
- Single source of truth for language
- Clients can't manipulate detection
- Consistent behavior across SDK versions
Decision: Match keywords as whole words only, not substrings.
Why:
- Avoids false positives (e.g., "daily" containing "ai")
- More predictable ad targeting
- Reduces advertiser confusion
Decision: Translate non-English text to English before keyword matching.
Why:
- Advertisers only need to define English keywords
- Consistent matching logic regardless of input language
- Easier to scale to new languages
Decision: 60-second cooldown per conversation after showing an ad.
Why:
- Prevents ad spam in active conversations
- Better user experience
- Matches natural conversation pacing
-
Language Support: Only English and Japanese currently supported. Other languages return no ads.
-
No User Authentication: The SDK accepts
userIdoptionally but does not validate it. -
No Advertiser Dashboard: Advertisers currently rely on direct Firestore access or API calls.
-
No Real-time Analytics: Event data is stored but not aggregated in real-time.
-
Four Ad Formats: action_card, lead_gen, static, and followup are all implemented and live.
This repository contains both the backend API and the Web SDK. The SDK is published separately to npm as @ceedhq/ads-web-sdk.
- Start the dev server:
npm run dev - Visit
/sdk-testfor the demo chat page - Use
initialize("demo-app", "/api")to point SDK to local API
// ads/{adId}
{
advertiserId: string,
format: "action_card",
title: { eng: string, jpn?: string },
description: { eng: string, jpn?: string },
ctaText: { eng: string, jpn?: string },
ctaUrl: string,
tags: string[],
status: "active" | "paused" | "archived",
meta: { createdAt: Date, updatedAt: Date }
}
// advertisers/{advertiserId}
{
name: string,
status: "active" | "suspended",
websiteUrl?: string,
meta: { createdAt: Date, updatedAt: Date }
}- Created comprehensive README.md with setup instructions and API reference
- Created CONTEXT.md with project background and architectural decisions
- Implemented v2 ad decision algorithm with intent-aware scoring
- Added opportunity scoring (0-1 scale) based on user context
- Added CPC-based ranking with fatigue penalty
- Added epsilon exploration (5% random from top-5)
- Added feature flags for gradual v1/v2 rollout
- Added in-memory caching for ads/advertisers (60s TTL)
- Word Boundary Matching: Changed from
includes()to regex/\b${keyword}\b/to prevent false positives (e.g., "hi" matching "this") - Intent Categories: sensitive, chitchat, low_intent, medium_commercial, high_commercial
- Thresholds: T_LOW=0.3 (no ad), T_HIGH=0.7 (all formats allowed)
- Feature Flag Control: V2_ENABLED, V2_APP_IDS, V2_PERCENTAGE env vars
- Fixed opportunityScorer.ts keyword matching bug (was matching "hi" in "this", "machine", "history")
- 171 unit tests (all passing)
- 19 E2E tests covering 8 real-world scenarios
- src/lib/ads/deciders/v2/* (7 files)
- src/lib/ads/cache/* (2 files)
- src/lib/ads/featureFlags.ts
- src/tests/e2e/adDecisionV2.e2e.test.ts
- acb198f: feat(ads): implement Ad Decision Algorithm v2
- Added
formatsparameter support to/api/requestsendpoint - Backend now filters ads by requested formats (v1 and v2 deciders)
- Created
/ad-showcasedemo page for screen recording - Updated SDK documentation for
staticformat positioning
A demo page displaying 4 separate chat rooms, each demonstrating one ad format:
- action_card: Language learning conversation, CTA opens URL in new tab
- lead_gen: Travel planning conversation, email form with success message
- static: Fitness goals conversation, fixed banner below input field
- followup: Cooking ideas conversation, tappable card with expand action
- Updated
staticformat description to match Koala Labs specification - Static ads now described as "below text input field" (not sidebars/headers)
- Added visual diagram showing input field + ad layout
- dc9ba46: feat(api): add formats parameter support to /api/requests