The Data Gap in Mobile Games

Most mobile games collect basic analytics: installs, sessions, purchases. But the real gold lies in the hundreds of micro-interactions players make every session — what level they reached, which items they bought, when they rage-quit, what time of day they play, how often they share on social media.

For CCFish, a Cocos Creator fishing game with a Telegram Mini App companion, this data gap was costing us revenue. We knew players who caught certain rare fish were 3x more likely to make in-app purchases — but our analytics pipeline couldn't trigger marketing actions based on in-game events in real-time.

The Architecture: Game Events → Marketing Actions

We built a server-side event pipeline that transforms raw Cocos Creator game events into structured marketing signals. Here's the full flow:

```

Game Client (Cocos Creator)

↕ WebSocket / HTTP

Event Ingestion API (Cloudflare Workers)

Event Buffer (D1, batch writes every 5s)

Event Processor (Workers Cron, every 60s)

↓ ↓ ↓

D1 Store Redis Cache Message Queue

Marketing Trigger Engine

↓ ↓ ↓

Offer Engine Push Notifications Telegram Bot

```

Step 1: Defining Custom Game Events

In Cocos Creator, we instrument key player actions with a lightweight event emitter. Each event follows a consistent schema:

```typescript

interface GameEvent {

eventName: string;

playerId: string;

sessionId: string;

timestamp: number;

properties: Record<string, string | number | boolean>;

gameVersion: string;

}

```

Example events CCFish tracks:

| Event Name | Trigger | Properties | Marketing Use |

|------------|---------|------------|---------------|

| `rare_catch` | Player catches a legendary fish | fishType, fishRarity, level | Push limited-time offer |

| `streak_ended` | Player misses a daily login | streakDays | Send re-engagement message |

| `level_retry` | Player retries a failed level | levelId, attempts | Offer power-up discount |

| `shop_browse` | Player opens shop but doesn't buy | section, timeSpent | Send price-drop alert |

| `social_share` | Player shares catch on Telegram | platform, friendCount | Trigger referral bonus |

Step 2: The Event Buffer (D1)

Raw events arrive at high volume (up to 100/sec during peak). Writing each to D1 individually would be expensive. Instead, we buffer events and batch-write every 5 seconds:

```typescript

const EVENT_BUFFER = new Map<string, GameEvent[]>();

const BATCH_INTERVAL = 5000; // 5 seconds

async function bufferEvent(event: GameEvent): Promise<void> {

const key = `${event.playerId}:${event.sessionId}`;

if (!EVENT_BUFFER.has(key)) {

EVENT_BUFFER.set(key, []);

}

EVENT_BUFFER.get(key)!.push(event);

}

// Cron: flush buffer every 5s

async function flushBuffer(): Promise<void> {

const stmt = env.DB.prepare(

`INSERT INTO game_events (player_id, session_id, event_name, properties, timestamp)

VALUES (?, ?, ?, ?, ?)`

);

for (const [key, events] of EVENT_BUFFER) {

const batch = events.map(e =>

stmt.bind(e.playerId, e.sessionId, e.eventName, JSON.stringify(e.properties), e.timestamp)

);

await env.DB.batch(batch);

}

EVENT_BUFFER.clear();

}

```

This reduces D1 writes by 95% while keeping data fresh enough for real-time marketing responses.

Step 3: The Event Processor (Cron-Driven)

Every 60 seconds, a Workers cron job processes the raw event buffer into actionable insights:

```typescript

export default {

async scheduled(event: ScheduledEvent, env: Env): Promise<void> {

// 1. Aggregate events from last 60 seconds

const recentEvents = await env.DB.prepare(`

SELECT player_id, event_name, COUNT(*) as count,

MAX(timestamp) as last_seen

FROM game_events

WHERE timestamp > ?

GROUP BY player_id, event_name

`).bind(Date.now() - 60000).all();

// 2. Evaluate marketing rules

for (const row of recentEvents.results) {

const triggers = await evaluateRules(row as EventRow);

for (const trigger of triggers) {

await env.QUEUE.send(trigger);

}

}

// 3. Update player segments

await updateSegments(recentEvents.results);

}

};

```

Marketing Rule Examples

Rules are stored in a D1 table and evaluated against the aggregated events:

```sql

-- Rule: Offer fishing bait discount after 3 session losses

INSERT INTO marketing_rules (trigger_event, conditions, action, priority) VALUES

('level_failed', '{"count": {"gte": 3}, "window": "session"}',

'{"type": "offer", "offerId": "bait_discount", "delay": 5000}', 50);

-- Rule: Send VIP offer after catching 5 rare fish in one day

INSERT INTO marketing_rules (trigger_event, conditions, action, priority) VALUES

('rare_catch', '{"count": {"gte": 5}, "window": "1d"}',

'{"type": "offer", "offerId": "vip_pass", "delay": 0}', 90);

```

Step 4: Multi-Channel Marketing Actions

Once a marketing rule triggers, the system can push to any channel:

```typescript

async function executeAction(action: MarketingAction, playerId: string): Promise<void> {

switch (action.type) {

case 'offer':

// Push in-game offer via WebSocket

await pushOffer(playerId, action.offerId);

break;

case 'notification':

// Send push notification

await sendPushNotification(playerId, action.message);

break;

case 'telegram':

// Notify via Telegram Mini App

await sendTelegramMessage(playerId, action.message);

break;

}

}

```

Real Results

| Metric | Before Pipeline | After Pipeline |

|--------|----------------|----------------|

| Time from event to action | 5-30 minutes (manual) | < 60 seconds |

| Marketing actions per day | 500 (batch) | 15,000+ (real-time) |

| Offer conversion rate | 2.1% | 5.3% |

| Player re-engagement rate | 8% | 22% |

| Revenue lift (first 30 days) | — | +34% |

Key Learnings

1. **Start with 3-5 event types.** Don't instrument everything at once. Pick the events that have the clearest marketing follow-up (rare catch → offer, streak end → reminder).

2. **Batch aggressively.** Raw event volume is high. Batching is necessary to keep D1 costs reasonable.

3. **Delay matters.** A 5-second delay on an offer after a rare catch feels natural — let the player enjoy the moment first.

4. **Events are perishable.** Old events (>30 days) should be archived or purged. We use a D1 cron job that moves events older than 30 days to R2.

Conclusion

CCFish's server-side event pipeline turns raw Cocos Creator game events into a real-time marketing engine — all running on Cloudflare Workers + D1 with near-zero operating cost. By connecting what players do in-game to when and how we market to them, we've unlocked a 34% revenue lift without increasing ad spend.

This is the essence of Hybrid Dev+Marketing: building infrastructure that serves both engineering and growth.