This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

For a lot of people, betting is the side hustle. The ones who last at it tend to say the same thing: write the money rules down and keep them. Never more than a small share of the bankroll on one bet. No six-leg accumulators. Stay out of markets where the house keeps too much. The rules are easy. Keeping them at the moment of placing a bet is the hard part, and the numbers are where it goes wrong without a sound: odds of 17.47 read as 1747, a 14% margin that looks like any other price, a stake that was 12% of the bankroll. Rules don't make betting pay, since the house margin is in every price. What they do is cap the damage, and only if they are applied every time.

betorskip takes a bet slip (pasted text or a screenshot) and gives one word back:

$ betorskip examples/slips/thin-market.txt --rules examples/rules.toml SKIP · the house keeps 14.4% on this market (your max 8%) Redcliff Rangers v Stonebridge · Match Result bet single · Redcliff Rangers @ 1.72 · stake £10.00 · returns £17.20 market 1.72 / 3.20 / 4.00 · house margin 14.4% · fair odds 1.97 (50.8%) 
  • SKIP names the rule the bet breaks.
  • CHECK means something a rule needs could not be confirmed, or the slip disagrees with itself, so you look before placing anything.
  • BET means no rule is broken. It says what share of your bankroll the stake is, how often the bet has to win to break even and, when the whole market is on the page, what the house keeps.

It never says a bet will win. Gemma 4 reads the slip on your own machine, code checks every number against the slip and against the slip's own arithmetic, and your rules, in a TOML file, decide. It is for someone who already bets with rules and wants them applied to every slip, without sending their slips, their bankroll or their history to anyone. The one who would pay for it is the bettor who already pays for a bet tracker or an odds tool and still checks each slip by eye before placing it.

Demo

betorskip: Gemma reads the slip with receipts, code checks every number and the slip's arithmetic, your rules decide

Fractional odds, with the whole market on the page:

$ betorskip examples/slips/uk-fractional.txt --rules examples/rules.toml BET · stake 4.0% of your bankroll · break-even 44.4% Ormsdale Athletic v Millbrook United · Full Time Result bet single · Ormsdale Athletic @ 5/4 (2.25) · stake £20.00 · returns £45.00 market 5/4 / 12/5 / 11/5 · house margin 5.1% · fair odds 2.36 (42.3%) 

A stake the example rules don't allow (the made-up bettor has a 500 bankroll and a 5% cap; first lines only):

$ betorskip examples/slips/big-stake.txt --rules examples/rules.toml SKIP · stake £60.00 is 12.0% of your £500.00 bankroll (your max 5%) 

And one it won't decide for you:

$ betorskip examples/slips/odds-changed.txt --rules examples/rules.toml CHECK · the slip says the price moved: "Odds changed 1.95 → 1.80" Larkhill v Kingsport City · Match Result bet single · Kingsport City @ 1.80 · stake £20.00 · returns £36.00 

Every slip, team and player in the repo is made up.

Code

GitHub logo vinimabreu / betorskip

Paste a bet slip, get BET, SKIP or CHECK: read by a local Gemma, every number checked against the slip and its own arithmetic, decided by your bankroll rules.

betorskip

Paste a bet slip, or point at a screenshot of one, and get one word back: BET, SKIP or CHECK. A model on your own machine reads the slip. Code checks every number it read against the slip and against the slip's own arithmetic, then applies your bankroll rules.

It never says a bet will win. It reads the slip right and holds you to the rules you wrote down.

betorskip: Gemma reads the slip with receipts, code checks every number and the slip's arithmetic, your rules decide

$ betorskip examples/slips/thin-market.txt --rules examples/rules.toml SKIP · the house keeps 14.4% on this market (your max 8%) Redcliff Rangers v Stonebridge · Match Result bet single · Redcliff Rangers @ 1.72 · stake £10.00 · returns £17.20 market 1.72 / 3.20 / 4.00 · house margin 14.4% · fair odds 1.97 (50.8%) 

People who bet seriously write their rules down: never more than a small share of the bankroll on one bet, no six-leg accumulators, no market…

Python, no dependencies, about 1,060 lines plus 134 tests. It needs Ollama with gemma4:12b, and Tesseract for screenshots.

How I Built It

The model reads, with receipts

The slip goes to Gemma 4 12B through Ollama on 127.0.0.1, with a JSON schema. For every fact, the model returns the value and the exact words it came from: a stake with "Stake £20.00", a price with "Ormsdale Athletic 5/4". Odds come back exactly as printed (2.10, 2,10, 5/4, +120) and code turns them into decimal odds. The prompt has no example numbers in it on purpose, so the model has no default to reach for when it can't see.

Code keeps only what the slip backs up

A fact survives only if its quote is really on the slip, its number is really in the quote, and the right label sits next to it. Odds also have to be in their quote as printed, and not be an amount of money: a 10.00 that only appears on the slip as £10.00 is the stake, not a price.

The first slip that tested this was an American parlay. The slip prints "Risk $20.00" and "To Win $64.00". Gemma returned both, and also a returns of 84 with the quote "$84.00". It had added the stake to the profit on its own. The prompt says never to compute one amount from another, and it did anyway. $84.00 is not on the slip, so it is dropped, and the arithmetic below runs on the $64.00 the slip prints.

The slip checks itself

A slip carries its own arithmetic: stake times odds is the return it prints, the legs multiply to the total odds, and a "6-Fold" has six legs. Code checks all three, and that turned out to be the most useful check in the project, because it catches the errors where every quote is fine.

  • A lost decimal point. On a low-resolution screenshot, Tesseract read "Total odds 17.47" as "Total odds 1747". Quote, number and label all pass. But £5.00 at 1747 is not the £87.33 the slip prints, and £5.00 at 17.47 is, give or take the rounding, so the point goes back, with a note saying so.
  • Misread legs. On the same slip, 1.62 came out as 1.82, 1.80 as 4.80 and 1.30 as 4.30. Each one looks like a price. Multiplied, the legs give 173.08 instead of 17.47, and the slip is flagged.
  • "Winnings" that include the stake. Slips word this differently. If the number matches stake times odds it is the return, and if it matches the profit it is the profit; the arithmetic decides, not the label.

When the slip disagrees with itself, the numbers it disputes can't be used to SKIP. Without that rule, one screenshot that dropped the minus of "-110" was skipped as "odds 242.00 are above your max": 110 as decimal odds, times the other leg's 2.20.

Screenshots: text you can check

There are two ways to read a screenshot: Gemma reads the image, or Tesseract turns it into text and Gemma reads the text. I rendered the twelve made-up slips as app-style screenshots, in four styles and two resolutions, and measured both.

Reading the pixels is often right, and it can't be checked. On a small screenshot of a slip with a £4.00 stake and £36.00 returns, Gemma read £40.00 and £360.00, and even the arithmetic agrees: 40 at 9.00 is 360. Nothing inside that reading says it is wrong. So a screenshot goes through Tesseract first, and every quote is checked against text the model did not write.

Tesseract fails in its own ways, all found by measuring:

  • Its default layout analysis reads a slip as two columns, every label first and every amount after, so "Stake" ends up far from "£10.00" and the label check fails. Reading the slip as one column (--psm 4) took the clean screenshots from 19 to 24 correct verdicts out of 24.
  • It reads "returns" as "retums" or "tums", the plus of American odds as #, and drops minus signs.
  • When a line comes out garbled, Gemma tidies it. From "Redcliff Rangers 472" and a Stonebridge with no price, it quoted "Redcliff Rangers 1.72 Draw 3.20 Stonebridge 4.72". That line is not in the text, so the quote check refuses it.

The fixes above were made on one batch of screenshots. Then I rendered the same slips in two styles I had not used and ran a small-JPEG batch once, made one more fix from what it showed (the margin rule now also stands down when the pick's odds are disputed), and ran a last batch once. Nothing was tuned on that last one, and the final code gives the same 24 verdicts on it:

screenshots right CHECK wrong
new styles, small JPEG 10 13 1 SKIP that should have been BET
new styles, 2x PNG, like a phone screenshot 17 6 1 BET that should have been SKIP

The wrong BET is the number to keep in mind. Tesseract dropped the "Draw 3.20" line of a market where the house keeps 14.4%; with two prices out of three there is no margin to compute, so the margin rule had nothing to apply to and the slip passed. It now says "house margin unknown" when that happens, but it is still a BET. Worse, in that batch Tesseract read a whole market on only 2 of the 24 screenshots, neither of them a BET: none of the seven BETs had the margin rule applied, and five were right only because the made-up markets sit under 8%. A price you did not pick, read wrong, would change the margin with nothing on the slip to catch it. On the small JPEGs most slips came back CHECK: that is the tool noticing text it can't trust, not reading it right.

The rules are a file

[bankroll] bankroll = 500 max_stake_pct = 5 [odds] min_odds = 1.30 max_odds = 6.0 max_legs = 3 max_margin_pct = 8 [markets] never = ["correct score", "first goalscorer", "bet builder"] 

An unknown setting, a setting given twice, a wrong type, a negative or non-finite number, or an empty word in never is refused, so a typo can't silently switch a rule off. --bankroll overrides the bankroll for today, and --prob 52% turns your own estimate into expected value and a Kelly stake, never above your max stake. The probability is yours; the tool only does the arithmetic.

Tested

Each of the twelve made-up slips, one for each rule or odds format, went through Gemma 4 12B three times: 36 of 36 runs gave the expected verdict, with the same first line every time, at a median of 5.5 seconds a slip on my laptop. Twelve slips is a small set: it says the reading is stable on clean text, not that it is right on every slip.

The 134 tests replay recorded answers, for the twelve slips and for four low-resolution screenshots, so they run in CI without a model. Each check has the failure that motivated it as a test: the $84.00, the lost decimal point, the misread legs, the lost minus sign, retums, a single quoted twice, a market misread three times the same way.

Why Does Open Innovation Matter?

It runs on every slip and costs nothing per slip. A check you have to pay for per call is a check you skip on the small bets, which are most of them.

What it reads stays with you. A bankroll and a betting history are about as private as numbers get. The model client refuses any Ollama host that is not loopback and ignores proxy settings, so nothing leaves the machine by accident.

You can see and change every step. The prompt is a string in the repo, the schema is a dict, the OCR mode is one flag. When Tesseract's layout analysis split labels from amounts, I could measure it and change one argument. When Gemma tidied a garbled line into a quote that didn't exist, the fix was a check in plain code, not a request to a vendor.

Prize Categories

Best Use of Gemma: Gemma 4 12B, running locally through Ollama, reads every slip and returns each fact with the words it came from. The repo also measures where Gemma reading screenshots directly goes wrong in ways nothing can check, and why the tool gives it OCR text and the slip's own arithmetic instead.