[{"data":1,"prerenderedAt":519},["ShallowReactive",2],{"post-\u002Fwriting\u002Fintegers-all-the-way-down":3,"post-more-\u002Fwriting\u002Fintegers-all-the-way-down":261},{"id":4,"title":5,"body":6,"date":248,"description":249,"draft":250,"extension":251,"meta":252,"navigation":253,"path":254,"seo":255,"stem":256,"tags":257,"__hash__":260},"writing\u002Fwriting\u002Fintegers-all-the-way-down.md","Integers all the way down",{"type":7,"value":8,"toc":240},"minimark",[9,13,21,26,36,39,42,46,49,52,68,72,75,78,119,127,130,134,137,150,157,161,168,171,219,222,226,229,236],[10,11,12],"p",{},"There is a class of bug that never shows up in your tests, never reproduces locally, and\nsurfaces three months into production as a slow, unexplained drift between what your\nledger says and what your payment provider says.",[10,14,15,16,20],{},"It usually starts with a ",[17,18,19],"code",{},"float64",".",[22,23,25],"h2",{"id":24},"the-failure-mode","The failure mode",[10,27,28,29,32,33,35],{},"Floating point cannot represent ",[17,30,31],{},"0.1",". It represents something extremely close to ",[17,34,31],{},",\nand the error is deterministic but not intuitive. Add enough of those together —\nmultiply them by a payout multiplier, apply a percentage rake, split a pot three ways —\nand you accumulate a residue.",[10,37,38],{},"The residue is tiny. That is exactly the problem. It is too small to notice in a single\ntransaction and too large to ignore across a million of them, and by the time\nreconciliation flags it you have no idea which transactions were wrong.",[10,40,41],{},"Worse, in regulated gaming the drift is not just an accounting nuisance. A payout table\nthat does not sum to the certified return-to-player figure is a compliance failure,\nregardless of how small the discrepancy is.",[22,43,45],{"id":44},"the-rule","The rule",[10,47,48],{},"Money is an integer count of minor units. Always.",[10,50,51],{},"Not \"usually\". Not \"except for this one percentage calculation\". The moment a float\nappears anywhere in the path — computing a bonus, applying a multiplier, splitting a\npayout — you have reintroduced the whole problem, because the value will be converted\nback to an integer eventually and that conversion is where the error crystallises.",[10,53,54,55,58,59,61,62,67],{},"In Go this means ",[17,56,57],{},"int64"," cents, or ",[17,60,57],{}," of whatever the smallest indivisible unit is\nfor your currency. In the slot engine I built for\n",[63,64,66],"a",{"href":65},"\u002Fwork\u002Fneon-nights","Neon Nights",", the game core has a hard invariant: no floating-point\ntype appears in any function that touches balance, stake, or payout. It is enforceable\nby reading the code, which is the point.",[22,69,71],{"id":70},"but-percentages","But percentages",[10,73,74],{},"The objection is always the same: some calculations are genuinely fractional. A 3.5%\nrake. A 96.2% RTP. A payout multiplier of 2.5×.",[10,76,77],{},"Those are fine — you just do the division last and decide explicitly what happens to the\nremainder.",[79,80,85],"pre",{"className":81,"code":82,"language":83,"meta":84,"style":84},"language-go shiki shiki-themes github-light github-dark","\u002F\u002F rake in basis points: 350 = 3.50%\nfunc applyRake(amount int64, rakeBps int64) (net, rake int64) {\n    rake = amount * rakeBps \u002F 10_000  \u002F\u002F truncates toward zero\n    return amount - rake, rake\n}\n","go","",[17,86,87,95,101,107,113],{"__ignoreMap":84},[88,89,92],"span",{"class":90,"line":91},"line",1,[88,93,94],{},"\u002F\u002F rake in basis points: 350 = 3.50%\n",[88,96,98],{"class":90,"line":97},2,[88,99,100],{},"func applyRake(amount int64, rakeBps int64) (net, rake int64) {\n",[88,102,104],{"class":90,"line":103},3,[88,105,106],{},"    rake = amount * rakeBps \u002F 10_000  \u002F\u002F truncates toward zero\n",[88,108,110],{"class":90,"line":109},4,[88,111,112],{},"    return amount - rake, rake\n",[88,114,116],{"class":90,"line":115},5,[88,117,118],{},"}\n",[10,120,121,122,126],{},"Multiply first, divide once, and the truncation happens in one known place instead of\nbeing smeared across every intermediate step. Then answer the question the float was\nletting you avoid: ",[123,124,125],"strong",{},"who gets the remainder?"," The house? The player? A rounding\naccount?",[10,128,129],{},"That question has a correct answer in your jurisdiction and your certification\ndocuments. A float lets you ship without ever asking it.",[22,131,133],{"id":132},"representing-the-split","Representing the split",[10,135,136],{},"For anything where the remainder must be distributed rather than absorbed — splitting a\npot, allocating a jackpot contribution — the pattern is largest-remainder:",[138,139,140,144,147],"ol",{},[141,142,143],"li",{},"Compute each share by integer division",[141,145,146],{},"Sum the shares and compare against the total",[141,148,149],{},"Distribute the difference, one unit at a time, to the parties with the largest truncated remainders",[10,151,152,153,156],{},"It is more code than ",[17,154,155],{},"total * 0.333",". It is also the only version that guarantees the\nparts sum to the whole, which is the property you actually needed.",[22,158,160],{"id":159},"at-the-boundary","At the boundary",[10,162,163,164,167],{},"The last place floats sneak in is serialisation. JSON has one number type, and it is a\ndouble. ",[17,165,166],{},"1234.56"," in a JSON payload is already a float by the time your parser sees it,\nand if your API contract says \"amount is a decimal number\" you have pushed the problem\nonto every consumer.",[10,169,170],{},"Send minor units as an integer, with the currency alongside:",[79,172,176],{"className":173,"code":174,"language":175,"meta":84,"style":84},"language-json shiki shiki-themes github-light github-dark","{ \"amount\": 123456, \"currency\": \"AMD\", \"exponent\": 2 }\n","json",[17,177,178],{"__ignoreMap":84},[88,179,180,184,188,191,194,197,200,202,206,208,211,213,216],{"class":90,"line":91},[88,181,183],{"class":182},"sVt8B","{ ",[88,185,187],{"class":186},"sj4cs","\"amount\"",[88,189,190],{"class":182},": ",[88,192,193],{"class":186},"123456",[88,195,196],{"class":182},", ",[88,198,199],{"class":186},"\"currency\"",[88,201,190],{"class":182},[88,203,205],{"class":204},"sZZnC","\"AMD\"",[88,207,196],{"class":182},[88,209,210],{"class":186},"\"exponent\"",[88,212,190],{"class":182},[88,214,215],{"class":186},"2",[88,217,218],{"class":182}," }\n",[10,220,221],{},"Format for humans at the very edge — in the template, in the UI, after every\ncalculation is finished. Never before.",[22,223,225],{"id":224},"why-this-is-worth-being-dogmatic-about","Why this is worth being dogmatic about",[10,227,228],{},"Most engineering rules deserve exceptions. This one mostly does not, because the cost\nasymmetry is severe: the discipline costs you a small amount of ergonomics on every\ntransaction, and violating it costs you an unfalsifiable accounting discrepancy that you\nwill debug under pressure with real customer money involved.",[10,230,231,232,235],{},"I would rather write ",[17,233,234],{},"applyRake"," than explain drift to an auditor.",[237,238,239],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":84,"searchDepth":97,"depth":97,"links":241},[242,243,244,245,246,247],{"id":24,"depth":97,"text":25},{"id":44,"depth":97,"text":45},{"id":70,"depth":97,"text":71},{"id":132,"depth":97,"text":133},{"id":159,"depth":97,"text":160},{"id":224,"depth":97,"text":225},"2026-06-18","Why there is no floating-point number anywhere in the money path of anything I build, and what replaces it.",false,"md",{},true,"\u002Fwriting\u002Fintegers-all-the-way-down",{"title":5,"description":249},"writing\u002Fintegers-all-the-way-down",[258,83,259],"money correctness","igaming","BC-jq2vA-vxB2s3t0_a6Buqr0Dj6zncDTsppnsk2rXk",[262,369],{"id":263,"title":264,"body":265,"date":358,"description":359,"draft":250,"extension":251,"meta":360,"navigation":253,"path":361,"seo":362,"stem":363,"tags":364,"__hash__":368},"writing\u002Fwriting\u002Fthe-country-your-threat-feed-forgot.md","The country your threat feed forgot",{"type":7,"value":266,"toc":351},[267,270,273,277,280,283,291,294,298,308,311,314,318,321,328,331,335,338,341,344,348],[10,268,269],{},"Every URL reputation system I have worked with leans, directly or indirectly, on domain\npopularity. Tranco, Majestic, Cisco Umbrella — the reasoning is sound: a domain in the\nglobal top 100,000 with years of history is unlikely to be a phishing page spun up\nyesterday.",[10,271,272],{},"Then you point it at Armenia and it tells your users that their bank is suspicious.",[22,274,276],{"id":275},"why-popularity-fails-outside-the-anglophone-web","Why popularity fails outside the anglophone web",[10,278,279],{},"A national bank serving three million people generates a rounding error of global\ntraffic. So does the state tax portal, the electricity utility, the largest local news\nsite, and the government services gateway.",[10,281,282],{},"These domains have every property a reputation system claims to want — a decade of\nregistration history, valid certificates, stable infrastructure, institutional\nownership. They just do not have volume, because there are not enough people in the\ncountry to produce it.",[10,284,285,286,290],{},"Meanwhile the phishing page impersonating that bank is often hosted on a subdomain of a\nservice that ",[287,288,289],"em",{},"is"," globally popular, inheriting reputation it did not earn.",[10,292,293],{},"The signal is not merely weak in small markets. It is occasionally inverted.",[22,295,297],{"id":296},"the-real-cost-is-not-the-false-positive","The real cost is not the false positive",[10,299,300,301,304,305,307],{},"A security product that flags your bank is annoying. A security product that flags your\nbank ",[287,302,303],{},"and"," your tax portal ",[287,306,303],{}," your utility company teaches you that its warnings are\nnoise.",[10,309,310],{},"That is the actual damage. Every unnecessary warning spends a small amount of the\nuser's willingness to believe the next one, and you cannot get it back. By the time a\ngenuine credential-harvesting page appears, the user has been trained by your own\nproduct to click through.",[10,312,313],{},"Alert fatigue is not a UX problem. It is a security failure with a UX-shaped cause.",[22,315,317],{"id":316},"what-to-do-about-it","What to do about it",[10,319,320],{},"The fix is conceptually dull: stop treating global popularity as evidence of\nlegitimacy, and give the system a jurisdiction-aware notion of which institutions\nactually matter locally — sourced from authoritative local records rather than inferred\nfrom traffic.",[10,322,323,324,327],{},"The important part is what that changes. A low global ranking stops being ",[287,325,326],{},"evidence of\nbadness","; it goes back to being what it always was, an absence of information. The\nother signals in the pipeline then decide, on their own merits, instead of starting\nfrom a thumb on the scale.",[10,329,330],{},"I am deliberately not publishing the structure I use, how it is weighted, or how it\ninteracts with the rest of the detection pipeline. Anything specific enough to be\nuseful to another engineer here is also specific enough to be useful to someone\nbuilding a page designed to score well against it. If you are working on this problem\nseriously, get in touch and we can talk properly.",[22,332,334],{"id":333},"the-unglamorous-part","The unglamorous part",[10,336,337],{},"There is no clever algorithm in any of this. Someone has to assemble the data for each\njurisdiction, verify it against official sources, and maintain it as institutions merge\nand rebrand.",[10,339,340],{},"It does not generalise automatically, it does not scale to two hundred countries\nwithout two hundred units of work, and it demos terribly.",[10,342,343],{},"It is also the difference between a product that works where I live and one that does\nnot. Most security tooling is built by people in markets where the global signal\nhappens to be adequate, and the failure mode is invisible from there — you have to be\nin the small market to notice that the tool is quietly wrong about everything that\nmatters.",[22,345,347],{"id":346},"the-general-lesson","The general lesson",[10,349,350],{},"If your detection logic depends on a popularity, volume, or \"we have seen this before\"\nsignal, go and find the population for whom that signal is structurally unavailable.\nThey exist. They are not an edge case to be handled later — they are the group your\nproduct is actively worst for, and they will churn without ever filing a bug report\nexplaining why.",{"title":84,"searchDepth":97,"depth":97,"links":352},[353,354,355,356,357],{"id":275,"depth":97,"text":276},{"id":296,"depth":97,"text":297},{"id":316,"depth":97,"text":317},{"id":333,"depth":97,"text":334},{"id":346,"depth":97,"text":347},"2026-07-09","Domain reputation systems are trained on the anglophone web. Outside it, they flag the sites that matter most — and teach users to ignore you.",{},"\u002Fwriting\u002Fthe-country-your-threat-feed-forgot",{"title":264,"description":359},"writing\u002Fthe-country-your-threat-feed-forgot",[365,366,367],"security","phishing","product","zGecium6aHSjx6aT0oZEPje8imdJH7ZazVME0GDtpaI",{"id":4,"title":5,"body":370,"date":248,"description":249,"draft":250,"extension":251,"meta":516,"navigation":253,"path":254,"seo":517,"stem":256,"tags":518,"__hash__":260},{"type":7,"value":371,"toc":508},[372,374,378,380,386,388,390,392,394,396,404,406,408,410,434,438,440,442,444,452,456,458,462,464,496,498,500,502,506],[10,373,12],{},[10,375,15,376,20],{},[17,377,19],{},[22,379,25],{"id":24},[10,381,28,382,32,384,35],{},[17,383,31],{},[17,385,31],{},[10,387,38],{},[10,389,41],{},[22,391,45],{"id":44},[10,393,48],{},[10,395,51],{},[10,397,54,398,58,400,61,402,67],{},[17,399,57],{},[17,401,57],{},[63,403,66],{"href":65},[22,405,71],{"id":70},[10,407,74],{},[10,409,77],{},[79,411,412],{"className":81,"code":82,"language":83,"meta":84,"style":84},[17,413,414,418,422,426,430],{"__ignoreMap":84},[88,415,416],{"class":90,"line":91},[88,417,94],{},[88,419,420],{"class":90,"line":97},[88,421,100],{},[88,423,424],{"class":90,"line":103},[88,425,106],{},[88,427,428],{"class":90,"line":109},[88,429,112],{},[88,431,432],{"class":90,"line":115},[88,433,118],{},[10,435,121,436,126],{},[123,437,125],{},[10,439,129],{},[22,441,133],{"id":132},[10,443,136],{},[138,445,446,448,450],{},[141,447,143],{},[141,449,146],{},[141,451,149],{},[10,453,152,454,156],{},[17,455,155],{},[22,457,160],{"id":159},[10,459,163,460,167],{},[17,461,166],{},[10,463,170],{},[79,465,466],{"className":173,"code":174,"language":175,"meta":84,"style":84},[17,467,468],{"__ignoreMap":84},[88,469,470,472,474,476,478,480,482,484,486,488,490,492,494],{"class":90,"line":91},[88,471,183],{"class":182},[88,473,187],{"class":186},[88,475,190],{"class":182},[88,477,193],{"class":186},[88,479,196],{"class":182},[88,481,199],{"class":186},[88,483,190],{"class":182},[88,485,205],{"class":204},[88,487,196],{"class":182},[88,489,210],{"class":186},[88,491,190],{"class":182},[88,493,215],{"class":186},[88,495,218],{"class":182},[10,497,221],{},[22,499,225],{"id":224},[10,501,228],{},[10,503,231,504,235],{},[17,505,234],{},[237,507,239],{},{"title":84,"searchDepth":97,"depth":97,"links":509},[510,511,512,513,514,515],{"id":24,"depth":97,"text":25},{"id":44,"depth":97,"text":45},{"id":70,"depth":97,"text":71},{"id":132,"depth":97,"text":133},{"id":159,"depth":97,"text":160},{"id":224,"depth":97,"text":225},{},{"title":5,"description":249},[258,83,259],1784965943894]