Netanya, Israel

Tomer Berger

Backend, AI systems and security.

Final-year B.Sc. Computer Science & Mathematics student. I build backend systems end to end — problem definition, architecture, data model, tests and deployment — on a low-level foundation in operating systems, memory management, networks and assembly.

Currently open to a student position.

Degree
B.Sc. CS & Mathematics, 2027
GPA
98 this year · 89.72 cumulative
Shipped
2 products, end to end

About

I'm completing a B.Sc. in Computer Science & Mathematics at Netanya Academic College through an academic excellence program that ran alongside high school — I finished my matriculation in 2026 and the degree finishes in 2027. I completed the Computer Science matriculation in 9th grade through the same program.

GPA 89.72 cumulative — 98 this year, 95 the year before. Coursework: Secure Programming, Operating Systems, Computer Networks, Assembly, Algorithms, Data Structures, Software Engineering, Object-Oriented Programming.

I've shipped two products that put ML and LLM capabilities into real workflows. What I care about in both is that the output is traceable: a validator that answers differently on the same input twice isn't a reference, and a sizing recommendation nobody can review isn't advice.

Since January 2025 I've tutored high school and college students in computer science and mathematics — currently 10 active students, whose curriculum and progress I manage, reading and correcting their code across a wide range of levels. All of them have passed their courses.

Projects

Bid Request Auditor

OpenRTB 2.6 validator

Live

  • TypeScript
  • Node.js
  • Zero dependencies
  • Vanilla JS
  • node:test

A validator for OpenRTB 2.6 bid requests. Paste a request, drop a .json file on the page, or load one of five samples, and it reports spec violations, invalid AdCOM 1.0 enum values, internal contradictions, and privacy signals contradicted by the identifiers the request actually carries — a COPPA or DNT flag asserted while device IDs, user IDs and ten-metre geo are still being sent. It runs 21 checks across four categories, each citing the spec section it came from and graded by severity. The same rules run three ways: a CLI that exits non-zero on errors so it drops into CI, a browser page, and a Claude Code skill.

Stack

TypeScript, run directly on Node using native type stripping: no compiler, no bundler, no package.json, zero runtime dependencies. The page is plain HTML, CSS and vanilla JavaScript; its one script is generated from src/ by a small Node script using node:module's stripTypeScriptTypes, so the browser runs the real rules rather than a copy of them. Tests are node --test.

What I decided, and why

  • Deterministic rules, no LLM at runtime. Results are reproducible, need no network and cost nothing per run. I used AI heavily to build it and not at all to run it — “the spec says X” has to trace to a line of code and a section number, not to a model's judgment.
  • ERROR only where a spec table literally says “required.” “Recommended” caps at WARNING. Unknown fields and unrecognised enum values cap at INFO, because §2.6 requires implementers to tolerate them. The three privacy contradictions are the one deliberate exception, promoted to ERROR.
  • One engine, two front doors. The CLI and the page import the same modules, so no rule is duplicated, and a test fails if the generated browser bundle drifts from the source.
  • No backend. The audit runs entirely in the browser, so a bid request pasted into the page never leaves the analyst's machine.
  • Cut a check the spec supports. §3.2.19 says city should use UN/LOCODE, but effectively nobody sends it that way. Flagging “Tel Aviv” would have been inventing a problem, so it's out.

AI Surfboard-Sizing Web App

Board dimensions from a video of you surfing

Live

  • Python
  • Cloudflare Workers
  • FastAPI
  • Cloudflare D1
  • Workers KV
  • Stripe
  • Gemini 2.5 Flash

A surfer signs in with Google, uploads a video of themselves in the water, and enters height, weight and skill level. Gemini 2.5 Flash confirms the clip actually shows surfing, assesses technique, and returns a recommended board volume in litres and length in feet and inches. To keep recommendations grounded in real expertise, the app pulls an experienced coach's previous sizing decisions out of the database and feeds them into the prompt as few-shot examples, so the model mirrors a human coach's logic rather than sizing from scratch. Clips that aren't surfing are rejected and the user's bundle is refunded. It runs on Cloudflare Python Workers with D1, and the three bundles are bought through Stripe Checkout, in test mode. Built and tested with the guidance of an Olympic surfing coach.

Stack

Python on Cloudflare Workers — FastAPI and Jinja2 on Pyodide, server-rendered, no frontend framework. Cloudflare D1 for the dataset, reached through prepared statements rather than an ORM; Workers KV for the uploaded clips, behind storage.py; hand-rolled Google OIDC and HMAC-signed cookie sessions; Gemini 2.5 Flash over REST, because the google-genai SDK doesn't run under Pyodide; Resend's HTTPS API for result emails, because a Worker can't open an SMTP socket; Stripe Checkout for the bundles. Queued analyses run from a job table in D1 that a Cron Trigger sweeps once a minute — a Worker can't keep a thread alive past its response.

What I decided, and why

  • Swapped the model out when it couldn't do the job. I prototyped a 3D linear regression over height, weight and a computer-vision-derived skill score. It couldn't capture technique, so I moved to a multimodal model prompted with few-shot examples from the coach's past sizing decisions.
  • Grounded the prompt in real decisions. The few-shot examples come out of the database at request time, not from a fixed prompt, so the model's output tracks the coach's actual record.
  • Kept a human in the loop. An admin dashboard supports manual review, sizing overrides, inventory and user chat, so the model's answer is reviewable rather than final. On the coach tier the model's answer is stored as a draft that an admin has to sign off before the surfer sees it.
  • Moved it off Flask onto Python Workers and D1. Two native Cloudflare services: D1 for the dataset, Workers for the routes. The platform forced most of what followed — the Worker filesystem is read-only and isn't shared between isolates, so the SQLite file and the uploads/ directory had nowhere to live; a thread can't outlive the response, so the background analysis had to become a job somewhere; and neither Authlib nor google-genai runs under Pyodide.
  • Video went to Workers KV, not R2, and that cost something. R2 is the right store for video and was the first implementation, but turning it on puts a payment card on the Cloudflare account even inside the free tier. KV is a config and cache store: 25 MiB per value, 1 GB free, no range requests, and eventually consistent between locations. That last one is the sharp edge — a read returning nothing can mean "not here yet", so the store raises VideoMissing and the sweeper retries, instead of concluding the clip wasn't surfing and refunding a session that was fine. What KV gives back is expirationTtl, which R2 has no equivalent for: every clip carries an expiry, so storage plateaus instead of growing out of the free tier. storage.py is the only file that knows where bytes live, so moving to R2, Supabase or Cloudinary is a rewrite of one file.
  • D1 with prepared statements, no ORM. D1 speaks SQL over a binding and there's no SQLAlchemy dialect for it, so the models became db.py. It reads better than a concession: D1 bills per row read, so it's worth seeing the exact SQL a page runs, and the awkward operations collapse into one statement. Spending a bundle guards itself in the WHERE clause rather than reading a balance and writing it back, so two overlapping requests can't both spend the same one.
  • Made the payment replay-safe. The Buy buttons were href="#". They now open Stripe Checkout, so card details never reach the Worker, and the bundle is granted by the webhook rather than the success redirect — a redirect is just a URL anyone can visit, and the browser may never arrive at it. The webhook is verified by recomputing Stripe's HMAC over the raw body with a constant-time compare and rejecting signatures older than five minutes. The delivery is then claimed with INSERT OR IGNORE, so the check and the claim are one statement: Stripe delivers at least once, and without that, one payment becomes five bundles.
  • Hand-rolled the session layer, and hardened it. Flask's session and Authlib don't exist here, so sessions are an HMAC-signed cookie, CSRF is a token compared in constant time, and Google sign-in is the authorization-code flow written out. The CSRF gate is pure ASGI rather than an HTTP middleware, because reading the token out of the body there drains the receive channel and the route then sees an empty form. HttpOnly and SameSite=Lax as before, Secure now on where the Flask app hardcoded it off, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and uploads capped at 20 MB, under Workers KV's hard 25 MiB per-value ceiling — down from the Flask app's 500 MB. /video_serve/<id> also had no access control at all; it's now owner-or-admin.

Security & systems

The low-level side is where I started, and it's still the part I reach for first when something behaves strangely.

Offensive security labs

From Secure Programming coursework: stack buffer overflows and the countermeasures built to stop them; environment-variable attacks and Shellshock; Set-UID privileged programs, including capability leaking and command injection through system(); SQL injection and XSS.

Reverse engineering

Self-taught from an online video series, working through all five of its levels, plus CTF challenges. Static and dynamic analysis of Linux/x86 binaries.

Systems base

Operating Systems (virtual memory, processes, memory management), Computer Networks and Assembly. Two further cyber and network security courses in the final year of the degree.

This shows up in the work: the auditor's privacy category exists because the interesting failure in a bid request isn't a malformed field, it's a COPPA flag sitting next to a child's advertising ID.

Skills

Languages
  • C
  • Java
  • Python
  • C#
  • TypeScript
  • C++
  • Bash
  • x86 Assembly
  • SQL
Backend & systems
  • Flask
  • FastAPI
  • Node.js
  • REST APIs
  • SQLite
  • Cloudflare Workers
  • Cloudflare D1
  • Stripe API
  • OAuth 2.0
  • Git
  • Vercel
  • Linux
  • OS internals
  • Memory management
  • Computer networks
ML & AI systems
  • LLM & multimodal APIs (Gemini)
  • Few-shot prompting
  • Linear regression
  • Claude Code
  • CLAUDE.md project instructions
  • Plan-first workflows
  • Custom skills
Security
  • Memory corruption
  • Buffer overflows
  • Set-UID & privilege escalation
  • SQLi
  • XSS
  • Reverse engineering
  • CTFs
  • gdb
Spoken
  • English — proficient
  • Hebrew — native

Contact

Email is the surest way to reach me — about the work here, a problem you're stuck on, or anything adjacent.

Currently open to a student position in backend, AI systems or security.

No contact form: this is a static site with no backend, and a form that silently drops what you write is worse than no form at all.