How Block Blast harvests its players for ad money
Rules downloaded from their server decide your pieces, your score and your ads. One waits for an expensive ad, then asks for a hand you can’t place.
Ayo Omolewa
October 15, 2026
I posted a video about this game that got over 15 million views, so I wanted to do the full technical write-up: the code, and how you can reproduce it yourself.
Some context on why it matters: between January and May 2026, Block Blast made an estimated $127 million from ads. Candy Crush Saga made $81.1 million over the same months (Sensor Tower estimates).
Most of Block Blast isn’t in the app you install
We started with the Android app, version 10.8.2. We decompiled the Java side with jadx and the native Cocos library with Ghidra, and found almost nothing interesting in the package you download from the store. Block Blast is built from small modules the code calls traits, and each trait has variants called arms. Running it on a clean Android 14 emulator with no account and watching its cache and logcat showed how it works: when the app launches, the server sends a config that says which arm of which trait you get, and the app downloads them as encrypted JavaScript. The config my install got names about 700 traits. My install selected 254 of them, and 101 were downloaded after install.
CTAgloReviveTraitCTReviveTimeOutSkipSplashTraitClassNewDeathOptimizeTraitDeadlyToHardTraitDecreaseReviveUIShowByLimitHighScoreTraitDifferentStrategyDieTraitEndlessDeathBoardTraitFastHandPunishTraitGLBFDieReplayTraitIsCacheReviveNumTraitIsClassADReviveNoInerestingTraitIsOpenInAdvanceDeathByLowEfficiencyGiveReviveTraitIsOpenRandomToRandomNoDieTraitLevelReviveCondiTraitLevelReviveCountTraitLevelReviveTraitMABXiBaiTraitMultiClearBoostThenKillTraitRepateCPlusPlusRandomNoDeathTraitReviveBuffTraitReviveCleanBoardTraitReviveCountTraitReviveLimitTraitRewardHighValueAdHarvestTraitAdLevelTraitAdPlusMinusStrategyTraitAddClearTipsExTraitAddMoreGameTraitAdjustPercentChTraitAlgoFillSortEdgeTraitAlgoSDK_modifyBlockPosTraitAlgoSDK_useAlgoSoAloneTraitAlgoSameBlockShangZengTraitAlgoSymmetryBlockTraitBComboScoreChangeTraitBCommentDisplayTraitBPuzzleTimeTraitBReplaceBinaryTraitBackSpaceClickLimitTraitBaseplateConditionDiffTraitBaseplateLaneTraitBaseplateTraitBlockLimitClassicsTraitBlockLimitLevelTraitBlockPoolRoundAdjustV2TraitBlockPoolStrategyExperimentTraitButtonClickAddShakeTraitCTAdvertisementMultipleCallTraitCTAdvertisementNoHideTraitCTAgloNameSpecialTraitCTAgloNameTraitCTAgloStrategyTraitCTBAlgorithmGaohuibaoARGTraitCTChapterScoreComboTraitCTChapterSkinBonesTraitCTChapterWinOverTraitCTClassSkinBonesTraitCTComboDotTraitCTCommentskinWithFeedBackTraitCTDotClickGameEndInfoTraitCTDotClickUsrDataGameEndTraitCTDotDeadReportToUsrDataGameEndTraitCTDotFirstClickDataGameEndTraitCTEffectAccTraitTraitCTGBMClearTraitCTGBMEmptySurfaceGameStartDotTraitCTGameComboScoreTraitCTGameEndStatusDotTraitCTLowDeviceEffectTraitCTMergeBlocksAgloNameTraitCTMergeBlocksSkinBonesTraitCTSkinBonesTraitChapterAlgoStrategyTraitChapterCollectScoreFixedTraitChapterWinShowRainbowTraitClassGuideEndNoAwaitNextFrameTraitClassicsRandomReplaceTraitCleanSceneRandomSkinTraitCleanSceneUseSequenceSkinTraitClearBoardHeartRippleAniTraitClearBoardPlusBlockPosTraitClearBoardPlusTraitComboAddRemoveNumTraitComboModeCtrlTraitComboRainbowTraitComboScoreSplitTraitComboTimeTraitCombo_tips_lv_blockTraitComboflyScoreTraitConsecutiveLossRecoveryTraitContinueSameMoreRoundLimitTraitControlHighestScoreRhythmTraitCrossClearHardTraitCtrlAlgoLimitTimeTraitDailyPerformanceReviewTraitDailyWinStreakStateTraitDifficultChangeColorTraitEightHoursTravelTraitExceedAimScoreTraitExpendBoardScoreTraitFastNearTkxcClassTraitFastNearTkxcTraitFeatclearscreenbombTraitFeatclearscreenbombfixTraitFillCleanAlgoTraitFillExcludeTraitFirstGameScoreTraitFirstHardMoreClearAlgoTraitFiveRoundStatisticsTraitGLBFMiddleTipsViewTraitGLBFProgressTraitGLGameAdStrategyOptimizeTraitGLResultView6TraitGLShowAdsByGameTimeTraitGameInfoDisplayTraitGameLobbyNewEntryCtrlTraitGameLobbyTraitGameLobbyUnlockByBreakHighScoreTraitGameOverShowMiniGameIconTraitGateWinStreakStateTraitGpEmptyBoardRatioGuardTraitHighProgressShowBannerTraitHighScoreBreakthroughBoostTraitHighScoreLimitByUserAbilityTraitHighValueInterstitialReliefTraitHighWeightSessionQuestionReplacerTraitHoleFillPriorityTraitHotAreaAdaptScreenBottomStateTraitHotAreaClickUtilTraitInitBoardDelayTraitInitBoardSnakeProStyleProTraitInitialBoardDisableTraitIsOpenBottomCoverChangeTraitIsOpenChangeSkinTraitIsOpenClassEncourageEffectByScoreTraitIsOpenComboContinuousTraitIsOpenComboInterruptionStrategyReplaceBlockTraitIsOpenExtraReportTraitIsOpenGLHallMoreGamesPopupTraitIsOpenGlLoadAllGameBackTraitIsOpenLevelOverTipsChangeTraitIsOpenMoreCleanChangeTraitIsOpenMoreCleanTraitIsOpenNoVideoNoRevivieTraitIsOpenOperaPosTraitIsOpenRainbowPutBlocksEffectTraitIsOpenRightPutTraitIsOpenShang1ToShang3TraitIsOpenVibrateTraitIsOpenZhiJueToKunNanTraitIsOpenZiMuShangZengTraitIsPuzzleTimeTraitIsShowAchievementTraitIsStatisticalDataCollectTraitIsTravelMoreDiamondAwardTraitIsTurnAroundSurfaceTraitIsWarmhandsTraitJewelNewOutBlockTraitLDEnableNoSyncHighScoreRollingTraitLWMDualConditionHardPuzzleTraitLaneSchemeTraitLevelConfigTraitLevelExpAlgorithmTraitLevelLoopTraitLevelNoThemeTraitLevelRandomReplaceTraitLevelRemoteThemeTraitLevelWayHelpConfigTraitLevelWayHelpTraitLimitHighComboTraitLimitNoviceBlockSizeTraitLineDynamicSendTraitLoadingTieringTraitLocalThemeSaveTraitLoginTimeFirstDifficultyRatioTraitLongThinkClearAssistTraitLowFeedbackPleasurableOfferTraitLowHardSetFps30TraitLowWeightFillBlockSortAdjustAlgoTraitLowWeightFillBlockSortTraitMaturityDevelopmentV2TraitMergeBlocksIsPuzzleTimeTraitMergeBlocksLaneSchemeTraitMoreClearStopComboTraitMoreTimeToOpreateStateTraitMultiElementCollectTraitNewAbilityModelTraitNewBottomOfferTraitNewPlayerContinuePlayIncentiveTraitNewSeasonNotifyTraitNewStructTraitNewUserLevelTraitNew_player_direct_guide_to_thirdTraitNewbieScoreRegulationTraitNoBannerAdNewUserByDaysTraitOpenChangeContinueHardTraitOpenComboModelTraitOpenLevelModelTraitOriginSkinShowGreyDefultSetBtnTraitOverHighScoreClearBoardTraitParallelClearHardTraitParam230012TraitPixelBitControllerTraitPlaceBackAdaptTraitPlaceHabitAdaptPathTraitPlaytimeNoInterstitial1TraitPostGameToServerTraitPreClearScreenAnimOptimizeTraitProblemIntervalTimeRandomBetaTraitPromotionMoreGamesInBlockCtrlTraitPutBlockOptimizeTraitPuzzle100TraitPuzzle100WeightConditionTraitPuzzleAfterTimingTraitPuzzleBeforeTimingTraitPuzzleConditionDiffTraitPuzzleFail100TraitPuzzleSuccess100TraitPuzzleTiming100TraitPuzzleTimingMaxPriorityTraitRatioAdjustTraitRatioDownTraitReduceTravelSettlementTraitReplaceBoardTraitReplaceClearnScreenAniTraitRightPutHintCtrTraitRobotModelEventDataTraitSequencePredictDynamicDifficultyTraitServerControl_board_init_v2TraitSetAddDefultSkinBtnTraitSettleFireworkTraitSetupNewTraitShangZengAddRandomTraitShangzeng3ReplaceZimushangzengTraitShowSSSWhenSkinChangeTraitShowTravellClearAnimaTraitSimpleMultiSolutionTuning2TraitSkinChangeAblePushTraitSkinPreEliminateGoodPerformanceTraitSkinRandomTraitSkinThree24NewColorsTraitSkinThreeOptimizationsTraitSkinThreeStageTraitSlowPlayerAnimTraitSmallgameLocalResTraitSolidColorTraitTerminateRefreshScoreTraitTheMoreEliminateTheMoreGoodEffectTraitTravelEntryClockShadowFixTraitTravelEntryClockShadowTraitTravelHappyOverTraitTravelNoSkinTraitTravelResultOptimizingTraitTravelScoreCutoffByProgressTrait
All 254 traits assigned to my Android test install. The ones about losing, revives and ads are highlighted.
So two people playing side by side can be running different games, and the studio can change yours without an app update. Most of those 254 are ordinary: tutorials, animations, sounds. A handful are about when you lose. One of them is about ads.
The rule that waits for an expensive ad
It’s called RewardHighValueAdHarvestTrait. By finding where the game unlocks these files in its own native library, we decoded it into 15,616 bytes of plain, minified JavaScript. Its constructor sets the parameters:
RewardHighValueAdHarvestTrait.js · lines 63–67
t.HIGH_PRICE_MULTIPLIER = 3.5;
t.MEDIUM_PRICE_MULTIPLIER = 1.5;
t.MIN_HISTORY_AD_WATCH_COUNT = 3;
t.RECENT_AD_NO_ESCAPE_COUNT = 3;
t.TARGET_AD_LEVEL = "high";
It waits for three things to line up: you finished your last three rewarded ads, the ad loaded right now is worth at least 3.5 times your last one, and you’re 1,000 points past your high score. All three get checked in one function. Here it is with the minified null checks rewritten as optional chaining; the logic is the same:
shouldTriggerDeath · lines 262–267 (reformatted)
shouldTriggerDeath() {
if (!this.canTrigger()) return false; // finished your last 3 rewarded ads
const d = this.storageData;
return d.currentGameNum === hs.classGameInfo?.gameNum // same game
&& !d.currentGameHasTriggeredDeath // hasn't fired yet
&& !(hs.classScoreInfo?.score < this.currentScoreThreshold) // past high score + 1,000
&& !!this.isTargetAdReady(); // loaded ad ≥ 3.5× the last one
}
Here’s how it checks each one.
1. You finished your last three rewarded ads. Every time a rewarded ad starts, the rule pushes false onto a list and keeps the latest three (lines 174–183). If the ad SDK reports that the reward was earned, it flips that entry to true (191–199). The check needs all three to be exactly true:
canTrigger · lines 268–275
t.prototype.canTrigger = function() {
var e = this.storageData;
if (e.totalRewardShowCount < this.MIN_HISTORY_AD_WATCH_COUNT) return !1;
var t = e.recentRewardResults.slice(0, this.RECENT_AD_NO_ESCAPE_COUNT);
return !(t.length < this.RECENT_AD_NO_ESCAPE_COUNT) && t.every(function(e) {
return !0 === e;
});
};
That flag comes from AppLovin MAX’s onUserRewarded callback in the Java layer, so an ad you close early stays false.
2. The ad loaded right now is worth at least 3.5 times your last one. The rule reads the value of the best rewarded ad currently loaded with NativeAd.callNativeGetMaxEcpmNumber("2") (line 304). In the Java layer, that call goes through each ready rewarded ad slot, takes the revenue AppLovin MAX reports for it, multiplies it by 1,000, and returns the highest. The value of your last rewarded ad gets the same ×1,000 (line 310), so the comparison is like for like:
classifyLoadedRewardLevel · lines 290–293
t.prototype.classifyLoadedRewardLevel = function(e) {
var t = this.storageData.lastRewardShowEcpm;
return !Number.isFinite(t) || t <= 0 ? "none" : e >= t * this.HIGH_PRICE_MULTIPLIER ? "high" : e >= t * this.MEDIUM_PRICE_MULTIPLIER ? "medium" : "none";
};
3. You’re past your old high score. At the start of each game the rule saves your high score (line 145). The bar is that high score plus scoreDeltaStep (lines 107–110). Each player gets one arm: mine, 332243014, uses 1,000, and the app’s bundled config has others at 500 and 750. If you revive after the rule fires, the bar moves up another step, so it can fire again in the same game.
Then it asks for the lethal puzzle. Before dealing your next set, the game runs a list of “bottom offer” rules in order. When this one qualifies, it replaces the list with a single generator:
onBottomOfferAlgo · lines 245–251
t.prototype.onBottomOfferAlgo = function() {
if (this.shouldTriggerDeath()) {
hs.algorithmBottomSequenceInfo.setAlgorithmBottomList([ this.deathAlgorithm ]);
this.storageData.currentGameHasTriggeredDeath = !0;
this.saveStorageData();
}
};
deathAlgorithm returns hs.OFFER_TYPE.ZHI_SI_TI (line 102). The game sends that request to its puzzle engine and deals the three piece IDs it gets back (main script lines 45062–45121 and 15074–15123). If the engine fails to find a set, the rule resets and tries again on the next deal.
To watch it decide, we ran the downloaded file itself, unmodified, with test ad and score values. It requests the lethal puzzle exactly when all three conditions hold: 28 cases, including two ads instead of three, one ad closed early, a score 1 point short, and an ad worth just under 3.5×.
Then we ran it live, in a test copy of the iPhone app running in Apple’s simulator, and read the rule’s inputs while we played. It had three good rewarded ads on record. The score was past the line. A rewarded ad from Google AdMob was loaded and ready. The rule priced it at 72.39, against 37.41 for the last one, which is 1.94 times. It needs 3.5. So on iPhone the rule is live, reading real ad prices, and waiting for an ad worth 3.5 times your last one.
The lethal puzzle
The name maps through three tables in the main script:
main/index.45fec.js (original line numbers)
23878 e[e.ZHI_SI_TI = 9] = "ZHI_SI_TI";
16092 … r[n.OFFER_TYPE.ZHI_SI_TI] = i.AlgoClassTypeConst.algo_must_death, …
258441 e[e.algo_must_death = 24001] = "algo_must_death";
Algorithm 24001 exists twice: in JavaScript, and in the game’s native C++ library, where the function is called func_death_blcok (their spelling). They do the same search. Here’s the JavaScript core:
rC · main/index.45fec.js lines 318602–318678 (excerpt)
for (var d = 0; d < n.length; d++) if (41 != d) {
var m = n[d];
r.canPut(m) ? o.push(m) : i.push(m); // o: fits now, i: doesn't fit now
}
…
iC(r, o[p], o[f], i[_], h); // try every order of two pieces that fit
iC(r, o[p], i[_], o[f], h); // and one that doesn't, clearing lines
iC(r, o[f], o[p], i[_], h); // after each placement
iC(r, o[f], i[_], o[p], h);
if (0 == h.length) { // no order worked: take this set
u = [ o[p], o[f], i[_] ];
…
a.names = [ "致死题", "致死题", "致死题" ]; // "lethal puzzle" ×3
It looks for two pieces that fit now and one that doesn’t, and keeps the set only if no order and no set of positions places all three, counting every line you’d clear along the way. Two of the three fit when they’re dealt, so the hand doesn’t look impossible until you try it.
Here’s what that looks like, with the game’s own code running in your browser. Step through it:
Step 1 of 5
A normal hand
No space left
Every step comes from running the game’s own generator code in your browser.
Press “New random board” for a fresh mid-game board, and the game’s code picks a new hand for it. On the test board in the evidence bundle, the game’s generator accepts a vertical domino, a horizontal domino and a 3×3 square as a lethal hand: we tried all 171 placements, and none of them finish it. On an empty board, those same three pieces fit 67,680 ways in each order.
The ad rule isn’t the only one that asks for it
The lethal puzzle is a generator, and any rule can ask for it. The ad rule is one caller. While we were testing it live on iPhone, a second caller fired on camera: PersonalMarginBreakTrait. Once you’re 10% past your high score, it rolls on every deal, and 20% of the time it asks for the same lethal puzzle:
PersonalMarginBreakTrait · onAlgorithmStrategyPriority, lines 153–159 · live config: prob 20, recordPercent 10, replaceAlgoId 9
t.prototype.onAlgorithmStrategyPriority = function() {
this.hasReplacedThisGame = !1;
if (this.state.isFirstGame) ; else if (this.isReviveRound()) ; else if (this.state.isBreakHighScore && this.isCurrentScoreOverRecordPercent()) if (100 * Math.random() >= this.prob) ; else {
var e = this.replaceAlgoId; // 9, the lethal puzzle
…
if (this.callNewAlgorithm(e)) ; else {
Our record was 1,304, so its line was 1,434. At 3,356 points it rolled, and the game dealt a five-long bar, a five-tall bar and a zigzag. The game’s own record of that deal names this rule as the caller, shows algorithm 9 both requested and used, and labels all three pieces 致死题, lethal puzzle. There was no way to place them. Two separate exhaustive searches agree: zero. The rule, the live settings and the deal are in the live iPhone bundle.
The deal at 3,356 points, through to the Revive button. Recorded in a test copy of the iPhone app in Apple’s simulator, after we reset its saved high score so we could reach the line quickly.
The model that picks where you lose
The rules so far are hand-written. The game also downloads trained models and runs them on your phone, and one of them decides where in a level the lethal puzzle shows up.
It’s a 500-tree XGBoost classifier for the game’s level mode, which the code calls Chapter. Its file is named predict_travel_difficulty_dead, and its 52 inputs are mostly about how you’ve been doing: your current level, how many times you’ve tried it, how many games you’ve finished this week, and how long you’ve had the app. The game feeds it 12 options, four values of a setting called sw_process times three difficulty levels, and keeps whichever one the model scores highest:
ClientDieModelLoadTrait · doPredictFeature, lines 1286–1312 (excerpt)
if (0 == e) for (var i = 1; i <= 4; i++) for (var l = 1; l <= 3; l++) {
var c = this.getInputFeature(this._jsonModel.learner.feature_names, i, l);
if ((u = this.xgb_clf_predict(c, …)) > n) { // keep the highest-scoring option
n = u;
a = i;
o = l;
}
}
…
storage.setItem("DieModel_last_predict_info", { swProcess: a, travelLvDifficulty: o, pre: n });
Another rule turns that pick into a stretch of the level: the first 30%, 30 to 80%, or the last 20%. When you reach that stretch, it offers you an extra revive if you usually take them (more than 75% of the time). Then, from the fourth deal after that, it asks for the lethal puzzle:
IsOpenDeathChangeReviveCountTrait · lines 224–229 and 181–184
var t = hs.chapterConfigInfo.getChapterProgress(), i = e.getLastPredictInfo(this.traitName), r = i.swProcess, …;
…
1 == r ? s = [ 0, .3 ] : 2 == r ? s = [ .3, .8 ] : 3 == r ? s = [ .8, 1 ] : 4 == r && (s = [ 1, -1 ]);
return t < s[0] ? a.First : t <= s[1] ? a.Second : a.Third; // before, inside or after that stretch
…
if ((n = this.saveData).needZs && n.zsRoundNum > 3) {
hs.algorithmStrategyInfo.setAlgorithmPriorityList([ hs.OFFER_TYPE.ZHI_SI_TI ]); // the lethal puzzle
hs.algorithmStrategyInfo.setAlgorithmFailList([ hs.OFFER_TYPE.SHANG_ZENG_3 ]);
}
Other rules read the same pick the opposite way and make a level easier when it lands on the fourth option.
The model that prices you
A second model puts a price on you. The game downloads xgboost_regression_model111.json from its server and runs it on your phone.
It’s an XGBoost regression model: 59 decision trees, up to 8 levels deep, 27 inputs. Most of it is about money: 18 of the inputs are about the ads the game has loaded for you, today and yesterday. The rest are where you are, your phone, when you installed, and how your first game went. The trees lean hardest on your first score, the price of your latest ad, your RAM, your phone model and your country.
The code calls its answer finalEcpm. eCPM is the ad industry’s name for what a thousand ads are worth, and it’s the same unit the game stores its ad prices in. So the model is estimating what you’re worth in ads.
GPT-6 Astra ported the game’s input code and tree walk, and the port reproduces the game’s own outputs to the last decimal. We ran it on a made-up new player in the US with a first score of 1,000, once on an iPhone 15 installed from the App Store and once on Android, then changed one input at a time:
| Change only this | Ad value, iPhone 15 | Ad value, Android |
|---|---|---|
| Nothing (US, first score 1,000) | 25.48 | 48.96 |
| Live in Brazil instead | 8.08 | 17.90 |
| Live in India instead | 5.59 | 14.83 |
| First score of 500 | 25.79 | 53.57 |
| First score of 2,000 | 21.12 | 52.64 |
| First score of 10,000 | 20.34 | 58.26 |
Moving the same player from the US to Brazil cuts their value to about a third, and India cuts it further. Your first game matters too, but not in a straight line: on iPhone, a first score of 500 makes you worth more than 2,000 or 10,000.
The game uses prices like this to decide what kind of ads you get, and how many. The clearest case asks its server for two prices for you, one for full-screen ads and one for rewarded ads, and compares them:
IsOpenInterAndRewardAdvDecideReviveTrait · checkEcpm, mainTraits/index.88dc4.js lines 38529–38534
t.prototype.checkEcpm = function() {
var e, t, o, r = this.saveData.interEcpm, i = this.saveData.rewardEcpm; // your full-screen and rewarded prices
…
return (…isReverse) ? r <= i * (…multiple) : i >= r * (…multiple);
};
In the versions the app ships, if your rewarded ads are worth at least twice your full-screen ads (1.5 or 2.5 times in other versions), you get an extra chance to revive, and a revive plays a rewarded ad. If full-screen ads pay more for you, you don’t.
Other rules work the same way. FullScreenAdAddByEcpmTrait adds an extra full-screen ad when your price is high. IsOpenEcpmTooLowNoInterTrait skips full-screen ads when you’re worth almost nothing. A few more open an early or a third revive when your rewarded ads are worth enough.
Try the on-phone model yourself:
25.48
Your ad value
per 1,000 ads (eCPM)
finalEcpm 25.47779944720001 0/59 trees moved
Here’s the part of the game that walks the trees. It’s a plain loop: start at the root, go left or right on each input, add up the leaf weights.
EcpmFeatureBaseInfo · runModel, main/index.45fec.js lines 63141–63175 (excerpt)
for (var m = parseFloat(a + ""), u = 0; u < t.length; u++) { // m starts at the base score; t is the 59 trees
for (var p = t[u], f = 0, _ = 0; _ < o; ) {
_ += 1;
var h = p.left_children[f], g = p.right_children[f], y = p.split_conditions[f], C = p.default_left[f], v = s[e[p.split_indices[f]]], T = -1;
(null === v && 1 === C || v < y) && (T = h);
(null === v && 1 !== C || v >= y) && (T = g);
if (-1 === T) {
m += parseFloat(p.base_weights[f].toString()); // reached a leaf: add its weight
break;
}
f = T;
}
if (_ >= o) throw new Error("[EcpmFeature] 树的遍历超过最大深度,可能存在死循环。");
// "Tree traversal exceeded max depth, possible infinite loop."
}
A model that guesses you’ll play less tomorrow
There’s also a 1,000-tree LightGBM model with 93 inputs that predicts whether you’ll be less active tomorrow. It leans hardest on how long you’ve had the app, your high score, your RAM and your average score. If it scores you at 0.69 or above, a companion rule swaps in a pink revive button, and the revive skips the ad:
NewNextDayLowActivityTrait line 1010 · NextDayLowActivityFirstReviveFreeTrait lines 112–117
return this.state.predictionScore >= this._lowActivityThreshold; // 0.69
…
if (hs.tp.isClassAdvertisement_RewardProxyShieldPlayAdvertisement(e) || …) {
var r = cc.isValid(this._pinkReviveNode);
if (!this.isTrigger || !r) return;
e.returnValue = !0;
e.args[0] = !0; // shield the ad: the revive goes through without one
this.saveNextDayLowActivityReviveTime(Date.now());
}
It works once a day.
Models on their server
Some rules send your data out and act on the score that comes back, from server models with names like churn, LTV, revive-click and xDeepFM. Their weights stay on the server, so all we can show is what the phone sends. One of those requests, to /v1/ecpm_prediction, sends 40 fields, including your carrier, storage, the hour and stats on your recent ad prices.
Other rules that shape your game
The ad rule isn’t the only one that changes your pieces or your score. We caught one working live on iPhone. MaturityDevelopmentV2Trait gives each session a “maturity” score made of three parts: habit (10.98), skill (14.66) and your ad history (19.27). Ours added up to 44.91, under its line of 65, so it swapped some of the hard puzzles it would have dealt for easy fills. Another, FailStreakStrategyTrait, stepped in after two games below our record and asked for easier pieces.
These came up while tracing the ad rule on Android, and each has the same kind of evidence in my research files.
| Rule | What the code does |
|---|---|
MultiClearBoostThenKillTrait | Sends your user ID to two server endpoints and gets back two labels. The code names both tiers as “not sensitive to being forced to die” (怼死不敏感). For those tiers, it waits for a big clear, then requests the lethal puzzle a few rounds later once the game has lasted about as long as your usual game. |
MABXiBaiTrait | A six-arm experiment on how soon the lethal puzzle can be requested: never, or after 4, 6, 10, about 17 or 30 minutes. For each arm it reports whether you started a new game within 15 minutes, and what share of revive offers you accepted. |
FastHandPunishTrait | Asks for hard pieces when your last six sets averaged under 4.2 seconds each and you’re past 93% of your high score. |
AdPlusMinusStrategyTrait | After one kind of extra revive offer, adds a random bonus of up to 10% of your high score to your current score. |
Questions people asked
Is it the same on iPhone?
Yes. We took apart Block Blast for iPhone too, version 7.4.9, and a second GPT-6 Astra run checked those findings from scratch. It downloads the same rule from its own update server, and the file matches the Android one except for one line. Everything else is identical: three ads, 3.5×, the score bar, and the same lethal-puzzle generator.
The iPhone app also lists a second version of the rule, RewardHighValueAdHarvestV2Trait, and in that one the ad-history check can be switched off:
RewardHighValueAdHarvestV2Trait.js (iOS) · lines 323–331
t.prototype.canTrigger = function() {
var e = this.storageData, t = this.viewCountThreshold;
if (e.totalRewardShowCount < t) return !1;
if (!this.requireRecentNoEscape) return !0;
var r = e.recentRewardResults.slice(0, t);
return !(r.length < t) && r.every(function(e) {
return !0 === e;
});
};
The app ships 12 versions of it. Half turn that check off, and they need anywhere from 1 to 3 ads, so in some of them a single rewarded ad you closed early is enough. The price bar depends on how long you’ve had the app: 3.2× the first ad loaded that session in your first week, 2.7× through day 30, and 3× your last ad after that.
What happens if I play without wifi?
This rule is saved on your phone after it downloads, so it still runs offline. But it only fires when a rewarded ad is loaded, and ads load over the internet, so once nothing is loaded it can’t fire. Other rules keep going. On my Android install, the one that asks for hard pieces when you’re playing fast near your record runs entirely on your phone. The one that asks the server which players “don’t mind being forced to die” can’t get an answer offline, so it stays off for that session.
Does an ad blocker do anything?
It depends on the blocker. Browser ad blockers, on iPhone or Android, only work inside the browser, so they don’t touch the game. A blocker that covers your whole phone, like Private DNS on Android or a DNS or VPN-based blocker on either, can stop rewarded ads from loading, and then this rule can’t fire, for the same reason as playing offline. The rules that don’t use ads keep running: the fast-play one, the experiment that times when the lethal puzzle can be requested, and, since you’re still online, the one that asks the server about you.
Is this why I always lose right after beating my high score?
That’s exactly where these rules are aimed. We caught PersonalMarginBreakTrait doing it live: once you’re 10% past your record, every deal has a 20% chance of being the lethal puzzle. The ad rule waits for 1,000 points past your record (500 or 750 in other versions), and after a revive it moves the bar up and can fire again.
Can I avoid it?
Not really. On Android, the original rule needs your last three rewarded ads to pay out, so closing one early does turn it off until you’ve had three good ones in a row again. But on iPhone, the V2 versions can skip that check completely and fire after a single ad, even one you closed. And the other rules that ask for the lethal puzzle, like the timing experiment and the one that waits for a big clear, don’t look at your ads at all. The one thing that stops this rule everywhere is not having a rewarded ad loaded, which is what happens offline or with a phone-wide ad blocker.
Isn’t this just adaptive difficulty?
Adaptive difficulty changes the pieces based on how you’re playing. This rule doesn’t look at how well you’re placing pieces. Its inputs are your recent ad results, the price of the ad sitting in memory, and your score relative to your record. And the generator it asks for isn’t a harder set of pieces. It’s a set chosen because no way of placing it works.
Is this even allowed? Doesn’t Apple review apps?
Apple reviews the app you download from the store, and this rule isn’t in it. The app downloads it from the game’s server after you install, and the server decides which players get which version. Apple does let apps download JavaScript updates like this, which is how tools like Expo Updates work, as long as the update doesn’t change what the app is. Whether changing your pieces based on ad prices fits that is up to Apple.
Why does it run on the phone instead of their servers?
The server does decide who gets the rule and which version. But the decision itself needs things only your phone knows in that moment: your board, the next set it’s about to deal, and the ad that’s loaded right now. So that part runs on the phone, which is also why it could be taken apart.
How I know this is real
- The apps. Android: the 10.8.2 XAPK from APKPure, downloaded October 11, 2026. All its split APKs pass signature verification, and the version matches the public listing on APKMirror. iOS: the 7.4.9 IPA from a public mirror. 4,670 of its game resources, including the main script and the catalog, check out against Apple’s code signature.
- The rules came from their servers. The Android app’s cache says it downloaded the rule from
bband1.afafb.com, the same host it fetches all its traits from. We fetched that URL separately, and the file matched the app’s cached copy byte for byte. The iOS app’s signed catalog names both iOS rules, and the app fetches them frombbios1.afafb.com. As of October 15 all three URLs still serve the same files, so you can check this yourself (commands below). - It was assigned and loaded. On a fresh Android 14 emulator with no account, at 23:01:15 UTC on October 11, the app had the config ready, arm 332243014 selected for this rule, the module loaded with its storage initialized, and the bottom-offer pipeline it plugs into switched on.
- The decoding is exact. The traits are encrypted with XXTEA and compressed with Brotli. The decoded rule’s hash is below. For the native library, my offline decode matched a memory dump of the running app across 25,317,680 bytes with zero mismatches.
- Nothing in the Android game was changed. The only change was a rendering workaround so the game would draw on the emulator. No scores, ads, flags or config values were edited.
Check it yourself
There are three evidence bundles: Android (113 KB, SHA-256 bd55151e6a1527f628459c2fffa115d5b14a765a3fb5a3f729387fbac6f707c8) and iOS (2.7 MB, SHA-256 02bdb0e0e9b02d77844efbcf4b82f48e17f2b688fc48cbf84f83e75ae289df7d), plus a third for the live iPhone catch (270 KB, SHA-256 5f36863daabd2b500c8062d005f41e6c5e7dcea5a238c89e558e19a928dcc57f). They have the decoded rules, the encrypted originals, excerpts from the rest of each app with their original line numbers, the test scripts and the provenance records. You need Node.js and Python 3.
Terminal · Android bundle
# the rule, run unmodified with fake inputs: 28 checks, prints "passed": true
node proof/verify_reward_harvest.js
# brute-force search of the test board: 0 solutions in every order
python3 proof/verify_unsolvable.py
# the game's own generator on that board: picks pieces [2, 3, 13]
node proof/verify_packaged_algorithm.js
Terminal · iOS bundle
# both iOS rules, the generator, two solvers and the code-signature check
python3 proof/run_all.py
Terminal · the encrypted rules on their servers match the bundles
curl -s https://bband1.afafb.com/update/block_android_online/remoteRes/0/remote/RewardHighValueAdHarvestTrait/index.700f5.js | shasum -a 256
# dec91aa4001f651a74bf101d512587b9a364cb71ea2a81a9cc9dbbcd2444b477
curl -s https://bbios1.afafb.com/update/block_ios_online/remoteRes/0/remote/RewardHighValueAdHarvestTrait/index.ff35e.js | shasum -a 256
# 61ac70045995f44b3c6bb22b2d062a386593ce6a632fbcec3863a51110850cc2
curl -s https://bbios1.afafb.com/update/block_ios_online/remoteRes/0/remote/RewardHighValueAdHarvestV2Trait/index.677f6.js | shasum -a 256
# e38953430980f4b6a10b29337ec9a41634bb732ee84dd5997bfe78507b26ee1e
| Android base APK | a520fcc1d1ba1ff0c643e30b059c87a01fde650ec46be5c85fde5d815ec82447 |
| Android XAPK | 91988cf6e92cd0d346b869df0a8c6740f3a5fd8b722e21779e045426c7553015 |
| Android rule, decoded | f94a5d54d7f6ab0af097f408bda09b091e7767b17749f8ba828b41b638b330ad |
| Android main script | 8dce6532cafc25d982f2af3659647e0e0017c3902cbc31f872d375804dc3678c |
| iOS IPA | 7fc9b47e10dc8303f662e1c0ad88b93e7119e2846972eddb1a18bd205b54056a |
| iOS rule, decoded | 179bf50ae3c0c4a9bd5457281e4876faae728778f6181dbb0d3799b42ec70d61 |
The key and the steps for getting past the apps’ protection aren’t in the bundles. If you’re a journalist or researcher and want to verify the decoding step, email me.
I write up every breakdown in full, with the actual code. Put your email in and I’ll send you the next one.
You can unsubscribe anytime. Privacy
I’m Ayo, a data scientist and app developer. I post these breakdowns as videos on Instagram. If you find a mistake, email me and I’ll correct it here with a note.
Block Blast Android 10.8.2 (build 10820) and iOS 7.4.9 (build 7491). Ad revenue: Sensor Tower estimates via GameDev Reports and WN Hub. AppLovin on reported revenue precision: MAX advanced settings.
Original investigation by Ayo Omolewa (@ayoships), first published at https://ayoships.com/breakdowns/block-blast on October 15, 2026. Anyone quoting, summarizing or building on this work must credit Ayo Omolewa and link to the original.