Most game studios think they have a segmentation problem. They don't — they have a timing problem.
I've seen this pattern over and over: the data is there, the dashboards look good, the "whale / dolphin / minnow" buckets exist. But none of it fires fast enough to actually influence a player's decision during an active session. By the time a batch job reclassifies someone as "at-risk," they've already uninstalled.
This post walks through the architectural approach behind Auxta DB — a real-time behavioral classification engine we built specifically to fix this — and shows what the actual integration looks like in TypeScript.
The Core Problem: Batch Pipelines vs. Session Windows
Here's the brutal truth about rule-based segmentation in live games:
Daily batch job fires at 2am
→ Player classified as "at-risk" at 2:03am
→ Push notification queued
→ Player opens app at 9am, already disengaged
→ Notification fires into the past
The monetization moment and the churn signal are session-level events. Classification that doesn't operate within the session is just reporting dressed up as decisioning.
The three failure modes we see consistently:
- Coarseness — "whale" describes two completely different players who need opposite treatment
- Latency — batch pipelines run on a different timescale than player decisions
- Fragmentation — behavioral data lives in 3–4 separate systems, never assembled in real time
The Architecture: Players as Behavioral Vectors
Auxta represents every player as a multi-dimensional vector — a live, structured snapshot of every behavioral parameter relevant to your game. Not a fixed schema. Not a row in a table. A vector that updates in real time as gameplay events occur.
Here's what a player vector definition looks like:
// PlayerVector.ts — defines the full behavioral profile for a player.
// Static fields (country, acquisitionSource) are set on registration.
// Computed fields (winRateLast10, consecutiveLosses, etc.)
// are updated by the game server on every session event — no batch job required.
class PlayerVector extends AuxtaVector {
country: string;
region: string;
acquisitionSource: string; // 'organic' | 'paid_ua' | 'influencer' | ...
totalSpend: number; // lifetime, updated on every purchase event
totalRounds: number; // lifetime, incremented per round completion
lastLoginTimestamp: number; // unix ms, updated on every session open
sessionFrequency7d: number; // computed: sessions in rolling 7-day window
averageSessionLength: number; // computed: minutes, rolling 30-day average
winRateLast10: number; // computed: 0.0–1.0, last 10 sessions
consecutiveLosses: number; // live: resets on win, increments on loss
currentLevel: number; // live: updated on level completion event
lastInGameAction: string; // live: 'shop_open' | 'level_fail' | 'ad_watch' | ...
preferredGameMode: string[]; // computed: top modes by session share last 14d
lastPurchasedItemCategory: string;
daysSinceLastPurchase: number;
churnRiskScore: number; // 0.0–1.0, recomputed on each session close
predictedLTV: number;
}
Upserting a player profile on session close is a single typed operation:
const playerVector = new PlayerVector({
country: 'DE',
totalSpend: 142.50,
totalRounds: 387,
lastLoginTimestamp: Date.now(),
sessionFrequency7d: 6,
winRateLast10: 0.4,
consecutiveLosses: 3,
lastInGameAction: 'level_fail',
preferredGameMode: ['pvp', 'ranked'],
churnRiskScore: 0.61,
predictedLTV: 280,
// ... other dimensions
});
const updateCommand = new AuxtaCommand().add(playerVector);
await auxta.query(updateCommand);
// Player is now queryable across all 100+ dimensions with zero propagation delay
The Query That Makes It Real
Here's where it gets interesting. Instead of waiting for a batch job, the game server issues a typed search query during the active session — resolving in under 0.2ms.
This fires in real time when the server records consecutiveLosses === 3:
// Fired in real time when the game server records consecutiveLosses === 3.
// Resolves in under 0.2ms. Result drives immediate offer injection
// into the post-round screen — no async pipeline, no delayed push.
const recoveryOfferSegment = new AuxtaCommand()
.search(PlayerVector)
.where('consecutiveLosses', losses => losses.gte(3))
.where('daysSinceLastPurchase', days => days.gte(3).and().lte(14))
.where('lastInGameAction', action => action.match('level_fail'))
.where('churnRiskScore', risk => risk.gte(0.5).and().lte(0.8))
.where('preferredGameMode', mode => mode.in(['pvp', 'ranked']));
const eligiblePlayers = await auxta.query<PlayerVector>(recoveryOfferSegment);
Notice what this query combines in a single operation:
- A stable dimension (
preferredGameMode— computed preference over 14 days) - A slowly-changing dimension (
daysSinceLastPurchase— monetization recency) - Two live computed dimensions (
consecutiveLossesandlastInGameAction— present-tense gameplay state)
Same approach for premium offer targeting:
const premiumOfferSegment = new AuxtaCommand()
.search(PlayerVector)
.where('totalSpend', spend => spend.gte(50))
.where('sessionFrequency7d', freq => freq.gte(5))
.where('winRateLast10', rate => rate.gte(0.6))
.where('daysSinceLastPurchase', days => days.gte(5))
.where('acquisitionSource', src => src.in(['organic', 'influencer']))
.where('lastPurchasedItemCategory', cat =>
cat.match('booster').or().match('currency_pack')
);
This surfaces players who are: established spenders, highly engaged this week, on a winning streak, purchase-lapsed enough to be receptive, from high-quality acquisition sources, and with a purchase history in high-margin categories — all in one sub-millisecond query.
The Reindexing Problem Nobody Talks About
Here's a technical constraint that kills most traditional approaches: reindexing.
When a new game mode launches, a new economy mechanic ships, or a new event type becomes relevant — any column-store or document database with a fixed index structure needs a reindex operation. In a live game with millions of player records, that reindex takes hours.
For a seasonal event launch where the new mechanic is the entire basis of the LiveOps campaign, that's not a minor inconvenience — it forces teams to either plan campaigns around database maintenance windows or accept degraded targeting at launch.
Auxta's vector model eliminates this entirely. Because player data is stored as dimensional vectors rather than rows with fixed column indexes, adding a new parameter requires no reindex operation. The new dimension is immediately available for queries after the first player vector is written with it populated. Existing players without the new parameter simply go unmatched on that dimension — no errors, no migration job.
Integration Model
Auxta slots into an existing gaming stack without a data platform overhaul:
- Game server → Auxta: Write vector updates on session events (session open, round complete, purchase, session close)
- Offer engine / LiveOps platform → Auxta: Query for segment membership at decisioning moments (shop open, post-round screen, push trigger, event eligibility)
The Auxta UI lets LiveOps managers modify segment definitions directly — dimensional thresholds, combination operators, inclusion/exclusion conditions — without engineering involvement. Changes take effect on the next query. No deployment cycle. No pipeline reconfiguration.
What This Fixes (With Numbers)
The revenue inefficiency from coarse, batch-based segmentation typically sits between 10–30% of addressable revenue across a live game — not theoretical, but as a measured gap between what current segmentation-driven decisions produce vs. what the same player base generates under correctly timed classification.
Closing the three gaps (coarseness, latency, fragmentation) produces:
- ARPU improvement — offer type, value, and timing match the player's current behavioral state, not a stale archetype
- LTV improvement — retention triggers fire on behavioral signal, not schedule
- CAC payback shortens — newly acquired players are classified from their first session, not held in "new user" limbo for 30 days
- Bonus efficiency improves — precise micro-segmentation excludes players whose behavioral profile makes them unlikely to convert regardless of incentive
The bottom line: weak player segmentation isn't a data problem. It's a decision-timing problem caused by the absence of real-time behavioral classification. That's exactly what Auxta DB was built for.
If you're running a live game at scale and want to evaluate the integration, the ADAAS team runs scoped technical assessments — typically two weeks from kickoff to live classification in production.

Top comments (0)