# Serigne Saliou Lo AI engineer · Dakar, Senegal · hello@zaleo.dev GitHub https://github.com/saliou33 · LinkedIn https://www.linkedin.com/in/saliou33 · Site https://zaleo.dev ## Summary These days that mostly means AI engineering: agents, retrieval, evaluation, and the infrastructure that keeps them honest. Before that I spent two and a half years turning satellite data into forecasts for farmers, and before that I built banking systems. I think of myself as a seeker. I pick a domain, learn it properly, build something real in it, then move on to the next question. Path so far: 2018 Computer science (ESP Dakar) → 2020 Registry systems (NIC Sénégal) → 2023 Banking systems (Innov4Africa) → 2024 Geospatial & data (Tolbi) → 2025 Machine learning (production) → 2026 AI engineering (now) → next Robotics → after Neuroscience → … wherever it leads ## Highlights - **1B+** rows of model training data generated from satellite pipelines - **10k+** farmers reached by weather-driven voice advisories - **137×** faster on the heaviest yield endpoint, measured on 10,000 parcels - **2.08M** parcels in a single country-scale time-series extraction ## Now - **Learning: AI engineering, from the maths up.** Working through AI Engineering from Scratch, where every algorithm is derived from the maths before any framework is imported: backprop, a tokenizer, attention, an agent loop. Revising the machine learning concepts underneath as I go. (https://aiengineeringfromscratch.com) - **Building: A harness for code I don't trust.** Agency runs AI-written code in rootless, read-only sandboxes, driven by a Postgres state machine, with one-hour credentials scoped to a single repo, diffs checked before anything is pushed, and a reviewer from another model family. Whatever it builds is a side effect. The architecture is the point. (https://zaleo.dev/projects/agency) - Further down the line: robotics and neuroscience. - Updated October 2026 ## Capabilities ### AI Engineering Where most of my attention goes now. LLM features grounded in real data and safe to expose: retrieval and how to measure it, tool-calling agents, an MCP server that re-checks permissions on every call, and reports that still render when the model is down. - MCP server with tenant isolation - Bilingual PDF reports with LLM narrative - Sandboxed coding agents (Agency) ### Backend Development Django, FastAPI and Spring Boot services that hold up under real data: PostGIS, Celery and RabbitMQ, careful query work. I'd rather remove the 1,800 per-parcel queries than add a cache in front of them. - Yield API: 31.5 s → 0.23 s on 10k parcels - ~730 tests on the flagship service - Banking microservices over Kafka ### Distributed Systems Work that doesn't fit on one machine: Beam/Dataflow jobs with hundreds of workers, Celery chords per field, idempotent batch submission, Pub/Sub workers on Cloud Run, caches keyed so cost stays flat as projects grow. - Dataflow tiling at up to 500 workers - Climate cache: 130 s cold → 3 s warm - Earth Engine, BigQuery, Cloud Run, AWS ### DevSecOps Pipelines that test, tag by commit and wait for the rollout before calling it done. Hashed API keys, tenant-scoped RBAC, mTLS between bank zones, rootless sandboxes for AI agents. Security is part of the design, not a final checklist. - CI/CD across GCP, AWS, DigitalOcean - TLS/mTLS across MZ/DMZ network zones - 9 production databases migrated ### Full-Stack Engineering When a product needs shipping, I do the whole thing: Next.js frontends, Postgres schemas with row-level security, auth, storage and deploys. Dawah and Yégle (first launched as DawahRepost and SenPost) are mine end to end. - Dawah, Yégle, Ummah, Daara: solo, live - Next.js · Supabase · Convex · R2 - Rust/Axum for the heavy parts ## Experience ### Backend/GIS Consultant, Tolbi (Apr 2024 — now, Dakar) Agritech platform for crop monitoring, forecasts and climate data across West Africa. I joined as a backend developer, moved to a permanent contract after my first year, and today I'm the main author of most of the platform's services. - Built and run the yield forecasting backend that serves per-parcel results to cooperatives. - Wrote country-scale geospatial data pipelines (1B+ rows of training data), working with Google engineers through Google for Startups. - Added the platform's AI features and bilingual reports. - Worked on compliance tooling, farmer-facing voice and phone access, and shared auth, credits and CI/CD across services. ### Java Full-Stack Developer, Innov4Africa (Feb 2023 — Feb 2024, Dakar) Fintech and banking software for West African financial institutions. - Built secure banking microservices talking over Kafka, deployed across MZ/DMZ network zones with TLS and mutual TLS. - Worked on an early integration with PI-SPI, the BCEAO instant-payment interoperability platform for the UEMOA zone, including ISO 20022 message conversion. - Built the backend of a BPM platform on Activiti 7 and Spring Boot (Angular frontend) that banks used to define, schedule and monitor workflows, with ELK for monitoring. ### Software Engineering Intern, NIC Sénégal (Jun — Aug 2020, Dakar) The registry for Senegal's .sn domain. - Built a domain management web app for the registry's registrars, with spreadsheet exports and reporting. ## Education - MSc Computer Science, École Supérieure Polytechnique de Dakar (2018 — 2023). GPA 3.7 · certificates in CyberOps, Machine Learning and Deep Learning ## Recognition and open source - Winner, Cloud track, Huawei ICT Competition, Senegal (2022) - Upstream bug report: Dataflow runner options silently ignored, google-deepmind/geeflow (2025) https://github.com/google-deepmind/geeflow/issues/1 ## Tools - Languages: Python, TypeScript, Java, Rust, Kotlin, SQL - Backend: Django, FastAPI, Spring Boot, Axum, Celery, Kafka, RabbitMQ - Data & geo: PostGIS, BigQuery, Earth Engine, Beam / Dataflow, GDAL, TiTiler, COG - AI: RAG, MCP, pydantic-ai, pgvector, Qdrant, Vertex AI, Whisper - Infra: GCP, AWS, Cloudflare, DigitalOcean, Docker, Coolify, GitHub Actions ## Case studies ### Yield forecasts at 10 metres, for thousands of one-hectare farms Tolbi · Yield · 2024 — now · https://zaleo.dev/work/yield-forecasting Per-parcel yield forecasts, crop stages and fertility maps for smallholder farms, served fast and reliably. - 137×: faster heaviest endpoint (31.5 s → 0.23 s) - 28,000 → 1: PNG tiles per season replaced by one COG per layer - ~730: tests on the service Cooperatives upload their parcels and get back, for each field, a yield forecast and the maps that go with it, down to 10 metres. I've been the main author of this backend since 2024. My work was on the engineering side: keeping the product stable while the models behind it changed, making results correct and reproducible, and making the heaviest parts of the API fast enough to use day to day. Stack: Django, GeoDjango, PostGIS, Celery, RabbitMQ, Earth Engine, Vertex AI, Cloud Run, GDAL, TiTiler ### Pipelines at country scale Tolbi · Data platform · 2024 — 2025 · https://zaleo.dev/work/country-scale-pipelines Large-scale geospatial data pipelines covering whole countries, which produced the training data behind Tolbi's models. - 1B+: rows of model training data - 2.08M: parcels in one time-series extraction - 500: Dataflow workers on the tiling job These jobs process satellite data for entire countries and turned it into training data for the models: more than a billion rows. I wrote and ran the pipelines, worked with Google engineers on scaling them through Google for Startups, and kept an eye on cost as they grew. Stack: Apache Beam, Dataflow, BigQuery, Earth Engine, GeeFlow, Tippecanoe, Cloud Run ### Putting an LLM on farm data without leaking it Tolbi · AI · 2025 — 2026 · https://zaleo.dev/work/ai-on-farm-data AI features on top of agronomic data, built in three iterations, with reports that keep working when the model doesn't. - 3: iterations: RAG → agent → MCP - FR / EN: PDF reports with LLM narrative - 0: IDs leaked through error messages I added the AI layer to the platform step by step, so users can ask questions about their own farms and get answers grounded in their figures, without one customer's data ever reaching another. The bilingual PDF reports use the model for the narrative and fall back to plain figures if it's unavailable, so a report never fails because of the AI. Stack: pydantic-ai, MCP, pgvector, Gemini, OpenAI, WeasyPrint, SSE ## Projects ### Daara (2026, live) Memorise the Quran, with recitation checked on the phone. Role: Solo. https://zaleo.dev/projects/daara live: https://daara.zaleo.dev Listen to a reciter, repeat the verse the way children do at the daara, then recite it. Daara listens in the browser, marks each word right or wrong, and brings verses back with spaced repetition just as they start to fade. Families can split a khatm by sending one link on WhatsApp. It checks words, not tajwid rules, and only knows the Hafs reading. - Speech recognition runs on the device: a Whisper tiny model fine-tuned on Quran recitation, exported to ONNX (4-bit encoder, 8-bit decoder, about 40 MB) and run with Transformers.js. The voice never leaves the phone. - 98.0 % of words recognised correctly on a 48-clip benchmark of EveryAyah reciters, about 6 s per verse on a mid-range phone. - Each word is compared with the verse text, so the hidden text comes back one word at a time as you say it right. - FSRS spaced repetition for review; an optional Zaleo account syncs progress through Convex. - Works offline as an installable PWA; the model is downloaded once and kept. Stack: Next.js, Transformers.js, ONNX Runtime Web, Whisper, ts-fsrs, Convex, Cloudflare R2, PWA, Coolify ### Ummah (2026, live) Quran, hadith and tafsir search where every line has its source. Role: Solo. https://zaleo.dev/projects/ummah live: https://ummah.zaleo.dev Search the Quran, hadith and tafsir in French, English or Arabic. Paste a hadith someone forwarded on WhatsApp and Ummah finds the closest narration in the nine collections, with its reference and grade, or says it found nothing. A small wiki sits on top, where every sentence cites a verse or a hadith and every edit is reviewed. It shows texts and references, not rulings. - 6,236 verses, 36,108 hadith and the al-Mukhtasar tafsir, indexed in Meilisearch with hybrid keyword and vector search. - Embeddings come from a self-hosted multilingual MiniLM (int8 ONNX, 384 dimensions) on my own server; the corpus was embedded locally, and no third-party AI is used for search. - The checker scores forwarded text against the narrations by wording and by meaning, and every check becomes a public page so the next person gets the answer at once, with phone numbers and e-mails masked. - Wiki on Convex with revisions, roles and a review queue; sign-in through Zaleo Auth. Stack: Next.js, Meilisearch, ONNX Runtime, MiniLM, Python, Convex, Docker, Coolify ### Suuf (2026, live) Check a plot of land before you buy it. Role: Solo. https://zaleo.dev/projects/suuf live: https://suuf.zaleo.dev Draw a plot on a satellite map and Suuf reads ten years of open satellite data around it: a dry-season image for every year since 2017, radar flood history that sees through the clouds, terrain, surface water, and the roads and services nearby. You get a signed report in about a minute. It checks the ground, not the title, and says so on every page. - Python engine on rasterio and STAC: a cloud-masked Sentinel-2 composite per dry season, vegetation and built-up trends, and Sentinel-1 radar for flooding, all read as small windows from Cloud Optimized GeoTIFFs. - Copernicus DEM for slope and low ground, JRC Global Surface Water for 37 years of water, and OpenStreetMap for access, queried in parallel with hedged mirrors and an area cache. - Every report is Ed25519-signed. A QR code on the page verifies it publicly without revealing the plot. - Reports are private to whoever saved them, and shared through links that expire or can be switched off. - Weekly re-checks with email alerts when water, clearing or building appears; imagery served from Cloudflare R2. Stack: Python, FastAPI, rasterio, STAC, Sentinel-1, Sentinel-2, Copernicus DEM, SQLite, Next.js, MapLibre, Cloudflare R2, Resend ### Kàddu (2026, live) Speech AI for Wolof, measured in the open. Role: Solo. https://zaleo.dev/projects/kaddu live: https://kaddu.zaleo.dev An API that turns spoken Wolof into text and Wolof text into speech, with translation between Wolof, French and eight other languages. It runs open models on a CPU server, and every accuracy number on the site was measured on a public test set. A consented record-and-validate app collects new Wolof speech for the next model. Free during the preview. - Benchmarked the open Wolof models on Google FLEURS (word and character error rates, speed) and serve the best one among commercially usable licences. - FastAPI inference on CPU: XLS-R speech-to-text on ONNX Runtime int8, about a quarter faster than PyTorch at the same accuracy, SpeechT5 text-to-speech and NLLB-200 translation. - Convex backend for API keys (stored hashed, daily quotas checked in one transaction), usage dashboards and the contribution flow; recordings go to private R2 storage through presigned URLs. - The hero spectrogram is drawn live from the real audio with the Web Audio API. - Next: distil several open Wolof models into one fast model, using their agreement on untranscribed radio audio as training labels. Stack: Python, FastAPI, PyTorch, ONNX Runtime, Transformers, Convex, Next.js, Cloudflare R2, Vercel ### Dawah (2025, live) Schedule verses and hadith on your social accounts. Role: Solo. https://zaleo.dev/projects/dawah live: https://dawah.zaleo.dev Pick a verse, a hadith, a range of surahs or a recitation clip; Dawah turns it into a styled image with its reference and posts it on time to Facebook, Instagram, X, TikTok, YouTube and LinkedIn. A whole series can be prepared in one go. It started in 2025 as DawahRepost, where I first built the Postgres-driven scheduler; in November 2026 it moved into the Zaleo family under a new name, on Zaleo Auth, and became free for good. - Batch generator that turns a source range into a styled image series and spreads it across the calendar in one go. - Verse and hadith text always comes from verified editions, with every reference checked; AI only suggests captions, if you want it to. - pg_cron claims due posts in Postgres and hands them to a throttled FastAPI publisher, with per-network results and post stats. - Month, week and agenda calendar views with drag to reschedule; a free Quran image designer needs no account. - Sign-in moved to Zaleo Auth without losing data: old accounts are matched by verified e-mail. Stack: Next.js, FastAPI, Supabase, pg_cron, Zaleo Auth, Composio, Docker, Coolify ### EarthFlow (2026, live) A node canvas for geospatial pipelines. Role: Solo · five-day build. https://zaleo.dev/projects/earthflow live: https://earthflow.zaleo.dev What if GIS analysis felt like ComfyUI? Wire data sources and operations on a canvas, run the graph, and check every intermediate result on a map. The engine is Rust with GDAL underneath; demos and a quick GeoJSON viewer are open without an account. - Rust/Axum engine: topological sort with cycle detection, typed values flowing between named ports, fan-in merged into arrays. - GDAL-backed nodes: slope, reclassify, contours, viewshed, zonal statistics, reproject, merge, polygonize, plus GEOS vector operations. - Every raster output is written as a Cloud Optimized GeoTIFF and served as map tiles through TiTiler; node cards preview their own output. - Incremental runs reuse unchanged outputs, run history survives reloads, and "Run to here" keeps the rest of the graph's results. - Supabase sign-in with Google, organisations and scoped workflows; public demos and a no-login editor for quick checks. Stack: Rust, Axum, SQLx, GDAL, GEOS, PostGIS, MinIO, TiTiler, Next.js, React Flow, MapLibre, Supabase ### Mirath (2026, live) Islamic inheritance shares, calculated and explained line by line. Role: Solo. https://zaleo.dev/projects/mirath live: https://mirath.zaleo.dev Enter who was alive at the time of death and what there is to share. Mirath gives each heir's share as a fraction and in CFA francs, explains each step with the Quran, the Sunnah and the matching article of Senegal's Family Code, and includes a zakat calculator. Maliki by default, as in Senegal; the other three schools can be switched on. Everything runs in the browser. - Pure TypeScript engine on exact bigint fractions, with the classical special cases (mushtaraka, akdariyya, the two 'umariyyatan, radd) handled the way the Family Code writes them. - Checked against 65 solved cases from the Risala, Khalil's Mukhtasar and the Sirajiyya, plus invariant checks on 4,000 random families; the build runs them, so a broken case blocks the deploy. - No server and no account: a shared case travels in the URL after the #, which browsers never send. - French first, with English and Arabic (right-to-left). Stack: TypeScript, Next.js, Vitest, next-intl, Docker, Coolify ### Arnaque? (2026, live) Paste a suspicious message, get a verdict and what to do. Role: Solo. https://zaleo.dev/projects/arnaque live: https://arnaque.zaleo.dev A text saying money was sent by mistake, a fake Wave bonus, a job offer with a file fee: paste the message, a screenshot, a number or a link, and Arnaque? answers in seconds with a verdict (likely scam, suspicious, or no obvious sign), the reasons in plain French, key phrases in Wolof and what to do now. Built for Senegal and French-speaking West Africa. - Rules first: code requests, upfront fees, payment to a personal number, lookalike or throwaway links. They run in the browser too, so a check works offline, and the AI can explain or raise a score but never clear a hard rule. - AI explanations fall back from Gemini to Groq to Workers AI, cached by text hash and capped per device, per network and globally. - Only masked text reaches an AI: numbers, e-mails, codes, card and account numbers and names are replaced first. Raw messages, IPs and device ids are never stored. - Suspect messages are grouped into recurring scam templates with MinHash and LSH; a phone number is shown in full only after 3 independent reports. Stack: Next.js, Convex, Gemini, Groq, Cloudflare Workers AI, PWA, Coolify ### Courant (2026, live) Live map of power and water cuts in Dakar. Role: Solo. https://zaleo.dev/projects/courant live: https://courant.zaleo.dev Residents tap once when the power or water goes off and again when it comes back. Courant confirms a cut when enough independent signals agree, shows it by neighbourhood, notifies the people who follow that area, and keeps the history as open data that nobody else publishes for Senegal. Not affiliated with Senelec or SEN'EAU. - Crowd reports plus sentinel phones: an old Android phone left on a charger reports mains power through the Battery Status API every 60 s, and counts double. The same heartbeat API takes ESP32s and smart plugs. - An outage state machine in Convex: confirmed at weight 3 within the hour, restored at weight 2, a lone report expires after 60 min, and all signals behind one IP are capped. - Neighbourhood shapes built from OpenStreetMap place points, Voronoi-clipped to commune boundaries. - Web Push alerts, Internet-outage alerts from IODA for Senegal's 14 regions, a monthly bulletin and a public API. Stack: Next.js, Convex, MapLibre, OpenStreetMap, Web Push, PWA, Coolify ### Sajda (2026, live) Learn the prayer one position at a time, in 3D. Role: Solo. https://zaleo.dev/projects/sajda live: https://sajda.zaleo.dev A faceless 3D figure on a prayer mat walks through each position of the salah, with what to say, the common mistakes and the differences between the four Sunni schools, each one cited. Prayer times are computed on the phone and shown next to your mosque's own times. No account, no tracking, and it works offline. - The figure is built in code, not a downloaded model: a bone rig, a library of poses, and a contact solver that puts forehead, nose, palms, knees and toes on the floor in sujud. - Modest by construction: one continuous robe hangs from the shoulders and only grows to enclose the legs, so no body shape shows in any pose. Hands press the cloth in instead of sinking into it. - Every school difference and correction cites a hadith (collection and number) or a standard school text; what couldn't be verified is listed, not taught. - Prayer times on the device with adhan, a mosque-time column (in Senegal, Dhuhr is prayed around 14:00) and a share image sized for WhatsApp. - Wudu in 3D, sujud as-sahw, a kids mode with stars and a 'what comes next?' mosque quiz; every pose also exists as a pre-rendered still. Stack: Next.js, React Three Fiber, three.js, adhan, Playwright, PWA, Vercel ### Yégle (2025, live) Write a post once, schedule it on every network. Role: Solo. https://zaleo.dev/projects/yegle live: https://yegle.zaleo.dev Shops, creators and small teams write a post once, add an image or a video, pick the accounts and the time, and Yégle publishes to Facebook, Instagram, X, LinkedIn, TikTok, YouTube, Pinterest and Bluesky, then shows what actually went out, account by account. It launched in 2025 as SenPost; in November 2026 it was renamed Yégle (Wolof for "to announce"), redesigned, and moved onto Zaleo Auth. Free during the relaunch. - Every minute a pg_cron job claims due posts in one atomic UPDATE … RETURNING, respecting each account's timezone, and hands the batch to the publisher. A sweeper fails anything stuck so nothing silently hangs. - The FastAPI publisher fans out per account under a concurrency limit, with exponential retries and per-platform error reasons. - Post stats synced every 15 minutes; a dashboard with success rate and on-time rate per network. - AI studio for captions and images (Gemini with Groq and Workers AI fallbacks, Flux images), or the user's own key. - Public REST API with tokens and rate limits; old senpost.cc links redirect to the new address. Stack: Next.js, FastAPI, Supabase, pg_cron, Zaleo Auth, Composio, Gemini, Groq, Cloudflare Workers AI, Coolify ### Zaleo Auth (2026, live) One account for every app, without the redirects. Role: Solo. https://zaleo.dev/projects/zaleo-auth live: https://auth.zaleo.dev The sign-in behind every Zaleo app, from Kàddu and Suuf to Dawah and Yégle. It is a full OpenID Connect provider, but apps on zaleo.dev never send people away to it: each app shows its own sign-in form in its own design, and the shared session arrives as a short-lived signed token that app servers check offline. - Better Auth on Hono and PostgreSQL: email and password, Google, and verification and reset emails through Resend. - Embedded mode: one cookie on .zaleo.dev, CORS limited to the registered apps, and a 15-minute RS256 JWT that backends verify against the public JWKS, with no call home per request. - A small kit (@zaleo/auth) gives each app the forms, the account menu and server helpers, worded in the app's own name. - Locked down: only the operator registers apps, and every client-management endpoint is disabled. - Classic OIDC redirects still work for apps outside zaleo.dev. Stack: TypeScript, Hono, Better Auth, OpenID Connect, PostgreSQL, Docker, Coolify ### Zaleo Apps (2026, live) The catalogue of my public apps, with live status. Role: Solo. https://zaleo.dev/projects/zaleo-apps live: https://apps.zaleo.dev One page that lists the public Zaleo apps for the people who use them, not for recruiters: what each one does, who it is for, what it keeps about you and what it can't do. French first, English second. Each app is painted in its own colours, like stalls in a Dakar market. - Live up/down status for every app, refreshed every minute. - Signed-in visitors see which apps they already use, through Zaleo Auth. - A drop-in web component, generated from the same data file, lets any app show a switcher to the others. - Hosts the plain-language privacy, terms and data-deletion pages the social networks require. Stack: Next.js, Web Components, Zaleo Auth ### Agency (2026, live) Running AI-written code without trusting it. Role: Solo. https://zaleo.dev/projects/agency An experiment in containment: how do you let AI agents write and run code on your infrastructure without handing them the keys? Agency is the harness: an orchestrator, a Postgres state machine, disposable rootless sandboxes, short-lived credentials and an independent reviewer. The side projects it produces are almost a by-product. - Explicit task state machine in Postgres: legal transitions only, row locks on every change, work claimed with SKIP LOCKED. - Workers run on a separate rootless Docker daemon, read-only, all capabilities dropped, with memory, CPU, process and wall-time limits. - The agent never holds push credentials. It hands back a git bundle; the orchestrator checks diff size and protected paths, then pushes with a one-hour token scoped to a single repo. - Every change passes the project's own verification and a review by a model from a different family before it reaches me. - When a model runs out of quota it's benched until reset and the next one in the pool takes over. Stack: Python, Postgres, Docker (rootless), GitHub App, Telegram, Coolify ### FlowBot (2025, paused) WhatsApp automation you draw instead of code. Role: Backend & workflow engine · frontend by Alioune Sall. https://zaleo.dev/projects/flowbot Small businesses draw their WhatsApp bot on a canvas: triggers, messages, conditions, loops, API calls, LLM agents. I wrote the engine that runs those drawings as real conversations, including the parts where the bot waits for a customer for three days and then picks up exactly where it stopped. - Resumable graph executor: a run can pause on a node waiting for a reply, persist its state and resume on the next webhook, with a per-execution lock so two incoming messages can't race the same run. - For-each loops fork child executions with their own variable scope; the parent completes when the last child does. - Every node has a token cost, deducted atomically against the business's plan in one database call. - LLM agents with RAG (Qdrant), conversation memory and Composio tools across OpenAI, Gemini, Anthropic and Groq behind one adapter. - Real-time voice agent for WhatsApp Business calls (Pipecat: VAD, speech-to-text, LLM with tools, text-to-speech), plus voice notes via Whisper. - Two transports: the official WhatsApp Cloud API and a self-hosted gateway for numbers that aren't on it. Stack: FastAPI, Supabase Postgres, pg_cron, Qdrant, pydantic-ai, Pipecat, Next.js, Coolify ### Vipath (2024, live) Pathfinding, animated. Role: Solo. https://zaleo.dev/projects/vipath live: https://vipath.zaleo.dev · repo: https://github.com/saliou33/vipath A teaching tool written from scratch: draw walls on a grid, drag the start and end, and watch A*, Dijkstra, BFS, DFS, bidirectional BFS or greedy best-first search find a way through, step by step. - Hand-written A*, Dijkstra, BFS/DFS and bidirectional BFS, plus the priority queue under them. - Walls, start and end are drawn and dragged directly on the grid; speed and play/pause controls. Stack: TypeScript, React, Vite ### Visort (2024, live) Sorting algorithms, animated. Role: Solo. https://zaleo.dev/projects/visort live: https://visort.zaleo.dev · repo: https://github.com/saliou33/visort A companion to Vipath: watch eight sorting algorithms work on the same data, each one recorded as a sequence of steps that the UI plays back at your pace. - Eight algorithms: bubble, selection, insertion, shell, merge, quick, heap and radix sort. - Adding an algorithm means writing a single function that emits steps; the player does the rest. Stack: JavaScript, React, Vite ### Zale TV (2026, maintained) An Android TV media app, rebuilt around its player. Role: Fork maintainer · 218 of 220 commits since the fork. https://zaleo.dev/projects/zale-tv I forked an Android TV media app at v1.0 and have taken it to v2.7x, rewriting most of what happens once you press play. The part I'm proudest of is the dialogue control: the old one had never done anything on most devices, and finding out why took longer than fixing it. - Dialogue enhancement as a custom audio processor on the sink: mid/side lift, a presence filter, a compressor, then a speech-band detector and a limiter. It works with hardware decoding, which the old renderer-side boost never did. - Sources are resolved in parallel and the first healthy one wins; failing sources drop to the back of the queue. Streams are probed before playback with the headers each CDN expects. - Subtitle charset detection, in-app diagnostics, and a self-updater that installs new releases from GitHub. - Tag-driven CI that builds signed APKs for 32- and 64-bit ARM and publishes the release. Stack: Kotlin, Jetpack Compose for TV, Media3 / ExoPlayer, Coroutines, GitHub Actions ### Frugal Router (2026, prototype) Cheapest model per LLM request that clears the quality bar. Role: Solo. https://zaleo.dev/projects/frugal-router frugal-router classifies each LLM prompt on local signals — token estimate, JSON/table structure, task-type keywords, few-shot count, history depth — and maps it to an economy, standard, or premium model tier with a confidence score. A routing policy enforces a monthly budget cap, latency SLO, and confidence floor, walking fallback chains under budget pressure and logging each decision with tier, reason, remaining budget, and SLO flag. OpenAI and Anthropic adapters return per-call cost and latency telemetry, and a benchmark harness prices an always-premium baseline against the routed policy over a JSONL eval set. - Classifier maps prompts to a tier with confidence; ~50 µs p95 against a 5 ms budget, ≥ 90% accuracy on 101 labeled prompts. - Routing policy enforces a monthly budget cap, latency SLO, and quality floor; every decision is logged and replayable. - OpenAI and Anthropic adapters record cost and latency on every call, with injectable httpx transports. - Benchmark replays the eval set, always-premium vs routed: 55.7% savings and 0% regression on simulated outcomes with mock-tier pricing. Stack: Python, uv, httpx, pytest, ruff, pyright ### PlanLens (2026, live) Postgres plan analysis in-browser: hotspot tree + index DDL. Role: Solo. https://zaleo.dev/projects/planlens live: https://planlens.zaleo.dev PlanLens turns a pasted Postgres EXPLAIN (ANALYZE, FORMAT JSON) plan into an interactive tree with per-node timing, ranked findings and copyable CREATE INDEX DDL. Hotspots are ranked by exclusive time, misestimation magnitude and sort/hash spills, and index suggestions derive from filters, sort keys and join conditions, each with a rationale and a check-with-EXPLAIN caveat. Parsing, analysis and share links run entirely client-side, so plans containing real data never leave the machine. - Normalizes 50+ node types into a tree; exclusive time is derived by subtracting children's inclusive time. - Quantifies row-estimate drift per node as a signed log ratio between planner estimate and actual rows. - Generates CREATE INDEX DDL with equality-before-range-before-sort column order, partial and expression indexes, and prefix deduplication. - Flags filtered Seq Scans, sort and hash spills, dead branches, nested-loop misestimates, and trigger or JIT time. Stack: TypeScript, React, Vite, Tailwind CSS, shadcn/ui ### Raft Lab (2026, live) Crash nodes, partition a simulated cluster, and watch Raft election and replicat. Role: Solo. https://zaleo.dev/projects/raft-lab-browser live: https://raft-lab-browser.zaleo.dev Distributed-systems students and interview candidates read the Raft paper but never see elections, term fencing and log reconciliation happen; existing visualizers are canned animations you cannot break, so the failure modes never become intuitive. - The hard part is a deterministic, tick-driven Raft implementation (elections, log replication, commit rules) that is also the engine driving the UI, plus property-based tests proving safety invariants (at most one leader per term, election restriction, log matching, committed-prefix) across thousands of random crash/partition scenarios; success is measured by those invariants holding under fuzzing and by the UI replaying any failing seed step by step. Stack: TypeScript, React, Vite, Tailwind, shadcn/ui, vitest, fast-check ### Retrieval Ratchet (2026, prototype) Deterministic retrieval eval that gates merges in CI. Role: Solo. https://zaleo.dev/projects/retrieval-ratchet Retrieval Ratchet makes retrieval quality a merge gate for RAG applications. A seeded generator derives QA sets from the corpus itself with no LLM calls, a pluggable retriever protocol feeds a metric engine computing recall@k, MRR, precision@k, and lexically-grounded faithfulness, and `check` exits non-zero when results fall below a checked-in baseline. It runs offline in seconds inside GitHub Actions, with byte-identical QA output for a given seed. - Seeded synthetic-QA generator derives questions from the corpus itself — no LLM calls, byte-identical output per seed. - Metric engine computes recall@k, MRR, precision@k, and lexically-grounded faithfulness, stable run-to-run. - Regression gate compares against a checked-in baseline with float-noise tolerance; the exit code is the merge gate. - Retriever protocol needs only retrieve(query, k); TF-IDF and HTTP adapters pass a shared contract suite covering determinism and unicode queries. Stack: Python, uv, pytest, GitHub Actions, TF-IDF ## Writing ### Serving rasters on a small budget 2026-10-25 · https://zaleo.dev/blog/serving-rasters-on-a-small-budget Cloud Optimized GeoTIFFs, TiTiler, and knowing when not to cut a tile. What I learned making per-pixel maps fast on a one-vCPU API, with numbers. A lot of my work has involved one shape of problem: a model produces a value for every 10 m pixel over thousands of small polygons, and people want to see it on a map: in a browser, on a phone, sometimes offline in a field with no signal. The API that serves it runs on modest hardware. Here's what made that work, roughly in the order I learned it. #### Store values, not pictures The first version rendered a PNG per polygon per date, each one coloured against its own value range. A single season produced about 28,000 images, and the colours lied: the same green meant different things on neighbouring polygons, because each image had been normalised on its own. We replaced them with one **Cloud Optimized GeoTIFF** (COG) per layer and period, holding the raw values in physical units, plus a small manifest describing each file. [TiTiler](https://developmentseed.org/titiler/) serves tiles straight from those files, and the colour map is applied at request time against a single legend range for the whole project. The same value is now the same colour everywhere, and changing the legend no longer means regenerating anything. #### Two kinds of raster, two sets of rules Not every layer is a measurement. It helps to sort them into two families early, and let the family decide everything else: | | continuous | categorical | |---|---|---| | example | a quantity per pixel | a class per pixel | | data type | `float32`, physical units | `uint8` + colour map | | nodata | `NaN` | a reserved value (`255`) | | overview resampling | `average` | **`nearest`**, always | | legend | one range for the whole project, precomputed | fixed classes | The rule people get wrong is the last row for categories: averaging class 1 and class 3 gives you class 2, which might be something completely different. Categorical rasters use `nearest` everywhere: in the warp, in the overviews, and in the client. Also, **compute the legend range when the data is written**, not when it's read. A list endpoint that worked out each layer's extent on every request took 31.5 s on a 10,000-polygon test project. Precomputing the ranges at ingestion, and replacing a query per polygon with one spatial join, took it to 0.23 s. #### Range requests are great, until you look at the tail COGs are designed so a client can read just the bytes it needs over HTTP. GDAL does this through `/vsicurl/`, so a tile or a point lookup fetches a few small ranges, not the whole file. On a local disk, clipping one polygon's pixels measured 141 ms at p50 and 208 ms at p90. Reading the same COG from object storage told a different story: | COG location | p50 | p90 | first call (cold) | |---|---|---|---| | worker's local disk | 141 ms | 208 ms | n/a | | object storage, warm process | 259 ms | **909 ms** | **2,613 ms** | The median looked fine in a demo. The p90 blew a 500 ms budget, and the cold first call was worse. The fix was boring: keep a copy of the project's COG on the worker as a **disposable cache** (46 MB for a large project), derived and safe to delete, and go back to local-disk latency. Contention showed up too. The polygon clipper alone took 649–987 ms on a heavy view, and about 2 s while TiTiler was reading the same file for map tiles. Batching requests over a 250 ms window removed most of it. #### Pick the serving path by who's asking There is no single best way to serve pixels. The design we landed on uses three, chosen by client and zoom: - **One polygon on a phone:** clip directly from the COG. 17 ms, nothing stored, nothing to invalidate when the model is retrained. - **Close zoom on the web:** pixels as vectors from PostGIS through a vector tile server. 5–9 ms, filterable, and no reads from object storage, so no contention with the raster tiles. - **Wide zoom:** raster tiles from TiTiler. #### Know when not to cut a tile The phone app downloads a project's map for offline use. It stops at zoom 15, but every z15 tile was cut live from the COG, about 4.5 s each. An offline download asks for hundreds at once, the one-vCPU API saturated, and the app got 504s. The fix: **cut once, then redirect.** When a run finishes, a background worker cuts every z15 tile and stores it in object storage. The tile endpoint keeps the same URL, but for an exported project it now answers with a redirect: ```http HTTP/1.1 302 Found Location: https://storage.example.com/…/15/15095/15194.mvt Cache-Control: public, max-age=300 ``` and the stored file is immutable: ```http HTTP/2 200 content-type: application/vnd.mapbox-vector-tile content-encoding: gzip cache-control: public, max-age=31536000, immutable ``` A few details made it work: - **Store one zoom level.** Map clients overzoom: with `maxzoom: 15` set on the *source* (not the layer), z16–z22 are drawn from the z15 tile on the device, and a z15 tile is precise to about 0.3 m. Storing higher zooms would cost the same cut per level and approach one file per pixel. - **Don't store empty tiles.** A missing object returns 404, which the map draws as "nothing here", exactly like the empty 200 the live route sent. - **Byte-for-byte equal.** The stored tile is the same bytes the live route returns, checked on a hundred real tiles, so clients can't tell the difference. - **Retire on change.** A new run retires the old tiles the moment its numbers are stored. Until the new export finishes, the endpoint falls back to cutting live, so a stale picture is never served. On the largest project the slowest z15 request went from 10.5 s to 1.2 s, and the API stopped being the thing that falls over. #### Refuse what you can't serve The last lesson is about limits. Some requests shouldn't be answered with something approximate: - A grid of 10 m pixels over a whole country is about two billion pixels. The service says no instead of silently downsampling and returning a picture that looks right but isn't. - When an upstream source rate-limits us, a circuit breaker trips and the response says *throttled*, not *no data*. - Expensive backfills sit behind a separate key. Every one of these is a case where returning *something* would have looked better and been worse. #### In short 1. Store values, not pictures, and colour them at the edge. 2. Let the type of raster (continuous or categorical) decide its encoding and resampling. 3. Precompute anything a request would otherwise scan for. 4. Measure p90 and cold starts on the real storage path, not just p50 on a laptop. 5. Precompute the hot path once, then redirect to immutable files. 6. Say no clearly when a request is beyond what you can serve honestly. ### Anime, novels and addons on one TV remote 2026-10-20 · https://zaleo.dev/blog/anime-novels-and-addons-on-one-remote How I stitched anime, web novels, music and community addons into one Android TV app, and what integrating sources that break every week taught me about identity, failure and honest UI. I watch a lot of anime and read a lot of web novels, and for years that meant a different app for each, none of them made for a TV remote. So I forked an Android TV media app and spent my evenings turning it into the one place I go for all of it: films and series, anime, web novels, music, audiobooks and live TV, all driven by a D-pad. The features were the fun part. What made them hard was everything behind them: content comes from sources I don't control, and those sources change, rate-limit, or disappear without notice. Most of the interesting engineering turned out to be about three things: **identity**, **failure**, and **being honest in the UI**. #### Stop shipping the scrapers The original app had fifteen site scrapers compiled into the APK. Every time a site rotated its domain or changed a page, that source died until the next release. Users saw it as "most sources don't work", and they were right. The fix was structural. The app now speaks the community **addon protocol** (a manifest, catalogues, metadata and stream endpoints), where the source logic lives on a server that someone maintains and updates without touching my app. Built-in sources still exist, but where streams come from is now a setting with plain words on it: built-in sources, addons, or one before the other. The settings screen says what you trade, for example that addons without a debrid key mean peer-to-peer on a box that may struggle with it. #### Identity is the real integration problem Addons namespace their ids. A catalogue might call a show `somecatalog:1234`, while the most reliable stream addons only understand IMDb ids. The protocol's prefix filtering meant that anything opened from such a catalogue skipped the very addons most likely to work. That's not a permissions problem, it's an identity problem. So the app now resolves the IMDb id (from the addon's own metadata, or failing that a title lookup) and also asks the addons that couldn't understand the namespaced id. The original addon is still asked first and its streams stay on top. Anime got the same treatment. Matching shows by title across sites is a swamp of romanisations and season naming, so every anime source is keyed on its **AniList id** instead. No title matching, no guessing which "Season 2" is which. Addon-provided anime uses the same id with a prefix, so switching between built-in and addon sources doesn't change what you're looking at. #### Ranking what comes back Ask several addons for a film and you get dozens of rows. Sorting by popularity alone put a 263-film collection at the top of a single film, because it had the most peers. The ranking now: 1. drops things a TV can't play directly (disc images, archives); 2. ranks by how awkward startup will be: a direct link first, then a single-file torrent, then a film buried inside a pack; 3. then sorts by peers and language coverage, and caps the list. The detail that mattered: for a **film**, a file index inside a torrent means it's buried in a collection, so it gets pushed down. For a **series** that's normal (episodes live in season packs), so it must not be. #### Web novels: measure before you build When the comics section's only provider vanished from DNS, I replaced it with web novels. Instead of guessing which sites to support, I took the 147 English sources from a well-maintained open-source reader's plugin list and probed every one: - 38 answered; - 37 sat behind a JavaScript challenge a plain HTTP client can't pass; - 22 had no DNS at all. That's why sources are plural. When one host starts challenging, another still has to answer. Three ship today, each verified end to end (search → novel → chapter list → chapter text). A source in the code is an *engine for a family of sites* sharing the same CMS, so adding a sibling site later is configuration, not code. The library tracks new chapters itself: each saved novel remembers the chapter count from when you last opened it, and the badge is the difference. The obvious release tracker sits behind a challenge too, so counting ourselves was both simpler and the only thing that worked. Refreshes are batched four at a time, so a forty-novel library doesn't fire forty requests at three small sites. A session cache keeps going back to the chapter list instant. Without it, one 3,000-chapter series cost 32 requests every time. #### Music that never dead-ends The music section is radio-first: one button starts a mix seeded from artists I like (Japanese R&B and rap, plus anime songs), metadata from a music catalogue API, audio found and streamed separately. When the queue runs out, it refills from the same seeds and then from related artists, so autoplay never stops at the end of a short radio. #### A remote is not a keyboard The TV shaped everything. A keyboard that pops up whenever focus lands on a text field is maddening with a D-pad, so the keyboard now opens only when you press OK on the field. Dialogs let direction keys move between their own buttons but keep focus from escaping. Intro and recap segments come from two community databases, so you can skip them with one press. And when a stream fails, the player says which source failed and moves to the next one, instead of spinning forever. #### What I took away - With sources you don't control, design for **failure first**: several sources, clear fallbacks, nothing compiled in that will rot. - Most "integration" bugs are **identity** bugs. Pick a stable id (IMDb, AniList) and translate at the edges. - **Measure the landscape** before choosing what to support. Probing 147 sites took an afternoon and saved weeks of building on ones that would never work. - Say the trade-offs **in the UI**, in plain words. Users can make good choices when you tell them what they're choosing.