Formats & triggers
There are two formats. The takeover is the Bonus Round itself: 15 seconds of branded play. Ambient is a branded prop that lives in your world. A takeover starts from one of three triggers, and each trigger trades reach for engagement differently.
| Format · trigger | Who starts it | You add | Pays |
|---|---|---|---|
takeover · intermission | You, at a natural break | await BonusRound.break('intermission') | The best-quality slot |
takeover · interval | The SDK, every N minutes at a calm moment, as an offer | Nothing (optionally safe()) | Most volume, cheapest per view |
takeover · rewarded | The player, for a reward you define | BonusRound.rewarded({ onReward }) | Highest engagement and eCPM |
ambient | Always on once attached | Nothing (optionally placeAmbient()) | Viewable impressions |
Turn any of these on or off per game in the dashboard, or with PATCH /api/games/:id under settings.formats. All are on by default.
Anatomy of a Bonus Round
- Brand card: a short transition presents the sponsor.
- Intro (2 s): the round's instruction appears (“Grab 14 cans!”) in a branded pocket arena with the brand's hero (mascot or giant product) and banners.
- Play (15 s): players run around with their own character and collect, pass through or smash branded items. Touching an item scores it.
- Sponsored leaderboard (about 3.5 s): scores, the brand's message and its call to action.
- Back to your game, exactly where the player was.
Intermission: break()
Call break() wherever your game already pauses: a round ends, a level is complete, the player dies and respawns, the match returns to the lobby. It returns a promise that resolves when the round is over, so your code reads top to bottom.
async function onLevelComplete() {
game.pause(); // timers, enemies, input you don't want during the round
const r = await window.BonusRound?.break('intermission'); // { filled, completed, reason?, score? }
if (r?.filled) analytics.track('bonus_round', { completed: r.completed });
game.loadNextLevel();
}
- When there's no ad (no eligible brand, below your floor, the player hit the hourly frequency cap, takeovers turned off), it resolves right away with
filled: falseand areason. Your break just continues as before. - Don't call it in the middle of play. Breaks that interrupt players hurt engagement, and engagement is what brands pay most for.
- Call it as often as you have natural breaks. The ad server applies the frequency cap (
settings.frequencyCap.perHour, default 4 per player per hour).
Interval
With the interval trigger on, the SDK offers a Bonus Round at most every intervalSec seconds (default 300, minimum 60), counting from the last round or offer. It waits for a calm moment, then shows a small offer card in the corner with Play and Not now. Nothing takes over unless the player chooses to play.
// Tell the SDK when an offer would be welcome or not:
BonusRound.safe(false); // boss fight: no offers
BonusRound.safe(true); // in a menu or a calm stretch: offers are welcome
BonusRound.safe(null); // back to automatic: "calm" = 1.5 s without input
curl -X PATCH https://bonusround.io/api/games/gm_… \
-H "Authorization: Bearer $BONUSROUND_API_KEY" -H "content-type: application/json" \
-d '{ "settings": { "formats": { "takeover": { "intervalSec": 600 } } } }'
Rewarded: rewarded({ onReward })
The player opts in for something you give them: coins, a revive, a skin. Because they chose to play, rewarded rounds have the highest engagement and pay the most.
// Show the SDK's own entry button ("Bonus Round · 15 s") in the corner, once attach() has finished:
(window.bonusround = window.bonusround || []).push(async (BR) => {
await BR.attach({ THREE, scene, camera, renderer });
BR.rewarded({ label: 'Play for 50 coins', onReward: () => giveCoins(50) });
});
// Or start it from your own UI, e.g. on a "Watch for a revive" button:
reviveButton.onclick = () => window.BonusRound?.rewarded({ button: false, onReward: () => revivePlayer() });
onRewardis your code, and it runs only when the player finishes the round. Grant the reward there, and only there.- The entry button only appears once
attach()has resolved. Called before that (for example on the line after a queued attach),rewarded()resolves to{ filled: false, reason: 'not_attached' }and shows nothing, so awaitattach()first as above. Itsattach()is the same one-time call as your normal attach line: a second call just resolves to{ mode, already: true }. labelis the text on the entry button (default “Play a Bonus Round for a reward”). The call returns{ shown, hide(), start() }so you can remove the button later. Withbutton: falsethe round starts immediately and resolves to{ filled, completed, rewarded, … }, so call it from a click.- If no round is available, the player sees a short “No Bonus Round available right now” toast and nothing is granted.
Ambient props
Once attached, the SDK can place one branded prop in your world: a portal arch, a statue of the brand's hero, or a billboard. It's paired with the brand's Bonus Round, built at the scale our play agent measured in your game, and placed at an open spot it found while playing. It's billed by viewable impressions under the IAB/MRC in-game rule (half its pixels in view, at least 1.5% of the screen, at most 55° off-axis, for a continuous second), so it only counts while players can really see it.
Placement hints
You know your level better than our agent does. Suggest a spot with placeAmbient(): open ground, visible from where players spend time, not blocking a path.
BonusRound.placeAmbient({ position: [12, 0, -6], rotationY: Math.PI / 2 }); // meters, in world space, feet on the ground
BonusRound.placeAmbient(null); // forget the hint, use the agent's spot
Without a hint the SDK uses the spot from the ad package, then the spot our agent picked; if there's neither, it waits for your hint. Calling placeAmbient() again moves the prop. The prop is added to your worldRoot if you passed one (so it hides during rounds and moves with the level), otherwise to the scene. Turn ambient props off per game with settings.formats.ambient.enabled = false.
Portals
A portal arch is an ambient prop players can walk into. Walking into it counts as an engagement. If you have registered a reward with rewarded({ onReward }), it also starts a rewarded Bonus Round, and finishing that round runs your onReward. Portals need native mode, because the SDK has to know where the player is (host.getPlayerPosition).