In-game economy
An economy event in AgentHog is an ordinary custom event with three conventional props. You declare which events source (add) or sink (remove) which currency, at a cost of one command per event, and from then on the economy is a report: per-currency sourced / sunk / net flow with supply growth, a paid-versus-earned split, ranked sources and sinks, breakdowns by any dimension, a daily series, and a health line. Virtual currency is distinct from real money, which is Revenue. The two meet exactly once, at the paid/earned split below.
Mappings are retroactive and reversible. The economy is computed from props already sitting in your events, so mapping today backfills every grant and spend you have ever sent. Nothing is re-ingested, no data is rewritten, no SDK method exists to migrate to, and ah economy unmap reverses any mapping. Instrument first, map whenever.
1. Send the events: three props
The convention is three props on any event that moves currency. Names are yours; the props are the contract:
| prop | meaning | rule |
|---|---|---|
currency | which denomination moved: gems, coins, energy. Your names, not ISO codes. | currencies exist because events name them; nothing is declared anywhere |
amount | how many units moved | always positive. Direction comes from the mapping (source or sink), never from the sign. A negative amount is counted as a sign disagreement in the health line, not silently flipped or dropped |
balance | the player's holding of that currency after this transaction | optional but recommended: it powers drift detection, the check that finds the source or sink you forgot to map |
// Unity, but the shape is the same from any SDK or a server-side relay
AgentHog.Capture("gems_purchased", new() {
["currency"] = "gems", ["amount"] = 500, ["balance"] = 650,
["cents"] = 499, ["pack"] = "starter" }); // real-money props ride the same event
AgentHog.Capture("level_reward", new() { ["currency"] = "coins", ["amount"] = 1200, ["balance"] = 8400, ["level"] = 12 });
AgentHog.Capture("item_bought", new() { ["currency"] = "gems", ["amount"] = 250, ["balance"] = 400, ["item"] = "sword_epic" });Aggregate rapid pickups
Send one event when the run ends, not one per coin. A player vacuuming 300 coins across a level should produce a single level_reward with amount: 300. The totals are identical, and your event volume (and bill) stays proportional to gameplay moments instead of frame-rate. The same goes for continuous sinks: batch an energy tick into one energy_spent per meaningful unit.
2. Map each event: one flag
Start by letting AgentHog propose the config. ah economy suggest scans the events you already send for economy-shaped props and prints ready-to-run commands, with the share of occurrences whose amount actually parses and every flow direction labelled as the guess it is.
ah economy suggest # propose mappings from events already sent ah economy suggest --apply # write every new proposal
A game following the prop convention maps each event with a single flag:
ah economy map --event gems_purchased --source ah economy map --event item_bought --sink
Defaults resolve when you run the command and are stored explicitly, so ah economy config always shows exactly what the numbers were computed from: every mapping fully resolved, with the command that recreates it. Unusual prop names never require renaming anything in the game; override per mapping:
| flag | what it does |
|---|---|
--amount <prop> | the prop holding the amount (default amount) |
--currency-prop <prop> | read the currency name per event (default: the currency prop). A typo'd value surfaces as its own report block; gemz next to gems is how you find it |
--currency <name> | pin a fixed name for events that carry no currency prop at all; alongside --currency-prop it is the per-row fallback |
--balance <prop> | the prop holding the post-transaction balance (default balance) |
--paid / --earned | override the paid/earned classification (see the two IAP shapes below) |
--match prop=value | only count events carrying that prop value, e.g. --match mode=live. An absent prop counts as a match, so history predating the prop is not silently dropped |
Bundles: one amount prop per currency
A starter pack that grants two currencies stays one event: carry one amount prop per currency and map each prop. Mappings are keyed by (event, amount prop), so both coexist, and unmap removes exactly one of them:
AgentHog.Capture("starter_pack", new() {
["gems_amount"] = 100, ["coins_amount"] = 5000, ["cents"] = 499 });ah economy map --event starter_pack --amount gems_amount --currency gems --source ah economy map --event starter_pack --amount coins_amount --currency coins --source
Paid vs earned: the two IAP shapes
Sourced currency splits into paid (bought with real money) and earned (granted by gameplay). Which side a source lands on is config, and the common case needs none:
Atomic IAP: dollars and currency on one event. The purchase event carries both the real-money prop and the currency props, and you map it twice, once as revenue and once as an economy source. The economy side then counts as paid automatically, because its event also carries a revenue mapping:
ah revenue map gems_purchased --amount cents --unit minor --currency USD ah economy map --event gems_purchased --source # paid: inferred, no flag needed
Deferred grant: purchase now, claim later. The dollars land on one event and the currency lands on another (a claim screen, a delivered gift). Map the purchase event as revenue, and mark the claim event paid explicitly:
ah revenue map gem_pack_purchased --amount cents --unit minor --currency USD ah economy map --event reward_claimed --source --paid
--paid is declared once, in config, never per call. The mirror override exists too: --earned forces a source to count as earned even when its event carries a revenue mapping.
3. Read it
ah economy # one block per currency ah economy --by platform # segment the flows ah economy --by flag:econ_ab # economy per A/B variant (live-ops tuning) ah economy --series # sourced/sunk/net per day ah economy config # every mapping, fully resolved
ECONOMY — finalgame · 7d · gems sourced 1,100 paid 91% · earned 9% sunk 250 net flow +850 supply +85% TOP SOURCES event amount events players -------------- ------ ------ ------- gems_purchased 1,000 2 2 level_reward 100 1 1 TOP SINKS event amount events players ----------- ------ ------ ------- item_bought 250 2 2 HEALTH unparseable: 1 · sign disagreements: 1 · balance drift >5%: 66.7% of players (balance on 3 of 3 players)
| number | definition |
|---|---|
| sourced | units of this currency added, with its paid/earned split |
| sunk | units removed |
| net flow | sourced − sunk. Positive means the supply is inflating; supply % is net over the cumulative net of all history before the window |
| TOP SOURCES / SINKS | every mapped event that moved this currency, ranked by amount, with event and player counts |
Amounts are never summed across currencies: a two-currency game gets two blocks, because 1,000 gems plus 5,000 coins is not a number. Breakdowns follow the same attribution rule as revenue. A dimension the event itself carries (props.<key>) is read from the event and never inherited, while a person-level dimension (platform, country, flag:<key>) falls back to the player's own value, which is what makes a server-granted reward attributable to a platform or an experiment variant at all.
Every verb on this page takes --json, and the output is identical to the HTTP API payload. ah economy --help carries the full flag list.
The health line
Instrumentation gaps skew an economy silently, so the report always ends with a health line. Every figure is a gap made loud, never a correction applied:
| figure | what it means |
|---|---|
unparseable | mapped events whose amount prop was missing or not a number. Counted and excluded from totals, never silently zeroed. |
sign disagreements | a negative amount on a mapped event. Amounts are sent positive; the row still counts (as its absolute value, on the mapped side), and this counter tells you the game is encoding direction in the sign. |
balance drift | the share of balance-reporting players whose latest reported balance disagrees with their summed mapped flows by more than 5%. Drift means you forgot to map a source or sink; run ah economy suggest again. Balances are what the game reports, by design: AgentHog never derives them server-side, so drift stays a trustworthy signal instead of being reconciled away. Players who report no balance are excluded from the denominator, and the coverage is printed alongside. Unmeasured is never shown as 0% drift. |
Suggested event names
Conventional, unenforced: adopt these names and reports populate, and ah economy suggest reads the flow direction straight from almost every one of them, because the name carries a word its guesser recognizes (a bundle like starter_pack is instead guessed from the real-money prop it usually carries). Ignore them and nothing breaks; the mapping is what matters.
| event | flow | note |
|---|---|---|
gems_purchased | source | hard currency bought with real money; map the same event as revenue too and the grant counts as paid |
reward_claimed | source | deferred grant landing after a purchase-now/claim-later flow; map with --paid |
currency_granted | source | a server-side grant (support comp, promo, migration) |
level_reward | source | end-of-level payout; aggregate the pickups into this one event |
quest_reward | source | quest or mission completion payout |
daily_bonus | source | login streak / calendar bonus |
achievement_reward | source | one-time achievement payout |
chest_opened | source | loot container yield |
starter_pack | source | bundle: one amount prop PER currency (gems_amount, coins_amount), one mapping each |
item_bought | sink | shop purchase paid in virtual currency |
upgrade_bought | sink | upgrade or unlock paid in virtual currency |
crafting_spent | sink | crafting / forging cost |
energy_spent | sink | consumable spend (energy, stamina, tickets) |
timer_skipped | sink | hard-currency spend to skip a wait |
What this does not do
- No exchange rates between currencies. Flows stay per currency; converting gems to coins is a game-design question, not an analytics one.
- No items or inventory. Economy tracks fungible currencies. A sword is not 250 gems; the
item_boughtsink records what you paid, not what you got. - No server-derived balances. The
balanceprop is the game's own report, and drift against summed flows is a first-class signal. An analytics tool that "fixes" the discrepancy would be hiding your missing mapping.
Next
Real-money purchases belong on the Revenue & LTV page; server-side grants ride the server-to-server relay; Unity setup is in the Unity guide.