# Fariz Wibisono Full-Stack Software Engineer in Cirebon, Indonesia. Full-stack software engineer working with TypeScript, React, Next.js, Node.js and PostgreSQL. I build and modernize production web platforms: interfaces, APIs, data pipelines, and the tests and infrastructure around them. ## Experience ### Full-Stack Software Engineer, Enclavium 2023 – Present - Led the brownfield modernization of Invictus from an MVP built around Robinhood Chain (Telegram bot and Express-served web app) into a multi-chain, multi-protocol LP terminal on seven chains, serving 1,000+ concurrent users and $4M in transaction volume. - Re-architected it as a TypeScript monorepo (pnpm, Turborepo): Next.js web, Fastify API with oRPC and WebSockets, per-chain indexers, and BullMQ workers on PostgreSQL with TimescaleDB and Redis. - Integrated 31 protocol deployments across 15 variants (Uniswap, PancakeSwap, Aerodrome, Meteora) on EVM chains and Solana, normalized server-side into one position model. - Replaced the MVP's custodial hot wallets with Privy embedded and self-custody wallets so no private keys are stored, and chose TimescaleDB after measuring 30-day event storage at about 22 GB compressed versus 80 GB in plain Postgres. Stack: TypeScript, Next.js, Node.js, Fastify, PostgreSQL, TimescaleDB, Redis, BullMQ, Docker, MCP ### Software Engineer Intern, PT SCADA PRIMA CIPTA Feb 2026 – Jun 2026 · Bandung, on-site - Re-implemented core modules of PROMIA, a risk-based inspection (RBI) desktop application, as a Next.js web app on SQL Server, working from its screens, workflows and data structures rather than its source code; covered flows with Playwright E2E and Vitest unit tests. - Built scada.co.id as a static Astro site on Sanity CMS (GROQ, six cross-referenced content types), deployed with Docker on a VPS, so non-technical staff publish without code changes. - Automated PDF-to-article drafting with an n8n and LLM (OpenRouter) pipeline into Sanity, with field validation, run logging and human review before publishing. Stack: Next.js, TypeScript, SQL Server, Playwright, Vitest, Astro, Sanity CMS, n8n, OpenRouter, Docker ### Software Engineer (Volunteer), KEMAKOM UPI 2024 – 2025 - Built the Dinamik 19 event website (competition pages, technical guidelines) and the P2M community tourism website. Stack: PHP, Laravel, MySQL, JavaScript, Tailwind CSS ### Full-Stack Software Engineer (Freelance), Freelance 2021 – 2023 - Delivered websites and web applications for clients and communities end to end: requirements, REST APIs (Laravel, Express.js), React and Next.js frontends, MySQL and MongoDB, deployment and handover. Stack: JavaScript, TypeScript, PHP, Laravel, React, Next.js, Node.js, Express.js, MySQL, MongoDB, Tailwind CSS, Git ## Selected work - [Invictus](https://frzxw.me/projects/invictus): Multi-chain DeFi LP terminal. 1,000+ concurrent users and $4M in transaction volume across seven chains. - [PDF to blog pipeline](https://frzxw.me/projects/content-pipeline): AI-assisted workflow that turns PDF documents into blog drafts. Editors start each post from a structured draft instead of a blank page. - [Sakola](https://frzxw.me/projects/sakola): Modular multi-tenant school platform. Student savings module built on an append-only ledger; more modules in progress. - [Rasain](https://frzxw.me/projects/rasain): Recipe and cooking app with a step-by-step cooking mode. A Flutter app on Supabase with recipes, cooking timers, reviews, notifications and pantry management. ## Websites and landing pages - [PT SCADA PRIMA CIPTA](https://scada.co.id/): Company website - [Invictus](https://www.lpinvictus.com/): Product landing page - [Helmet](https://hermes-helmet-landing.vercel.app/): Landing page - [Laksana](https://www.laksana.dev/): Studio website - [Runtastic](https://runtastic-landing.vercel.app/): Landing page ## Skills - Languages: TypeScript, JavaScript, PHP, Python, Rust, Dart - Frontend: React, Next.js, Astro, Tailwind CSS, Radix UI, Flutter - Backend: Node.js, Fastify, Express.js, NestJS, Laravel - Data and queues: PostgreSQL, TimescaleDB, MySQL, MongoDB, Redis, BullMQ, Supabase - Testing and automation: Vitest, Playwright, n8n - DevOps and tooling: Docker, GitHub Actions, Cloudflare, Vercel, Git, GitHub, Biome, Turborepo, Postman - AI tools: MCP, OpenRouter, Claude Code, Cursor - CMS: Sanity CMS ## Education - Universitas Pendidikan Indonesia, Computer Science, undergraduate (2023 – Present) ## Writing - [A portfolio that agents can read](https://frzxw.me/blog/a-portfolio-for-agents) (2026-11-12): More people now meet my work through an AI assistant than through my home page. Here is what this site serves to agents, and why each piece is small. - [A shared brain for me and my AI tools](https://frzxw.me/blog/a-shared-brain-with-obsidian) (2026-11-12): My notes live in an Obsidian vault of plain Markdown files that both I and my AI assistants can read and update. How it is organised, and the rules that keep it useful instead of messy. - [How I use AI as leverage, not autopilot](https://frzxw.me/blog/ai-as-leverage) (2026-11-12): AI writes a lot of my first drafts now, in code and in content. This is where I draw the line between what the tool does and what stays my job, and the writing that shaped how I think about it. - [Context engineering: give the model the right information, not more of it](https://frzxw.me/blog/context-engineering) (2026-11-12): Better prompts only go so far. Most of the quality I get from AI tools comes from what they can see when they start: short rules, the right files, and nothing that distracts them. - [Designing for what you can't know](https://frzxw.me/blog/designing-for-what-you-cant-know) (2026-11-12): Good software is honest about uncertainty. A small example from a crypto terminal: when the data stops, we say so on screen instead of pretending everything is live. - [Harness engineering: the checks around the model matter more than the model](https://frzxw.me/blog/harness-engineering) (2026-11-12): If AI writes more of the code, the engineer's job shifts toward building the guardrails it works inside: tests, types, linters, review and safe limits. Here is the harness I use and why. - [Store the history, not just the number](https://frzxw.me/blog/ledgers-not-balances) (2026-11-12): Two very different products, a crypto terminal and a school savings app, ended up with the same rule for money: never edit a number, only add a record. Why that matters, and what it costs. - [Measure before you choose: why I start with boring technology](https://frzxw.me/blog/measure-before-you-choose) (2026-11-12): Data-heavy products attract exciting infrastructure. I usually start with the simplest setup that works, and I make that choice with numbers, so it is easy to explain and easy to revisit. - [Rebuild or refactor? How I decide](https://frzxw.me/blog/rebuild-or-refactor) (2026-11-12): The usual advice is never to rewrite a working product. Sometimes it is still the right call. These are the four questions I ask before choosing, and how they played out on a real product. - [Why strict languages and AI agents get along](https://frzxw.me/blog/rust-strictness-and-agents) (2026-11-12): AI tools fix mistakes fastest when the mistake is pointed out precisely. That is why strict languages like Rust, and strict TypeScript, make agent-written code easier to trust and debug. ## Contact - Email: frzwibisono@gmail.com - GitHub: https://github.com/frzxw - LinkedIn: https://www.linkedin.com/in/frzxw/ - X: https://x.com/frzwibisono - Website: https://frzxw.me - Resume (PDF): https://frzxw.me/cv.pdf --- # Work ## Products - [Invictus](https://frzxw.me/projects/invictus): Multi-chain DeFi LP terminal. 1,000+ concurrent users and $4M in transaction volume across seven chains. - [PDF to blog pipeline](https://frzxw.me/projects/content-pipeline): AI-assisted workflow that turns PDF documents into blog drafts. Editors start each post from a structured draft instead of a blank page. - [Sakola](https://frzxw.me/projects/sakola): Modular multi-tenant school platform. Student savings module built on an append-only ledger; more modules in progress. - [Rasain](https://frzxw.me/projects/rasain): Recipe and cooking app with a step-by-step cooking mode. A Flutter app on Supabase with recipes, cooking timers, reviews, notifications and pantry management. ## Websites and landing pages - [PT SCADA PRIMA CIPTA](https://scada.co.id/): Company website - [Invictus](https://www.lpinvictus.com/): Product landing page - [Helmet](https://hermes-helmet-landing.vercel.app/): Landing page - [Laksana](https://www.laksana.dev/): Studio website - [Runtastic](https://runtastic-landing.vercel.app/): Landing page --- # Blog - [A portfolio that agents can read](https://frzxw.me/blog/a-portfolio-for-agents) (2026-11-12): More people now meet my work through an AI assistant than through my home page. Here is what this site serves to agents, and why each piece is small. - [A shared brain for me and my AI tools](https://frzxw.me/blog/a-shared-brain-with-obsidian) (2026-11-12): My notes live in an Obsidian vault of plain Markdown files that both I and my AI assistants can read and update. How it is organised, and the rules that keep it useful instead of messy. - [How I use AI as leverage, not autopilot](https://frzxw.me/blog/ai-as-leverage) (2026-11-12): AI writes a lot of my first drafts now, in code and in content. This is where I draw the line between what the tool does and what stays my job, and the writing that shaped how I think about it. - [Context engineering: give the model the right information, not more of it](https://frzxw.me/blog/context-engineering) (2026-11-12): Better prompts only go so far. Most of the quality I get from AI tools comes from what they can see when they start: short rules, the right files, and nothing that distracts them. - [Designing for what you can't know](https://frzxw.me/blog/designing-for-what-you-cant-know) (2026-11-12): Good software is honest about uncertainty. A small example from a crypto terminal: when the data stops, we say so on screen instead of pretending everything is live. - [Harness engineering: the checks around the model matter more than the model](https://frzxw.me/blog/harness-engineering) (2026-11-12): If AI writes more of the code, the engineer's job shifts toward building the guardrails it works inside: tests, types, linters, review and safe limits. Here is the harness I use and why. - [Store the history, not just the number](https://frzxw.me/blog/ledgers-not-balances) (2026-11-12): Two very different products, a crypto terminal and a school savings app, ended up with the same rule for money: never edit a number, only add a record. Why that matters, and what it costs. - [Measure before you choose: why I start with boring technology](https://frzxw.me/blog/measure-before-you-choose) (2026-11-12): Data-heavy products attract exciting infrastructure. I usually start with the simplest setup that works, and I make that choice with numbers, so it is easy to explain and easy to revisit. - [Rebuild or refactor? How I decide](https://frzxw.me/blog/rebuild-or-refactor) (2026-11-12): The usual advice is never to rewrite a working product. Sometimes it is still the right call. These are the four questions I ask before choosing, and how they played out on a real product. - [Why strict languages and AI agents get along](https://frzxw.me/blog/rust-strictness-and-agents) (2026-11-12): AI tools fix mistakes fastest when the mistake is pointed out precisely. That is why strict languages like Rust, and strict TypeScript, make agent-written code easier to trust and debug. --- # About Fariz Wibisono I’m a full-stack software engineer based in Cirebon, Indonesia. I work across web and mobile applications: React and Next.js frontends, Node.js services, REST and WebSocket APIs, and PostgreSQL data models. What holds my interest is software architecture, and systems that are still maintainable after the first release. Most of what I know, I learned by shipping real products. I study Computer Science at Universitas Pendidikan Indonesia. ## Experience ### Full-Stack Software Engineer, Enclavium 2023 – Present - Led the brownfield modernization of Invictus from an MVP built around Robinhood Chain (Telegram bot and Express-served web app) into a multi-chain, multi-protocol LP terminal on seven chains, serving 1,000+ concurrent users and $4M in transaction volume. - Re-architected it as a TypeScript monorepo (pnpm, Turborepo): Next.js web, Fastify API with oRPC and WebSockets, per-chain indexers, and BullMQ workers on PostgreSQL with TimescaleDB and Redis. - Integrated 31 protocol deployments across 15 variants (Uniswap, PancakeSwap, Aerodrome, Meteora) on EVM chains and Solana, normalized server-side into one position model. - Replaced the MVP's custodial hot wallets with Privy embedded and self-custody wallets so no private keys are stored, and chose TimescaleDB after measuring 30-day event storage at about 22 GB compressed versus 80 GB in plain Postgres. Stack: TypeScript, Next.js, Node.js, Fastify, PostgreSQL, TimescaleDB, Redis, BullMQ, Docker, MCP ### Software Engineer Intern, PT SCADA PRIMA CIPTA Feb 2026 – Jun 2026 · Bandung, on-site - Re-implemented core modules of PROMIA, a risk-based inspection (RBI) desktop application, as a Next.js web app on SQL Server, working from its screens, workflows and data structures rather than its source code; covered flows with Playwright E2E and Vitest unit tests. - Built scada.co.id as a static Astro site on Sanity CMS (GROQ, six cross-referenced content types), deployed with Docker on a VPS, so non-technical staff publish without code changes. - Automated PDF-to-article drafting with an n8n and LLM (OpenRouter) pipeline into Sanity, with field validation, run logging and human review before publishing. Stack: Next.js, TypeScript, SQL Server, Playwright, Vitest, Astro, Sanity CMS, n8n, OpenRouter, Docker ### Software Engineer (Volunteer), KEMAKOM UPI 2024 – 2025 - Built the Dinamik 19 event website (competition pages, technical guidelines) and the P2M community tourism website. Stack: PHP, Laravel, MySQL, JavaScript, Tailwind CSS ### Full-Stack Software Engineer (Freelance), Freelance 2021 – 2023 - Delivered websites and web applications for clients and communities end to end: requirements, REST APIs (Laravel, Express.js), React and Next.js frontends, MySQL and MongoDB, deployment and handover. Stack: JavaScript, TypeScript, PHP, Laravel, React, Next.js, Node.js, Express.js, MySQL, MongoDB, Tailwind CSS, Git ## Education - Universitas Pendidikan Indonesia, Computer Science, undergraduate (2023 – Present) ## Skills - Languages: TypeScript, JavaScript, PHP, Python, Rust, Dart - Frontend: React, Next.js, Astro, Tailwind CSS, Radix UI, Flutter - Backend: Node.js, Fastify, Express.js, NestJS, Laravel - Data and queues: PostgreSQL, TimescaleDB, MySQL, MongoDB, Redis, BullMQ, Supabase - Testing and automation: Vitest, Playwright, n8n - DevOps and tooling: Docker, GitHub Actions, Cloudflare, Vercel, Git, GitHub, Biome, Turborepo, Postman - AI tools: MCP, OpenRouter, Claude Code, Cursor - CMS: Sanity CMS ## Contact - Email: frzwibisono@gmail.com - GitHub: https://github.com/frzxw - LinkedIn: https://www.linkedin.com/in/frzxw/ - X: https://x.com/frzwibisono - Website: https://frzxw.me - Resume (PDF): https://frzxw.me/cv.pdf --- # Contact Email is the best way to reach me, for work, collaboration, or a question about something on this site. ## Email Write to [frzwibisono@gmail.com](mailto:frzwibisono@gmail.com). A short note on what you are working on, what you need, and any timeline helps me reply with something useful instead of more questions. ## Elsewhere - GitHub: [github.com/frzxw](https://github.com/frzxw), where my public code lives. - LinkedIn: [linkedin.com/in/frzxw](https://www.linkedin.com/in/frzxw/), for my work history. - X: [x.com/frzwibisono](https://x.com/frzwibisono). ## Location I am based in Cirebon, West Java, Indonesia (UTC+7), and I study Computer Science at Universitas Pendidikan Indonesia. I have worked on-site in Bandung and remotely. ## What I can help with - Web applications end to end: interfaces, APIs, data models, and the tests around them. - Websites and landing pages, including a CMS so non-technical people can update them. - Making a site readable by AI agents: Markdown versions of pages, llms.txt, an MCP server, and the discovery files that point to them. > **For agents.** Everything on this page is also available as structured data at [/api/v1/portfolio](/api/v1/portfolio), and the whole site is described in [/llms.txt](/llms.txt). --- # Developers Everything on this portfolio is available to programs as well as people. All of it is public, read-only, and needs no API key. ## Quick start ```bash curl https://frzxw.me/api/v1/portfolio ``` The whole portfolio as JSON: profile, experience, education, certifications, skills, projects, websites and posts. ## REST API - `GET /api/v1/portfolio`: the portfolio as JSON. Described by the [OpenAPI document](/openapi.json). - `GET /api/portfolio`: the same response, kept for older clients. It links to its successor in a `Link` header. - `GET /openapi.json`: OpenAPI 3.1 description of every endpoint, including the error model. - `GET /.well-known/api-catalog`: an RFC 9727 API catalog that lists the API and the MCP server. Responses are cached at the edge and send `Access-Control-Allow-Origin: *`, so they work from a browser. ## Errors Errors use RFC 9457 problem details with the `application/problem+json` media type. Every error has a stable `code` and a `resolution` hint. An unknown path under /api returns 404 in this format, never an HTML page. ```json { "type": "https://frzxw.me/developers#errors", "title": "Not found", "status": 404, "code": "not_found", "detail": "No API endpoint at /api/v1/unknown.", "resolution": "See https://frzxw.me/openapi.json for every endpoint." } ``` ## Versioning The API is versioned in the path: /api/v1. A breaking change ships as a new major version (/api/v2) alongside the old one. Before a version is removed, its responses announce it with `Deprecation` and `Sunset` headers (RFC 9745 and RFC 8594) and a link to the successor. Adding fields is not a breaking change, so clients should ignore fields they do not know. ## MCP server A read-only Model Context Protocol server runs at [/mcp](/mcp) over Streamable HTTP. It is stateless and needs no authentication. - `get_profile`: who I am, contact links, education and skills. - `list_experience`: roles, dates, what I did, and the stack. - `list_projects`: products and websites, with ids for get_page. - `list_posts`: blog posts. - `get_page`: any page of the site as Markdown. Its server card is at [/.well-known/mcp/server-card.json](/.well-known/mcp/server-card.json). ## Markdown for agents Every page has a Markdown version. Add `.md` to the path (`/about.md`, `/index.md` for the home page), or request any page with `Accept: text/markdown`. The [llms.txt](/llms.txt) file indexes them all, and [llms-full.txt](/llms-full.txt) has the full text in one file. ## Discovery - `Link` headers on every page point to the Markdown version, sitemap, llms.txt, API catalog, OpenAPI document, MCP server card and Agent Skills index. - [Agent Skills index](/.well-known/agent-skills/index.json): a skill that explains how to read this site. - [ARD manifest](/.well-known/ard.json): Agentic Resource Discovery entries for the MCP server, API and skill. - WebMCP: in browsers that support it, the page registers `read_page` and `get_portfolio` tools. ## Source The site is open source at [github.com/frzxw/portfolio](https://github.com/frzxw/portfolio). Next.js, TypeScript, and content in plain JSON and Markdown files edited with Decap CMS. --- # Privacy This is a personal portfolio. It counts visits without cookies, does not track you across sites, and does not ask you for anything. ## What this site stores in your browser Two preferences, in your browser's local storage, so the site remembers them on your next visit. They never leave your device. - `theme`: light or dark, if you chose one. - `sound`: whether interface sounds are on. You can clear them at any time by clearing this site's data in your browser. ## Cookies This site sets no cookies of its own for visitors. Cloudflare, which protects the site, may set a strictly necessary security cookie when it needs to tell people from bots. The only cookie the site itself can set is a short-lived security cookie during sign-in to the content manager at /admin, which only the site owner uses. ## Visit statistics The site uses Vercel Web Analytics and Vercel Speed Insights to count page views and measure how fast pages load (Core Web Vitals). Both work without cookies and without identifying you: visits are counted from an anonymous, daily-rotating value, and no data is shared with advertisers. ## Third parties - **Hosting.** The site is served by Vercel through Cloudflare. Like any web server, they process your IP address and request details to deliver pages and protect the site from abuse. - **Fonts.** Fonts are self-hosted with the site, so loading a page does not contact Google. - **Spotify.** The "On repeat" card is a plain link until you press its play button. Only then does Spotify's player load, and Spotify's own privacy policy and cookies apply to it. - **Links.** Links to GitHub, LinkedIn, X and project websites take you to those sites, under their own policies. ## Email If you email me, I use your address and message only to reply. I do not add it to a mailing list or share it. ## AI agents and crawlers Search engines and AI assistants are welcome to read and cite this site. The robots.txt file states a preference against using the content to train models, using Content Signals (search=yes, ai-input=yes, ai-train=no). ## Questions Write to [frzwibisono@gmail.com](mailto:frzwibisono@gmail.com) with any question about this page. --- # Invictus Multi-chain DeFi LP terminal. People who lend their crypto to trading pools often have money in many places at once. Invictus is the terminal that puts all of it on one screen, live. I led the rebuild that took it from an MVP on Robinhood Chain to seven chains. - Role: Lead engineer: architecture and full-stack build - Platform: Web - Status: Live - Result: 1,000+ concurrent users and $4M in transaction volume across seven chains. - Stack: TypeScript, Next.js, Node.js, Fastify, PostgreSQL, TimescaleDB, Redis, BullMQ, Docker, MCP - lpinvictus.com: https://www.lpinvictus.com/ - Documentation: https://docs.lpinvictus.com ## The short version Invictus is a terminal for liquidity providers: people who deposit crypto into trading pools and earn a share of the fees when others trade. It shows every position they hold across seven blockchains in one place, updated as the markets move. It serves 1,000+ concurrent users and has recorded $4M in transaction volume. I lead its engineering: the architecture, the web app, the backend, and the connections to each blockchain. ## The problem, in plain words Imagine keeping savings at seven banks, where every bank prints its statements in a different format and updates them at a different time. Adding up what you own, and whether each account is still earning, becomes a job in itself. That is the daily life of a liquidity provider. Their positions sit on Solana, Ethereum, Base, Robinhood, BSC, Arc and HyperEVM, and each trading protocol describes a position its own way. Uniswap describes it as money inside a price range; Meteora splits it into small price buckets. So the hard part is not drawing a table. It is deciding what one row in that table means when the rows come from different systems. ## Where it started The first Invictus was an MVP built around Robinhood Chain (Arc came later) and one protocol family, Uniswap v3 and v4. It had a web workstation, a Telegram bot, custodial hot wallets and its own price ingestion, all in one Node package of about 54,000 lines with the web app nested inside it. It did its job: it proved the product. But the next step was seven chains including Solana, pools of three different shapes, and features that need data at very different depths and speeds. The MVP's structure was not designed for any of that. ## Rebuild, not refactor Refactoring a working product is usually the safer bet. Here it was not: the MVP's structure was built around a few chains and one protocol family, and widening it meant touching most of it. So I rebuilt it as a new workspace beside the MVP, with no code carried over, on top of recorded research. Two rules kept the rebuild honest. Decisions were made from measurements before features were built, and one feature was built all the way through, from chain to screen, before the rest. ## Decisions and trade-offs A few of the choices that shaped the system, and what each one cost: - Users' keys never live with us. The MVP held custodial hot wallets; the rebuild uses Privy embedded wallets or the user's own wallet, so a breach of our servers exposes no key. The cost is a dependency on Privy, and key export is the user's way out. - One server and one image, split only on measured need. Every process (API, workers, one indexer per chain) runs from the same image with a different role. That is one artifact to build and roll back; the single point of failure is accepted and covered by off-site backups. - Postgres with TimescaleDB instead of a second database. Measured on real Base traffic, a row of pool events costs 192 bytes in plain Postgres, 53 compressed in TimescaleDB and 35 in ClickHouse. Thirty days on six EVM chains drops from about 80 GB to 22 GB. ClickHouse compresses more, but it is a second system to run, back up and keep consistent. - Refresh by attention. Most cost comes from refreshing data nobody is looking at, so data has temperatures: hot when someone is viewing it, warm when it matters for alerts, cold when it can never change. A chain nobody is using costs nothing. - Halt, don't fail over, when a chain stops. Chains do stop: Base for 33 minutes in August 2025, HyperEVM dozens of times. When a chain's head stops moving, the terminal shows its last price with "chain halted since" and accepts no new operations, instead of switching providers and showing stale data as live. - Nothing is lost between the database and the queue. A change and the events it causes are written in one transaction to an outbox; jobs are idempotent and retried with limits; if the queue itself is lost, it is rebuilt from Postgres. A lost queue costs time, not work. ## How the data moves Seven chains, 31 protocol deployments across 15 variants (Uniswap, PancakeSwap, Aerodrome, Meteora and others), translated on the server into one shape that every client reads. ```mermaid flowchart LR subgraph sources["On-chain sources"] evm["EVM chains
Uniswap V3 / V4"] sol["Solana
Meteora DLMM / DAMM v2"] end subgraph backend["Backend, Node.js"] jobs["Background jobs
BullMQ on Redis"] db[("PostgreSQL")] api["API"] end subgraph clients["Clients"] terminal["Terminal
Next.js"] follow["Follow
copy-LP"] mcp["MCP server"] end evm --> jobs sol --> jobs jobs --> db --> api api --> terminal api --> follow api --> mcp mcp -.-> agents(["AI agents"]) ``` How data moves through Invictus, simplified: on-chain sources, the backend, and the three clients that read from it. ## What you see on screen A terminal is something people scan, so the screen is dense on purpose. The Discover view is one sortable table with price, one-day trend, market cap, age, liquidity, volume, fees and yield side by side, and search opens from the keyboard. The status bar shows the frame rate and how long the last server round trip took. A tool that shows live money should be honest about how fresh its own numbers are. - Discover: where could I put money to work? - Positions: what do I hold right now, and is it still earning? - Portfolio: what is all of it worth together? - Rewards: what have I earned? ## Built on top Because every position is already translated into one shape, new features are cheap. Follow lets users discover and copy the positions of high-performing wallets. An MCP server lets AI assistants read the same data a person sees, instead of scraping the screen. ## For engineers The stack, for readers who want it. - TypeScript on Node 24 in a pnpm and Turborepo monorepo: apps for web, API, indexers and workers, and layered domain packages with one owner per concern. - Fastify with zod, pino and OpenTelemetry; oRPC procedures defined contract-first; one WebSocket per browser tab fed by Redis pub/sub. - Postgres 18 with TimescaleDB through PgBouncer and Kysely; BullMQ on Redis with dead-letter queues and a transactional outbox. - viem reads EVM chains and @solana/kit reads Solana; protocols plug in by math family (reserve, tick and bin pools). - One image built in stages and deployed by digest with GitHub Actions and Docker Compose, behind Cloudflare and Caddy; telemetry in Grafana Cloud and Sentry. - Quality gates: strict TypeScript, Vitest with real database containers, Biome, Knip and import boundaries between packages. ## What I would watch next The single server is a deliberate bet, not an accident. Read replicas, sharding and a second region each have a stated trigger, so the system grows when measurements say so, not before. ## What I took from it - **Measure, then decide.** The database, the server size and the refresh rules all came from measured data, which made each trade-off easy to explain and easy to revisit. - **Know when not to refactor.** When the shape of a system is the problem, a clean rebuild with the old one as a reference can be cheaper than changing it piece by piece. - **Design for failure first.** Chains stop, queues get lost and providers fall behind. Deciding what the system does in each case, before it happens, is most of the backend work. --- # PDF to blog pipeline AI-assisted workflow that turns PDF documents into blog drafts. The company's know-how lived in PDF brochures and technical documents. I built an automated assistant that turns each PDF into a first draft of a web article, so a person only has to review and edit it. - Role: Built during my internship at PT SCADA PRIMA CIPTA - Platform: n8n workflow - Timeline: Feb – Jun 2026 - Status: Internal tool - Result: Editors start each post from a structured draft instead of a blank page. - Stack: n8n, OpenRouter, Sanity CMS, Astro, Google Drive, Docker ## The short version During my internship at PT SCADA PRIMA CIPTA, an industrial automation company, I built two connected things: the company's new website, and an automated workflow that writes first drafts of its articles from the PDF documents the company already had. Drop a PDF into a shared folder, and the workflow leaves a structured draft in the website's content manager, matched to the right products and industries. Nothing goes live until an editor approves it. ## Why it was needed Companies that sell to other companies win customers with detail. A plant manager wants to know how a product works, where it fits, and who has used it before they ever pick up the phone. Most of that detail already existed, in product brochures, proposals and catalogs saved as PDFs. Turning one of them into a web article by hand meant reading it, rewriting it for the web, filling in every field, and linking it to the right industries. Repeated for every document, it was slow, repetitive work. ## How it works, step by step The workflow runs in n8n, a tool for connecting apps and automating steps. Each step does one small job: - A PDF is dropped into a shared Google Drive folder, which starts the workflow. - The text is extracted and the key facts are pulled out into a structured format. - An AI model, reached through OpenRouter, matches the document to the products and industries that already exist on the website. - For each matching industry, the AI writes a draft article or case study in the exact shape the website expects. - The draft is checked so no required field is missing, then saved in Sanity, the content manager, as a draft. - Every run is logged in a spreadsheet, and a file that fails is moved to its own folder so nothing disappears silently. ```mermaid flowchart TD pdf[/"PDF in Google Drive"/] --> text["Extract the text"] text --> rewrite["Rewrite as a blog post
OpenRouter"] rewrite --> match["Match to industries
already on the site"] match --> check["Check every field is filled"] check --> draft[("Draft in Sanity")] draft --> editor(["Editor reviews and publishes"]) ``` The workflow from a PDF in Google Drive to a draft an editor can publish. Nothing goes live without a person. ## A person stays in charge The goal was never to publish automatically. Editors open a draft that already has a title, a structure and the right industries attached, and their job moves from typing to checking: facts, tone, and when it goes live. ## The website it feeds The drafts land in the new company website, which I built at the same time. Non-technical staff manage every page from Sanity Studio: products, solutions, industries, articles, case studies, navigation and SEO details, without touching code. - Astro builds the site as fast, static pages; Tailwind CSS handles the styling. - Six content types reference each other, so a product page can list its related industries and case studies automatically. - The contact form is protected from bots with Cloudflare Turnstile. - The site, the Sanity Studio and supporting services run in Docker on a VPS behind a reverse proxy. - A webhook that rebuilds the static site when content changes was designed; finishing and testing it, along with the contact form's email delivery, were open items when the internship ended. ## Decisions and trade-offs The choices that shaped it, and what each one cost. - Drafts, never auto-publish. AI output about industrial products has to be checked by someone who knows them, so the pipeline stops at a draft and an editor decides what goes live. - Choose from a list, don't generate one. The AI picks products and industries from the ones that already exist on the site, so every generated link points somewhere real. - Several small prompts instead of one big one. Extraction, mapping and writing are separate steps with their own output checks, so a bad draft can be traced to the step that caused it. - Log where people already look. Every run writes a row to a spreadsheet the team already uses, and failed files move to their own folder instead of disappearing. - Static pages over a server. Company content changes rarely and is read often, so building static pages is faster and cheaper to host; the price is a rebuild step after each change. ## For engineers What the screenshots show, in technical terms. - A Google Drive trigger starts the run; the extraction stage converts the PDF to text and the facts to JSON. - Prompt 2 maps the document to existing products and industries (chosen from a list taken from the site, not free text), with output parsing and a failure branch. - Prompt 3 loops over each mapped industry and produces one Sanity-ready document per industry through an LLM chain, with a wait step between calls. - Results are written to Sanity through its HTTP API as drafts; each stage appends a row to a Google Sheets log and moves failed files to a separate folder. ## What I would do next The same points I left in the internship report: stronger validation of the AI drafts before they reach an editor, an editorial standard so articles share one structure and voice, and finishing the rebuild webhook and the contact form's email tests. ## What I took from it - **Small steps beat one big prompt.** Each step does one thing, so when a draft looks wrong it is easy to see which step caused it. - **Choose from what already exists.** Matching documents to the website's existing industries, instead of letting the AI invent new ones, kept every link valid. - **The draft is the product.** The point was to give an editor something worth editing, not to remove the editor. --- # Sakola Modular multi-tenant school platform. Student savings is other people's money, handled every school day. I'm building Sakola so that every rupiah deposited or withdrawn is recorded once, can be explained later, and adds up at closing time. - Role: Design and build - Platform: Web - Timeline: Ongoing - Status: In progress - Result: Student savings module built on an append-only ledger; more modules in progress. - Stack: TypeScript, Next.js, PostgreSQL, Tailwind CSS ## The short version Sakola is a school platform built as one system that many schools can share. Each school keeps its own data separate, and features switch on per school. The first feature I built is student savings: a digital version of the savings book a school keeps for each student. The project is ongoing. ## The problem, in plain words Student savings means real money changing hands every day: deposits collected in class, withdrawals that someone has to approve, and cash in a safe that has to match the records when the day ends. If a balance can simply be typed over, nobody can tell later why it changed. So the design starts from one rule: money is never edited, only recorded. ## How it works for the school The treasurer's screen is organised around the day, not around tables: what came in, what went out, and what still has to be done before the cash is closed. - Deposits by QR code, or collected by the homeroom teacher for a whole class. - Withdrawals need two people: one records the request and another approves it. - At closing time the system compares its balance with the cash counted in the safe, note by note. - Passbooks are generated as PDFs, and parents receive notifications. ## Why a ledger Think of a bank statement you can never erase. Every deposit and every withdrawal is a new line; the balance is simply the sum of the lines. A mistake is fixed by adding a correcting line, never by changing an old one. That one rule makes the rest easy. Balances, passbooks, daily closing and reports are all just different views of the same list. ## Decisions and trade-offs The choices that shaped it, and what each one cost. - A ledger instead of balances. Storing only entries makes every balance explainable and every mistake visible, at the cost of computing balances instead of reading one number. - Two people for every withdrawal. Maker-checker approval slows a withdrawal down a little, which is the point when the money belongs to children. - Many schools in one system. Each school is isolated, but students, users and roles are modelled once and shared by every module, so a new module does not need its own copy of the student list. - Features per school. A school that only needs savings gets only savings; modules switch on per school and connect automatically when combined. - Screens built around the day. A treasurer thinks in handovers and closing time, not in database tables, so the navigation separates today's work from the records. ## For engineers Multi-tenant architecture: each school is an isolated tenant over shared foundation data (students, users, roles) with role-based access, and modules activate per school and integrate when combined. The savings module is an append-only ledger in PostgreSQL; balances are derived, never stored as editable values. The frontend is Next.js, TypeScript and Tailwind CSS. ```mermaid flowchart TB subgraph tenancy["Tenancy"] a["School A"] b["School B"] c["School C"] end shared[("Shared foundation data
students, users, roles")] a --> shared b --> shared c --> shared subgraph savings["Student savings module"] deposit["Deposit
QR code or homeroom"] withdraw["Withdrawal
recorded, then approved"] ledger[("Ledger
append-only")] deposit --> ledger withdraw --> ledger ledger --> balance["Balances"] ledger --> passbook["PDF passbooks"] ledger --> closing["Daily cash closing"] ledger --> notify["Parent notifications"] end shared --> savings ``` Two layers: schools as isolated tenants over shared data, and the savings module, where everything is derived from one ledger. ## Where it is going Savings is the first module, not the whole platform. Separately, my thesis explores a structured exam question bank, extracted and classified by agentic AI. That is an academic direction, not a shipped feature. ## What I took from it - **Model the money first.** Once every movement is an entry, reconciliation, passbooks, and balances are all views of the same table. - **A correction is an entry.** Nothing is edited in place, so the history always explains the balance. - **Design around the day.** A treasurer thinks in handovers and closing time, so the navigation does too. --- # Rasain Recipe and cooking app with a step-by-step cooking mode. A recipe is only useful while you are cooking it. Our team built a mobile app that walks you through each step with its own timer, and knows what is already in your kitchen. - Role: Team project; project setup, Supabase integration, pantry module - Platform: Mobile (Flutter) - Timeline: Apr – Jul 2025 - Status: Team project - Result: A Flutter app on Supabase with recipes, cooking timers, reviews, notifications and pantry management. - Stack: Flutter, Dart, Supabase, PostgreSQL - Source on GitHub: https://github.com/frzxw/rasain-app ## The short version Rasain is a mobile recipe app, built with Flutter and Supabase by a student team. It has recipe pages with ingredients and steps, a full-screen cooking mode with a countdown timer for each step, reviews, user profiles and notifications. ## What I worked on I started the project and its structure, and wrote a pretend data service filled with Indonesian sample recipes, so the team could build and test the screens before the real backend existed. Later I connected the app to Supabase for real data and settings. - The pantry: a list of what you have at home, with search, filters, statistics and quick add. - A shopping list generated from what a recipe needs and the pantry lacks. - Recipe suggestions based on what is already in the pantry. - Duplicate checks and clearer errors when adding pantry items. ## Decisions and trade-offs The choices that shaped it, and what each one cost. - A fake backend on day one. A mock service with realistic Indonesian recipes let the team build and review screens before the real backend was decided, at the cost of swapping it out later. - Organised by feature. Each feature (auth, cooking, chat, admin) owns its screens and logic, so team members could work in parallel without stepping on each other. - Security in the database. Supabase's row-level security decides who can read and change each row, so a mistake in the app cannot expose another user's data. - Timers as data. Each cooking step's timer is stored with the step in the database, so recipes can be corrected without releasing a new app version. ## For engineers The app is organised by feature (auth, recipe detail and cooking, chat, admin) on shared core utilities, with Provider and cubits for state and GoRouter for navigation. Supabase provides PostgreSQL, authentication and row-level security, and cooking timers are stored per instruction step in the database. ## What I took from it - **Mock the API on day one.** A fake service with realistic data let the interface move while the backend was still being decided. - **Design for the kitchen.** Big type, one step per screen, and a timer you can start with one tap matter more than features. - **Duplicates are a data problem.** Checking for an existing pantry item before adding one fixed confusion the interface alone could not. --- # A portfolio that agents can read *2026-11-12 · Fariz Wibisono* More people now meet my work through an AI assistant than through my home page. Here is what this site serves to agents, and why each piece is small. When someone asks an assistant about me, the assistant reads this site on their behalf. If it has to scrape a page full of animation and layout to find one job title, it will get things wrong. So the site speaks to programs as directly as it speaks to people. None of it is complicated. Each piece is a file or a header, generated from the same content as the pages, so it can never drift from what a person sees. ## Text first - Every page has a Markdown version. Add `.md` to any path, or ask for any page with `Accept: text/markdown`. - [llms.txt](/llms.txt) is a short index of every page, and llms-full.txt is the whole site in one file. - Every page's HTML response carries a `Link` header that points to its Markdown version, the sitemap and the files below. ## Structured data - A read-only JSON API at [/api/v1/portfolio](/api/v1/portfolio), described by an OpenAPI document. Errors are RFC 9457 problem details, so an agent gets JSON even when it asks for something that does not exist. - An MCP server at /mcp with five read-only tools, and a server card that describes it. - An Agent Skill that explains how to read the site, and an ARD manifest that lists all of the above in one place. ## Being explicit about consent The robots.txt file welcomes search engines and AI assistants, and uses Content Signals to say yes to search and answers and no to model training. It is a preference, not a lock, but it is written where well-behaved crawlers look. > **The test.** Ask an assistant what I have built and where I studied. If the answer is right and links back here, the site is doing its job. The details are on the [developers page](/developers), and the source is on [GitHub](https://github.com/frzxw/portfolio). --- # A shared brain for me and my AI tools *2026-11-12 · Fariz Wibisono* My notes live in an Obsidian vault of plain Markdown files that both I and my AI assistants can read and update. How it is organised, and the rules that keep it useful instead of messy. Every AI session used to start from zero. I would explain the project again, the decisions again, what I did yesterday again. The assistant was capable; it just had no memory of me. So I gave us a shared one: a notes vault in [Obsidian](https://obsidian.md/), kept in Git, that I read in the app and my AI tools read as plain files. ## Why plain Markdown Obsidian stores everything as ordinary Markdown files in folders. That one choice is what makes the whole thing work. I get links, search and a nice editor; an AI assistant gets text files it can read and edit; Git gives both of us history, so any change can be reviewed or undone. ## How it is organised The vault follows two well-known systems: - **PARA**: Projects (things with an end), Areas (ongoing responsibilities), Resources (reference material) and an Archive. - **Johnny Decimal** numbering, so every folder has a stable address. On top of that there is an inbox for quick capture, a daily log, and maps of content: index notes that link to every project, every decision and every working session. Each project note works like a ticket, with a status that says whether it is up next or in progress. A decision gets its own dated note: what was decided, why, and what it affects. ## The part that makes it work with AI At the root there is one instruction file for assistants. It does not restate the rules; it tells them where the rules live and what to read, in order: 1. The rulebook: folder layout, note templates, what never to do. 2. A small set of memory notes: durable facts and preferences. 3. A note called "next session", where the last session left off. 4. The latest daily log. 5. Only the projects that are currently active. Then it says the most important thing: if what you need is not in those reads, ask instead of guessing. The ground rules are short. Never delete my notes without confirmation. Use the templates. Update the index notes when creating something. Date every session and decision. When in doubt, ask. ## What changed Sessions start where the last one ended, because the assistant reads the "next session" note first and updates it at the end. Decisions stop getting re-argued, because they are written down with their reasons. And the vault became more useful to me, too: writing for a reader that needs clear structure turned out to be good for the human reader as well. > **What I would tell someone starting.** Start with plain files and a reading order, not a clever tool. The structure is what makes notes useful to both you and your AI, and you can change tools later without losing anything. --- # How I use AI as leverage, not autopilot *2026-11-12 · Fariz Wibisono* AI writes a lot of my first drafts now, in code and in content. This is where I draw the line between what the tool does and what stays my job, and the writing that shaped how I think about it. I use AI tools like Claude every working day. They read an unfamiliar codebase faster than I can, draft tests, sketch the first version of a feature, and turn a PDF into a structured draft in seconds. I would be slower without them, and I am not going to pretend otherwise. What I have learned is that speed was never really the job. The job is software that still works, and still makes sense to the next person, months after it ships. So I try to use AI as leverage on my judgement, not as a replacement for it. ## Where the line came from Andrej Karpathy gave the industry the phrase [vibe coding](https://x.com/karpathy/status/1886192184808149383): giving in to the vibes, accepting what the model writes and not really reading the code. He was clear that it suits throwaway weekend projects. In his talk [Software Is Changing (Again)](https://www.youtube.com/watch?v=LCEmiRjPEtQ) he describes the other end: keep the AI on a short leash, work in small steps, and make the loop between the model generating something and a person verifying it as fast as possible. That loop is most of how I work now. Addy Osmani put a name to something I kept running into, the [70% problem](https://addyo.substack.com/p/the-70-problem-hard-truths-about). AI gets you most of the way very quickly, and the last stretch, the edge cases, security and code that someone can maintain, is where engineering knowledge decides the outcome. He also draws a firm line between [vibe coding and AI-assisted engineering](https://addyo.substack.com/p/vibe-coding-is-not-the-same-as-ai), and that line is the one I try to stay on. Kent Beck calls his version [augmented coding](https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes). The difference from vibe coding, in his telling, is simply whether you still care about the code: its tests, its structure, whether it will be easy to change. Simon Willison's [notes on using LLMs for code](https://simonwillison.net/2025/Mar/11/using-llms-for-code/) say the same thing from daily practice: give the model good context, and never hand over the job of checking that the code actually works. And then there is taste. Emil Kowalski writes that [taste can be developed](https://emilkowal.ski/ui/developing-taste), by making things yourself and paying attention to great work. When output becomes cheap, knowing what good looks like is the part that stays scarce. ## What that looks like in my week I write the rules down where the tools will read them. In my larger projects, each part of the codebase has a short file saying what it holds and what it must not do. People and AI read the same rules, so suggestions start from the same constraints instead of from a guess. I keep changes small and I read every line. If I cannot explain a change in one sentence, it does not go in, whoever wrote the first draft. I let automatic checks have the final word on the boring questions. Type checking, tests, linting and a check for unused code run on every change. The tool proposes; the checks and I decide. I draft with AI and decide with people. In an [internship project](/projects/content-pipeline), AI turns technical PDFs into article drafts, but it can only pick products and industries from lists that already exist, and nothing goes live until an editor approves it. And I keep practising the fundamentals: data modelling, system design, debugging. They are what let me notice when a confident suggestion is wrong. If I stopped practising them, I would stop noticing. > **If I had to put it in one line.** Use AI to go faster on what you understand and to learn what you don't. Never use it to skip the understanding. The engineers I want to learn from are not the ones who type the least. They are the ones who know what good looks like, check their work, and spend the time they save on the decisions that matter. ## Further reading - Andrej Karpathy, [Software Is Changing (Again)](https://www.youtube.com/watch?v=LCEmiRjPEtQ), talk at Y Combinator AI Startup School. - Andrej Karpathy, [the post that named "vibe coding"](https://x.com/karpathy/status/1886192184808149383). - Addy Osmani, [The 70% problem: Hard truths about AI-assisted coding](https://addyo.substack.com/p/the-70-problem-hard-truths-about). - Addy Osmani, [Vibe coding is not the same as AI-assisted engineering](https://addyo.substack.com/p/vibe-coding-is-not-the-same-as-ai). - Kent Beck, [Augmented Coding: Beyond the Vibes](https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes). - Simon Willison, [Here's how I use LLMs to help me write code](https://simonwillison.net/2025/Mar/11/using-llms-for-code/). - Birgitta Böckeler and colleagues, [Exploring Generative AI](https://martinfowler.com/articles/exploring-gen-ai.html), on martinfowler.com. - Emil Kowalski, [Developing taste](https://emilkowal.ski/ui/developing-taste). --- # Context engineering: give the model the right information, not more of it *2026-11-12 · Fariz Wibisono* Better prompts only go so far. Most of the quality I get from AI tools comes from what they can see when they start: short rules, the right files, and nothing that distracts them. When people talk about getting better results from AI, they usually mean better prompts. In my experience the prompt is the smallest part. What matters more is everything else the model sees when it starts working: which files, which rules, which past decisions, and, just as important, what it does not see. Anthropic's engineering team calls this [context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents), and describes the goal well: find the smallest set of high-signal information that makes the right outcome likely. More context is not better context. A model given everything tends to latch onto the wrong thing, the same way a new teammate handed the whole wiki on day one would. ## What it looks like in practice **Short rules, close to the code.** In the [Invictus](/projects/invictus) codebase, every part of the system has a short file that says what it holds and what it must not do, plus one file of rules for the whole repository. They bind people and AI tools alike. When an assistant opens the indexer, it reads the indexer's rules, not a 40-page document about everything. **A reading order, not a library.** My personal notes vault tells an assistant exactly what to read first and in what order, and to ask rather than guess when something is missing. I wrote about it in [a shared brain for me and my AI tools](/blog/a-shared-brain-with-obsidian). **Reusable skills instead of repeated explanations.** I keep a small collection of agent skills, each one a folder with a single instruction file: how I write commit messages, how I want Playwright tests structured, how to review prose. Instead of explaining the same thing in every session, the tool loads the skill when the task needs it. **Make your own product readable.** This site serves every page as plain Markdown, an index at [/llms.txt](/llms.txt), and structured data for agents. That is context engineering pointed outward: making it easy for someone else's AI to get you right. ## What I avoid - Pasting entire codebases or documents "just in case". - Long instructions that repeat what the code already makes obvious. - Rules that live only in my head, which the tool can never see. - Letting old, wrong notes pile up. Stale context is worse than missing context, because it sounds confident. > **The habit that helped most.** Whenever I correct an AI tool twice for the same thing, I write the correction down where it will read it next time. The fix belongs in the context, not in my patience. ## Further reading - Anthropic, [Effective context engineering for AI agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). - [AGENTS.md](https://agents.md/), a simple open format for instructions that coding agents read. - Simon Willison, [Here's how I use LLMs to help me write code](https://simonwillison.net/2025/Mar/11/using-llms-for-code/), especially the part on context. --- # Designing for what you can't know *2026-11-12 · Fariz Wibisono* Good software is honest about uncertainty. A small example from a crypto terminal: when the data stops, we say so on screen instead of pretending everything is live. Most systems are built for the happy path and then patched for failures. I have come to prefer the opposite: decide early what the product should do when it cannot know something, because that is often when people need it most. ## The situation [Invictus](/projects/invictus) shows live prices and positions from seven blockchains. Blockchains occasionally stop producing new data, for seconds or sometimes much longer. Before designing anything, we looked at how often this had actually happened on the chains we support. It was not rare. ## The tempting answer The usual instinct when a data source goes quiet is to switch to a backup source and keep the numbers moving. That is right when one provider has a problem. But when the whole chain has paused, every source returns the same old data. Switching would keep the screen looking alive while showing information that is no longer true. ## What we did instead We separated the two cases. If one provider falls behind while others keep moving, the system switches providers. If every independent source agrees nothing has changed for a while, the chain itself has paused, and the product says so: - It keeps showing the last price, clearly labelled "chain halted since", with the time. - It does not accept new transactions on that chain until it resumes. - Anything already in progress waits, and its time limits pause too, so nothing fails just because the chain did. - When data resumes, everything picks up from where it stopped. ## The trade-off we accepted A very quiet chain can look paused for a few seconds even when it is fine. We decided a short, honest label was better than adding complicated special cases to hide it. > **The principle.** When you cannot know something, say so. "Paused since 14:02" is more useful to someone making a decision about their money than a number that looks live and is not. The same thinking applies far beyond crypto: a delivery app that says "location last updated 5 minutes ago" earns more trust than one that shows a confident dot in the wrong place. --- # Harness engineering: the checks around the model matter more than the model *2026-11-12 · Fariz Wibisono* If AI writes more of the code, the engineer's job shifts toward building the guardrails it works inside: tests, types, linters, review and safe limits. Here is the harness I use and why. Early in 2026 OpenAI's Codex team wrote about building a product where agents wrote almost all of the code, and named the work around it [harness engineering](https://openai.com/index/harness-engineering/). Birgitta Böckeler's [first thoughts on the idea](https://martinfowler.com/articles/exploring-gen-ai/harness-engineering-memo.html) put it simply: the harness is the tooling and practice that keeps an agent's work in check. I find the word useful because it describes a real shift. If a model can produce code quickly, the scarce skill is no longer typing it. It is designing the environment that catches mistakes before they reach users. ## Context and harness are two different questions Context engineering asks what the model should see. Harness engineering asks what the system should prevent, detect and correct, whatever the model does. You need both: good context makes good output likely; a good harness makes bad output harmless. ## The harness I actually use On this site and on my larger projects, every change, whoever or whatever wrote it, goes through the same gates: - **Types.** Strict TypeScript, or Rust's compiler, rejects a whole class of mistakes before anything runs. - **Tests.** Unit tests for logic, end-to-end tests with Playwright for the flows people use. On this site, tests also check the content itself: every screenshot exists, every link is safe, every page that must exist does. - **Linting and formatting.** Biome catches suspicious patterns and keeps style out of code review. - **Unused code.** Knip reports files, exports and dependencies nothing uses, which is exactly what fast generated code tends to leave behind. - **Architecture rules.** In larger projects, rules about which part of the code may import which, so a quick fix cannot quietly create a tangle. - **Supply-chain limits.** New package versions must be at least a week old before they install, and install scripts never run. Generated code suggesting a fresh dependency cannot sneak in something malicious. - **Review.** Automated review on pull requests, and my own reading of every change. All of it runs on every push, so a mistake shows up in minutes rather than in production. ## Why it is a career skill, not a chore > **The shift.** When code is cheap, trust is expensive. The engineer who can build the system that makes generated code trustworthy is more valuable than the one who can only write the code. A harness also makes me faster, not slower. I can accept a large suggestion with confidence because I know the checks will catch most of what I might miss, and I can spend my attention on what checks cannot judge: whether the design is right. ## Further reading - OpenAI, [Harness engineering: leveraging Codex in an agent-first world](https://openai.com/index/harness-engineering/). - Birgitta Böckeler, [Harness Engineering, first thoughts](https://martinfowler.com/articles/exploring-gen-ai/harness-engineering-memo.html). - Kent Beck, [Augmented Coding: Beyond the Vibes](https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes), on tests as the guardrail. --- # Store the history, not just the number *2026-11-12 · Fariz Wibisono* Two very different products, a crypto terminal and a school savings app, ended up with the same rule for money: never edit a number, only add a record. Why that matters, and what it costs. The simplest way to store money in software is a single number. A student has 150,000 rupiah, deposits 20,000, and you change the number to 170,000. It is quick to build and easy to read. It is also impossible to check. When the number is wrong, nothing tells you how it got that way. ## The rule In [Sakola](/projects/sakola), a savings platform for schools, money follows one rule: a balance is never typed in or edited. Every deposit and every withdrawal is added as a new line, like a bank statement you can never erase, and the balance is simply the total of the lines. The first version of Invictus handled money the same way. ## What it gives you - **Every balance can be explained.** Show the lines, and the number adds up. - **Mistakes stay visible.** A wrong deposit is fixed by adding a correcting line, so the history shows both the mistake and the fix. - **Many features come for free.** Passbooks, daily cash checks and notifications to parents are all just different ways of reading the same list. ## The process around it matters as much A good record makes changes honest; a good process makes them safe. - **Two people for every withdrawal.** One staff member records the request and another approves it. It is slower on purpose, because the money belongs to children. - **A daily check.** At closing time the treasurer counts the cash in the safe, note by note, and the system compares it with what the records say should be there. ## What it costs Adding up lines takes more work than reading one number, and people expect an edit button. The design has to make "add a correction" feel as natural as "change the number", and explain why. For a school that is a small price for being able to answer any question about any rupiah. > **The rule of thumb.** If a number represents someone else's money, store how it changed, not just what it is. --- # Measure before you choose: why I start with boring technology *2026-11-12 · Fariz Wibisono* Data-heavy products attract exciting infrastructure. I usually start with the simplest setup that works, and I make that choice with numbers, so it is easy to explain and easy to revisit. When a product handles a lot of data, it is tempting to reach for impressive tools from day one: a specialised analytics database, a cluster of servers, many small services. Each of those is a good tool. Each is also one more thing to run, back up, monitor and keep in sync with everything else. On the rebuild of [Invictus](/projects/invictus) I chose the plain option on purpose, and I made each choice with a measurement next to it. ## One database instead of two The heaviest data is a record of every trade on every pool we follow. We tested one real hour of traffic in three setups: an ordinary Postgres database, the same database with a time-series extension (TimescaleDB) that compresses old data, and a specialised analytics database (ClickHouse). The specialised database stored the data most compactly. The compressed Postgres came close: a month of data shrank from about 80 GB to about 22 GB. Since we needed Postgres anyway for users and app data, adding a second database would have meant a second system to operate for a modest saving. So we kept one. ## One server, split only when needed Every part of the system, the website, the API and the background workers, runs from the same package on one server, each with its own role. That gives one thing to build, test and roll back. One server is a single point of failure, so backups go to a different provider, and each next step, like a second server or a database copy, has a clear signal that would trigger it. ## Don't pay to refresh what nobody is looking at The biggest cost in a data product is often not storage. It is refreshing information that nobody is looking at. So data that someone is viewing right now updates constantly, data that matters for alerts updates in the background, and data that can never change is read once and kept. A part of the system nobody is using costs almost nothing. ## Why this matters beyond one project > **Boring is a decision, not a default.** Starting simple is only responsible if you measured what you are giving up and decided in advance what would make you change your mind. It also makes conversations easier. When someone asks why we did not use the exciting option, the answer is a number and a condition, not a matter of taste. --- # Rebuild or refactor? How I decide *2026-11-12 · Fariz Wibisono* The usual advice is never to rewrite a working product. Sometimes it is still the right call. These are the four questions I ask before choosing, and how they played out on a real product. "Never rewrite from scratch" is good advice most of the time. A working product carries years of small fixes that nobody wrote down, and a rewrite quietly throws them away. So when I proposed rebuilding [Invictus](/projects/invictus) instead of improving it step by step, I owed the team a better reason than "the code is messy". ## The situation Invictus started as a minimum viable product: a tool for people who provide liquidity to crypto trading pools, built around Robinhood Chain and one family of trading protocols. It had a web app, a Telegram bot and its own data collection, all in one large package. It did its job. It proved people wanted the product. The next version had to support seven blockchains and many more protocols, each describing the same idea in its own way, with some data needed every few seconds and some read once and kept forever. The first version was never designed for that. ## The four questions Instead of arguing about code quality, I asked four questions. 1. **Is the problem in the details, or in the shape?** Bugs and untidy functions are details, and refactoring fixes them well. A structure built around one chain is the shape, and no amount of tidying changes it. 2. **How much would survive the change?** If widening the structure means touching most of the code anyway, a refactor is just a slow rewrite with the old limits still in the way. 3. **Can users be protected while the new version is built?** A rebuild should never put the people using the product at risk. 4. **Do we know enough to design the new shape?** A rebuild without research only recreates the old problems in new files. For Invictus the answers lined up: the problem was the shape, most code would change, and the unknowns could be researched first. So we rebuilt it, keeping the lessons of the first version but none of its code. ## Two rules that kept it from going wrong Rebuilds fail when they turn into an open-ended second project. Two rules prevented that. - **Measure before building.** Storage, server size and how often data needs refreshing were measured on real data before features were written. - **Build one thing all the way through first.** One feature went from the raw data all the way to the screen before anything else started. It exposed the hard integration problems early, while they were still cheap to fix. > **What I would tell another team.** Refactor when the problem is in the details. Rebuild when the problem is the shape, you can protect users along the way, and you have done the research to know what the new shape should be. --- # Why strict languages and AI agents get along *2026-11-12 · Fariz Wibisono* AI tools fix mistakes fastest when the mistake is pointed out precisely. That is why strict languages like Rust, and strict TypeScript, make agent-written code easier to trust and debug. An AI assistant is only as good as the feedback it gets. When it writes something wrong, the question is how quickly and how precisely the mistake gets pointed out. Strict languages answer that question before the code ever runs. ## A compiler is a very patient reviewer In a loose language, a mistake often shows up much later: a value that was sometimes missing, a function called with the wrong kind of data, a case nobody handled. By then the trail is cold for a person and for an AI. Rust's compiler refuses to build code with a whole class of those mistakes, and its error messages say exactly where the problem is and often how to fix it. For an agent that is the ideal loop: write, compile, read a precise error, fix, repeat. The feedback is fast, specific and never tired. Strict TypeScript gives a lighter version of the same thing, which is why every TypeScript project I run uses the strictest settings. ## What I saw in practice My personal research engine, [Heuristic](https://github.com/frzxw/heuristic), is written in Rust, with a lot of help from AI tools. A few choices made that collaboration work well: - **A pure core.** The domain logic lives in a crate with no input or output at all: no network, no database. Code like that is easy for an agent to reason about and easy to test. - **One command for every check.** A single recipe runs formatting, a spell check, Clippy (Rust's linter), the tests and a dependency audit. An agent can run it and get one clear answer. - **A pinned toolchain.** Everyone, human or AI, builds with the same compiler version, so "it works on my machine" goes away. The result is code I can accept from an assistant with more confidence, because the compiler has already argued with it. ## The honest trade-offs Rust is slower to write and has a learning curve, for people and models. It is not the right choice for every project; most web products I build are TypeScript. But for logic where correctness matters more than speed of typing, data pipelines, money, anything long-running, the strictness pays for itself. > **The takeaway.** Choose tools that give fast, precise feedback. They make you better, and they make your AI tools better for the same reason. ## Further reading - [The Rust Programming Language](https://doc.rust-lang.org/book/), the official book. - Kent Beck, [Augmented Coding: Beyond the Vibes](https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes), on keeping tests as the guardrail.