Buy-Features
Buy-features let players purchase direct entry into a bonus feature (e.g. freespins) for a fixed price, bypassing the base-game trigger. The generator's job is to produce the selection pool each purchase draws from, and to write the matching pricing configuration into config.json.
This page covers how to define buy-features at generation time. For how the engine serves them at runtime, see SDK · Buy-Features.
Two parts of a buy-feature
A buy-feature in the generator has two pieces:
- Selection pool — an
IBuyFeatureEntry. Atend()the generator materialises it intoentries.jsonlas a synthetic feature namedbf_<sanitized-id>. This is the list of entries (with weights) the engine draws from when a player buys the feature. - Pricing config — an
IBuyFeatureConfigattached toconfig.buyFeaturesinconfig.json. This tells the engine the feature'stargetPrice,targetRtp, andinitialSpins.
You generally want both — one without the other is either an unpriced pool or a priced feature with nothing to select from.
Wiring the pricing config to the pool
Each resolved pool becomes a feature named bf_<sanitized-id> in entries.jsonl (hyphens and spaces become underscores, e.g. buy-freespin → bf_buy_freespin). Set IBuyFeatureConfig.feature to this name so the engine's placeBet draws from the pool when a player buys.
Scenarios are shared, not duplicated
A bf_<id> pool entry reuses its source entry's id, and scenarios are keyed by entryId (not by feature). So materialising a pool adds only the lightweight weight rows to entries.jsonl — the scenario data is stored once and shared. A pool entry can therefore re-weight an existing outcome but never invent a new one.
Resolution strategy: entrypool
Buy-feature pools are built by resolving an IBuyFeatureDefinition against the entries your simulation has recorded. The entrypool strategy includes only entries with at least one matching metaTag — the typical pattern for a "buy directly into the bonus" feature: tag your trigger entries, then pool them.
// All entries tagged 'freespin-trigger' form the pool.
// Each entry keeps its original weight (scaled by metaTagWeights, if set).
{ id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] }metaTagWeights
Scales tagged entries' weights by a per-tag multiplier, letting you weight one tag's entries relative to another within the pool. An entry matching several selected tags uses the highest weighting; unlisted tags default to 1.
// Pool = all 'trigger' and 'jackpot' entries. Within it, 'jackpot' entries are
// weighted 5× relative to plain 'trigger' entries.
{ id: 'buy-bonus', type: 'entrypool', metaTags: ['trigger', 'jackpot'], metaTagWeights: { 'jackpot': 5 } }TIP
A uniform multiplier over a single-tag pool has no effect on selection — every pool entry scales equally, so their relative probabilities are unchanged (and the total scales with them). metaTagWeights only changes outcomes when the pool spans more than one tag. To re-weight individual entries, use weightOverrides.
weightOverrides
Replaces computed weights for specific tagged entries. Useful for fine-tuning the pool's RTP after inspection:
{
id: 'buy-freespin',
type: 'entrypool',
metaTags: ['freespin-trigger'],
weightOverrides: { '3': 500, '7': 200 }, // entry IDs → explicit weight
}Pool scope
Definitions resolve against all entries in the generator, across every feature — an entrypool matches tagged entries regardless of which feature they belong to. If you want a single-feature pool, tag only entries from that feature.
Setting them up
There are two equivalent ways to register a buy-feature with the generator. Pick whichever fits your flow.
Option A — inline via end({ buyFeatureDefinitions })
Pass definitions directly to end(); the generator resolves them against the recorded entries and materialises everything in one shot.
import { HiziEngineGenerator } from '@hizi.io/engine-generator';
import type { IBuyFeatureDefinition } from '@hizi.io/engine-generator';
const gen = new HiziEngineGenerator();
await gen.start('./output/');
// ...run your simulation, calling gen.addResult(...) with metaTags...
const buyFeatureDefinitions: IBuyFeatureDefinition[] = [
{ id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] },
{ id: 'buy-boost', type: 'entrypool', metaTags: ['big-win'] },
];
await gen.end({
config: {
gameCode: 'my-slot',
gameType: 'slot',
stakes: [1.0],
features: ['freespin'],
buyFeatures: [
{ id: 'buy-freespin', feature: 'bf_buy_freespin', targetRtp: 95, targetPrice: 100 },
{ id: 'buy-boost', feature: 'bf_buy_boost', targetRtp: 95, targetPrice: 20 },
],
},
buyFeatureDefinitions,
});Option B — explicit via buildBuyFeatures()
Resolve the pool yourself before end(), inspect or post-process it, then pass the resolved IBuyFeatureEntry[] as end({ buyFeatures }).
const buyFeatures = gen.buildBuyFeatures([
{ id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] },
{ id: 'buy-boost', type: 'entrypool', metaTags: ['big-win'] },
]);
// Optional: inspect or adjust buyFeatures here
await gen.end({
config: { /* …as above… */ },
buyFeatures,
});buildBuyFeatures() is pure — it resolves and returns the pools without writing anything. The pools are materialised into entries.jsonl only when you pass them to end() (as buyFeatures, or by handing the definitions straight to end({ buyFeatureDefinitions })).
Use Option A for simplicity and Option B when you need to see the resolved pool before committing to it (e.g. to log per-entry weights, compute the buy-feature's RTP, or merge with hand-written entries).
Mixing pre-resolved and auto-resolved
end() accepts both buyFeatures and buyFeatureDefinitions in the same call. Resolved definitions are appended to the pre-resolved list, so you can hand-write a pool for one feature and auto-resolve another. Both land in entries.jsonl as bf_<id> features.
Full example
A minimal simulation with a freespin feature plus two buy-features. This is runnable end-to-end.
import { HiziEngineGenerator } from '@hizi.io/engine-generator';
import type { IBuyFeatureDefinition, IEndOptions } from '@hizi.io/engine-generator';
const gen = new HiziEngineGenerator({ maxScenariosPerEntry: 5 });
await gen.start('./output/');
// ── Basegame ──
for (let i = 0; i < 600; i++)
gen.addResult({ reels: [0, 0, 0] }, { feature: 'basegame', win: 0, metaTags: ['no-win'] });
for (let i = 0; i < 300; i++)
gen.addResult({ reels: [1, 1, 0] }, { feature: 'basegame', win: 2, metaTags: ['small-win'] });
for (let i = 0; i < 60; i++)
gen.addResult(
{ reels: [7, 7, 7] },
{
feature: 'basegame',
win: 0,
metaTags: ['freespin-trigger'],
featureAwards: { type: 'randomChoice', awards: [{ count: 10, feature: 'freespin' }] },
},
);
for (let i = 0; i < 40; i++)
gen.addResult({ reels: [5, 5, 5] }, { feature: 'basegame', win: 20, metaTags: ['big-win'] });
// ── Freespin ──
for (let i = 0; i < 70; i++)
gen.addResult({ reels: [0, 0, 0] }, { feature: 'freespin', win: 0, metaTags: ['no-win'] });
for (let i = 0; i < 25; i++)
gen.addResult({ reels: [2, 2, 0] }, { feature: 'freespin', win: 3, metaTags: ['small-win'] });
for (let i = 0; i < 5; i++)
gen.addResult({ reels: [5, 5, 5] }, { feature: 'freespin', win: 30, metaTags: ['big-win'] });
const buyFeatureDefinitions: IBuyFeatureDefinition[] = [
{ id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] },
{ id: 'buy-boost', type: 'entrypool', metaTags: ['big-win'] },
];
const endOptions: IEndOptions = {
config: {
gameCode: 'buyfeatures-demo',
gameType: 'slot',
stakes: [1.0],
features: ['freespin'],
buyFeatures: [
{ id: 'buy-freespin', feature: 'bf_buy_freespin', targetRtp: 95, targetPrice: 100 },
{ id: 'buy-boost', feature: 'bf_buy_boost', targetRtp: 95, targetPrice: 20 },
],
},
buyFeatureDefinitions,
};
await gen.end(endOptions);What gets written
entries.jsonl.br— the base entries, plus the materialisedbf_<id>pools. For the example above:bf_buy_freespin— 1 entry (the basegamefreespin-triggerentry), total weight 60.bf_buy_boost— thebig-winentries (basegame + freespin) at their original weights.
Pool entries reuse their source entry's
id, so they share scenarios — no scenario data is duplicated.config.json— includesbuyFeatureswith the pricing configs above, and an auto-populatedfeatureWeightsthat adds thebf_<id>features for each pool so their RTP can be computed alongside normal features.
There is no buyfeatures.jsonl — pools live in entries.jsonl, which is what the engine reads at runtime.
Related
IBuyFeatureDefinition— definition shapeIBuyFeatureEntry— resolved pool shapeIBuyFeatureConfig— pricing config shapebuildBuyFeatures()andend()— API reference- SDK · Buy-Features — runtime purchase flow