Anti-FraudX: Local Scam Defense Training Powered by Gemma 4 E4B

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

Anti-FraudX is a local anti-scam training platform designed for Hong Kong users. Instead of only showing static scam warnings, it lets people practise scam detection through interactive AI conversations, RPG-style training, and automated multi-agent simulations.

The project focuses on a real local problem: scams are fast, emotional, and often happen through familiar channels such as WhatsApp messages, phishing SMS, fake banking pages, fake police calls, romance scams, investment scams, and QR code payment traps. Anti-FraudX turns those risks into practice scenarios so users can learn how pressure, urgency, trust, and manipulation appear in real conversations.

The system includes:

  • an RPG-style training mode for scenario-based learning
  • a personal chat mode where users can test their reactions
  • an auto simulation mode with scammer, victim, expert, and recorder agents
  • multimodal scam review for suspicious screenshots or images
  • local model serving through Ollama for privacy-sensitive use

The goal is to make anti-fraud education more practical for schools, families, community centers, and privacy-sensitive training sessions.

Demo

Demo video: https://youtu.be/BLYm_VUpMUI

A good walkthrough should show:

  1. a Hong Kong scam scenario, such as a suspicious message or fake payment request
  2. the user entering the RPG or chat mode
  3. the scammer applying pressure or urgency
  4. the victim and recorder agents responding through Gemma 4 E4B
  5. the expert agent explaining the red flags
  6. the final trust score, risk explanation, and learning takeaway

Live demo: https://anti-fraudx-frontend-5gznvtwxga-uc.a.run.app/

Code

Repository: https://github.com/LamChingFung-2425/Anti-FraudX/tree/andy-v7(gemma-4.0)

Important implementation files:

  • backend/main.py — loads environment variables, applies startup checks, and serves the FastAPI app
  • backend/config.py — centralizes model defaults for each agent role
  • backend/services/vision_service.py — handles multimodal scam image analysis
  • docker-compose.yml — defines local deployment defaults for Ollama and the backend
  • backend/tests/test_environment.py — checks that the required Gemma 4 model is available
  • backend/tests/test_vision.py — verifies the vision flow against Gemma 4

Representative runtime configuration:

GEMINI_ENABLED=false
AGENT_MODEL=gemma4:e4b
AGENT_MODEL_VICTIM=gemma4:e4b
AGENT_MODEL_RECORDER=gemma4:e4b

Anti-FraudX keeps the older fine-tuned scammer and expert models for specialist behavior, while Gemma 4 E4B powers the default local reasoning, victim simulation, recorder analysis, and multimodal review paths.

How I Used Gemma 4

I used “Gemma 4 E4B” as the main local reasoning model for Anti-FraudX.

Gemma 4 powers the parts of the system where general reasoning and user-facing explanation matter most:

  • Default model path: handles general local AI responses when no specialist model is selected
  • Victim agent: simulates realistic user reactions under scam pressure
  • Recorder agent: records the simulation, analyzes risk, and generates feedback
  • Vision path: reviews suspicious screenshots and scam-related images

I chose “E4B” because Anti-FraudX is meant to be deployable in realistic community environments. A larger model such as 31B Dense may offer stronger reasoning, but it also increases hardware cost and makes local deployment harder. Smaller models may be easier to run, but they may not provide enough reasoning depth for role-based simulation, trust scoring, and clear anti-fraud explanations.

E4B is the best balance for this project because it is:

  • practical for local Ollama deployment
  • strong enough for short multi-step reasoning
  • suitable for privacy-sensitive scam training data
  • useful for both conversation and moderation-style feedback
  • accessible enough for schools, homes, and community workshops

This makes Gemma 4 more than a chatbot model inside the project. It sits inside the training loop: users interact with scenarios, agents respond, the recorder explains what happened, and the system turns the interaction into a learning experience.

Why I prefer docker + make

A development environment is one of those things you don’t notice until it goes wrong, and then you can’t think about anything else. A new hire spends two days getting the project to run. A test passes locally and fails in CI. The Postgres version on your laptop is 13 and production is 15, and a query that’s fast for you takes forever in staging. Someone updates a dependency and half the team can’t start the app until they delete and reinstall their entire toolchain. Every one of these is a small disaster that shouldn’t have happened.

My stack for avoiding this is unfashionable and boring: Docker for the environment, Make for the workflow on top of it. It isn’t the only way to do this and it isn’t always the best way. But it’s the combination I keep coming back to, and the reasons are worth writing down.

What Docker gives you

The pitch for Docker, after all the hype died down, is simple: the development environment is pinned, versioned, and identical to (or very close to) the production one. The image specifies the OS, the language runtime, the system libraries, the binaries. Two developers running the same image are running the same environment. The CI server running the same image is running the same environment. The container you deploy to production is running the same environment. “Works on my machine” stops being a meaningful thing to say, because the machine isn’t yours — it’s the image.

The same property extends to dependent services. With docker compose, adding a Postgres or a Redis or a RabbitMQ to your local stack is a few lines of YAML, and the version is pinned the same way. Nobody on the team has to install Postgres on their laptop. Nobody has to remember which Postgres version the project needs. The configuration is in the repo, and it stays in sync with the application code that depends on it.

Once you have this, you stop being surprised by environment differences. That alone is worth a lot.

Why Docker alone isn’t enough

The catch is that Docker commands are verbose, unmemorable, and easy to invoke wrong. Running tests inside the container looks something like:

docker compose run --rm app bundle exec rspec spec/

That’s four pieces of information — the orchestrator, the subcommand, the service name, the actual command — and you have to remember all of them in the right order. The first time you type it, you’ll fight with it. The tenth time, you’ll have it muscle-memorized. The hundredth time, you’ll mistype it under pressure and wonder why the test runner can’t find your file.

It gets worse. Most developers will have bundle or npm or composer installed locally too, because their editor wants it for IntelliSense or because of some other tool that needs it. The temptation to run bundle exec rspec outside the container (because it’s shorter and faster to type) is constant. And the moment some people on the team are running tests inside the container and others are running them outside, you’re back to “works on my machine,” just with extra steps. Discipline isn’t a feature of the stack at that point; it’s something each developer has to provide individually, which means at least one of them will provide less of it on a tired afternoon.

Make as the single front door

Make is what I use to fix this. It is older than most of the things on my computer, ubiquitous on Unix systems, and almost embarrassingly simple. A Makefile is a list of named targets, each of which expands to a command (or several). You type make test and the Makefile knows what that means.

A small Makefile might look like:

.PHONY: up down test lint shell migrate

up:
    docker compose up -d

down:
    docker compose down

test:
    docker compose run --rm app bundle exec rspec

lint:
    docker compose run --rm app bundle exec rubocop

shell:
    docker compose run --rm app bash

migrate:
    docker compose run --rm app bundle exec rails db:migrate

That’s the whole game. The verbose Docker commands are now invoked by their semantic names. make test is shorter, more memorable, and harder to get wrong than the underlying command. It also doesn’t matter whether you have Ruby or Node installed on your laptop, because make test always goes through the container, every time, for everyone. The discipline isn’t something each developer carries; it’s built into the workflow.

There’s a secondary benefit I value a lot: the Makefile is documentation. A new developer reading the Makefile learns what the project’s workflow is — these are the operations the team performs, with these names. The README can say “run make test” and the implementation details of how that actually happens are one click away, but invisible to anyone who just wants to do the thing.

The agent angle

This is where the stack pays an extra dividend. When a coding agent works on your project, it has to figure out how to do basic operations — run the tests, format the code, start the database, apply migrations. Without a clear convention, it’ll improvise, and it’ll improvise differently every time. Sometimes it’ll find your Docker commands. Sometimes it’ll try to run things on the host and fail because the language runtime isn’t installed there. Sometimes it’ll invent a new approach that almost works.

With a well-documented Makefile and an instruction in the agent’s harness that says “always use make targets; never invoke Docker or language tooling directly,” you’ve given the agent a stable, narrow interface to the project. It doesn’t have to figure out the environment. It just has to read the Makefile.

This is the same property that helps human developers, applied to a contributor that doesn’t get tired or bored of typing make test for the thousandth time. The agent gets the same single front door. Your CI uses the same targets. Your local development uses the same targets. The whole team, humans and agents both, converges on one way to do each thing, and that one way runs inside the same environment as production.

The honest caveats

Make isn’t fashionable. The syntax is finicky (tabs, not spaces, and the rules around variable expansion will bite you eventually). For very complex workflows, you’ll outgrow it, and you’ll either learn to live with its quirks or move to a task runner like grunt, rake, or just, which is essentially Make with the rough edges sanded off. The reason why I prefer Make over many other task runners is that it does not depend on a runtime.

Docker has its own costs. The image build cycle adds friction. File watching across the container boundary can be slow. On macOS, the virtualization layer has historically been a source of mysterious performance issues, though it’s gotten much better. None of this is free.

But neither is the alternative. The alternative is the new hire on day one, the version skew on Postgres, the test that passes for one developer and fails for another, the agent that can’t figure out how to run anything. Docker plus Make is a way of paying these costs once, up front, in the form of a Dockerfile and a Makefile, instead of paying them again and again as small ongoing surprises. That trade has consistently worked out in my favor.

Cognitive middleware for n8n agents: four ways to wire Ejentum in

One n8n workflow with four integration patterns for wiring a reasoning harness into an agent. Pick your tradeoff between determinism and model discretion.

LLMs are good at producing answers. They are not consistently good at applying the verification steps a human would have wanted them to apply. That gap is what cognitive middleware is for: a layer between the model and its output that injects task-specific failure patterns, target patterns, and falsification tests into the agent’s prompt before it answers.

This post walks through four ways to wire one such middleware (Ejentum’s reasoning harness) into an n8n agent, all in one importable workflow. Same chat trigger, four branches selected by slash command. Each branch is a different tradeoff between determinism (you decide) and model discretion (the agent decides).

What Ejentum is

Ejentum is a reasoning API for AI agents. Each call returns a structured scaffold:

  • Failure patterns to avoid (the specific failure mode for the task at hand)
  • Target patterns to hit (what success looks like)
  • Falsification tests (what would prove the answer wrong)
  • Amplify / Suppress signals (which reasoning moves to engage or block)

The agent absorbs that scaffold into its prompt before answering. Four modes available: reasoning, code, anti-deception, memory.

Why bother

Two concrete reasons.

1. Agents that catch failure modes most agents miss. On a 6-turn manipulation eval shipped in the same repo, the harness-augmented GPT-4.1 named all 7 manipulation patterns the customer used. Baseline GPT-4.1, same model, no harness, named zero. Blind judge totals: 23 vs 35 on a 7-dimension rubric (eval source).

2. Each reasoning ability is a self-contained cognitive operation. It is engineered to give procedural steps instead of theatrical content. The agent does not get a “be careful” prompt; it gets a topology of gates, traps, and verification points to execute against.

The four wiring patterns

The workflow has one chat trigger that routes to four branches based on the prefix of your input message.

1. Dynamic system prompt: /inject /reasoning

Trigger with any of: /inject /reasoning, /inject /code, /inject /memory, /inject /anti-deception.

The matching mode is called, the bracketed scaffold is parsed into separate fields, and a filter step assembles the final injection block into the agent’s system prompt. The model never decides whether to apply the harness; the prefix decides.

Three nodes per mode: HTTP Request, then a Code parser, then Edit Fields. The parser exposes each bracket as its own drag-and-drop field (negative_gate, procedure, reasoning_topology, target_pattern, falsification_test, amplify, suppress), so you can remix the injection or pull fields from another mode to build hybrid scaffolds.

Use when: routing reliability matters more than agent autonomy. Zero routing risk.

2. Reasoner agent: /reasoning

The reasoning harness is attached to the agent as one HTTP Request tool. The agent decides on its own whether to call it. One mode, one tool, one focused worker.

Use when: analytical tasks (explanation, comparison, tradeoff, root-cause) where you trust the model to call the tool at the right moment.

3. Full harness: /full

All four harnesses are attached as separate HTTP Request tools (reasoning, code, perception, anti_deception). The agent classifies its own task and picks which harness to call.

Use when: general-purpose agents handling mixed workloads. Routing accuracy depends on model strength; weaker models confuse harnesses. Naming the mode explicitly in your user prompt raises accuracy without changing the wiring.

4. Ejentum-mcp: /ejentum-mcp

Instead of four HTTP tool nodes, the agent connects to the hosted Ejentum MCP server at https://api.ejentum.com/mcp via the n8n MCP Client node. All four harnesses are exposed through one tool node.

Functionally equivalent to /full for the agent’s behavior, but the workflow footprint is much smaller.

Use when: the same agent runs across multiple workflows and you want one integration point to maintain.

Picking the right pattern

If you want… Use
Determinism (always apply the harness, same way every time) /inject
One specific mode wired in at the model’s discretion /reasoning
Model picks from all four harnesses based on the task /full
Same as /full, fewer nodes, single integration point /ejentum-mcp

The tradeoff axis is how much routing discretion you hand to the model. Determinism on the left, flexibility on the right.

Quick import

  1. Get an Ejentum API key (free tier, 100 calls, no card) at ejentum.com.
  2. Get an OpenRouter API key at openrouter.ai/keys.
  3. In n8n, open Workflows then Import from File. Select harness_integration_patterns.json.
  4. Configure two credentials: OpenRouter (for the chat models) and Header Auth on the Ejentum nodes (Name: Authorization, Value: Bearer <your_key>).
  5. Open the chat and send /inject /reasoning hello to test the first branch.

Without HTTP nodes

There is also an n8n community node, verified at creators.n8n.io:

npm install n8n-nodes-ejentum

Or install from inside n8n: Settings → Community Nodes → Install → n8n-nodes-ejentum. Once installed, the four harnesses appear as a single node with mode selection in the dropdown.

Three install paths total, depending on your runtime:

The four wiring patterns in this template work with any of them.

Things to hack on

  • Remix the injection. Open any filter* node and reorder, drop, or replace fields. Pull code_failure into a reasoning injection, or negative_gate into a code injection. Hybrid scaffolds work.
  • Add a fifth branch. Duplicate any branch, change the chat prefix, customize. Common additions: a stacked branch that calls two modes in sequence, or a branch that routes on content classification instead of prefix.
  • Swap the chat model. Each branch has its own OpenRouter Chat Model node. Replace with Claude, GPT-4.1, Gemini, Llama, or whatever else you want.
  • Replace the harness. The four patterns are generic. Drop any HTTP tool or MCP server in the same slot and the wiring shapes still apply.

Why this exists

Most agent demos show one wiring pattern and call it the way. Reasoning middleware is not a single-pattern problem; it is a tradeoff space. Sometimes you want deterministic routing because you cannot trust the model to pick the right tool. Sometimes you want full discretion because the workload is too varied to route by prefix. Sometimes you want the smallest possible workflow because the agent is one of fifty in your stack.

This template gives a builder all four wiring shapes side by side so the choice is informed instead of inherited.

Links

Free tier: 100 calls, no card.

If you build something on top of this template, drop a comment with what you wired in. I want to see the hybrid injection patterns people come up with.

Integrating OIC with Databricks using AWS S3 (External Tables Approach)

Overview

In modern data integration scenarios, seamless connectivity between cloud platforms is critical. One common requirement is integrating Oracle Integration Cloud (OIC) with Databricks for performing table-based operations.

However, a key limitation arises:

❗ Databricks does not provide native RDS-style connectivity for direct table operations from OIC.

To overcome this limitation, we implement a scalable and efficient architecture:
Proposed Solution Architecture
OIC → AWS S3 → Databricks (External Tables)

This approach leverages AWS S3 as an intermediate storage layer, allowing OIC to push data, which Databricks can then consume via external tables.
Architecture Flow

1. OIC (Oracle Integration Cloud)

  • Acts as the data integration layer
  • Extracts and prepares data from upstream systems
  • Pushes data files (CSV/JSON/Parquet) to AWS S3

2. AWS S3 (Storage Layer)

  • Serves as a staging area for data
  • Stores files uploaded by OIC
  • Organizes data into structured folders (e.g., by date/source)

3. Databricks (Processing Layer)

  • Uses External Tables to read data directly from S3
  • Processes data using Spark SQL / PySpark
  • Performs transformations and analytics

Why External Tables?
Since Databricks cannot directly expose tables like traditional RDS systems:

✅ External tables allow querying data stored in S3
✅ No need to move data into Databricks-managed storage
✅ Enables scalable and cost-effective data access
Example:
CREATE TABLE customer_data
USING CSV
LOCATION 's3://your-bucket/path/'
OPTIONS (header = "true");

Required Connection Details for OIC
To enable the integration (OIC → S3), the following details are required:
📦 AWS S3 Access Details

  • Bucket Name
  • Region
  • Folder Path (if applicable)
  • Access Key ID
  • Secret Access Key

🔗 Connectivity Configuration in OIC

Configure REST Adapter or FTP Adapter / Stage File Action
Use pre-signed URLs or API-based upload (if secured)
Ensure proper authentication mechanism (AWS Signature v4)

⚙️ OIC Design Reference
Step 1: Data Extraction

Fetch data from source application (DB / SaaS)

Step 2: Data Transformation

Convert to required format (CSV/JSON)

Step 3: File Creation

Use Stage File action in OIC

Step 4: Upload to S3

Use REST API or direct adapter
Push file into S3 bucket

✅ Advantages of This Approach

🔹 Decouples OIC and Databricks
🔹 Highly scalable using S3 storage
🔹 Cost-efficient (pay-as-you-go)
🔹 Supports batch and near real-time processing
🔹 Simplifies integration architecture

⚠️ Key Considerations

Ensure data format compatibility (CSV, Parquet preferred)
Maintain naming conventions in S3
Handle data partitioning for performance
Secure credentials using OCI Vault / AWS IAM policies
Monitor file upload success and retries in OIC

🎯 Conclusion
Due to the absence of native RDS-style table connectivity in Databricks, using an intermediary layer like AWS S3 is the most effective integration pattern.
The combination of:

OIC for orchestration
S3 for storage
Databricks external tables for processing

provides a robust, scalable, and cloud-native solution.

Hobbyist 3D Printing and Vibe Coding

Why both feel less like “technical work” and more like modern magic

If you’ve ever owned a 3D printer, you probably know this feeling.

Your charging cable keeps slipping behind the nightstand. It’s a tiny annoyance — not important enough to buy a product for, but annoying enough that you notice it every day.

So one afternoon you open Tinkercad, sketch a simple clip, and send it to the printer.

A few hours later, the problem is gone.

Something that didn’t exist this morning now exists because you decided it should.

That feeling is strangely addictive.

And honestly? Vibe coding feels exactly the same.

The same magic, just in software

Maybe your screenshots save with terrible filenames.

Maybe you keep copying the same text at work.

Maybe you’re tired of manually organizing files every week.

So you open an AI coding tool and say:

“Can you make me a script that renames screenshots automatically?”

A few prompts later, it mostly works.

A few tweaks later, it works well enough.

And suddenly a daily annoyance disappears from your life forever.

That’s the same emotional payoff as hobbyist 3D printing.

Different tools. Same experience.

In both cases, you noticed friction in your life and removed it yourself — quickly, creatively, and without waiting for someone else to solve it for you.

That’s what makes both hobbies feel less like technical work and more like modern magic.

We used to need years of preparation before we could make anything

For a long time, creating things required permission from expertise.

If you wanted to build furniture, you learned woodworking.

If you wanted to make software, you learned programming.

If you wanted to invent useful tools, you studied engineering.

The path looked like this:

Study first. Create later.

That model still matters professionally. Skilled engineers and developers are incredibly valuable.

But hobby-level creation is changing fast.

Now the tools handle a huge amount of the technical complexity for you.

A 3D printer handles precision manufacturing.

AI coding tools handle syntax, debugging help, scaffolding, and repetitive boilerplate.

You still need curiosity and problem-solving skills.

But you no longer need years of preparation before you’re allowed to make useful things.

That’s a massive shift.

Because creativity is becoming accessible before mastery instead of only after it.

The real addiction is the feedback loop

Both 3D printing and vibe coding run on the same cycle:

  1. Notice a problem
  2. Make a rough solution
  3. Test it immediately
  4. Improve it
  5. Repeat

That loop has always existed.

What changed is the speed.

You no longer wait weeks to see whether your idea works.

You can test ideas the same afternoon you have them.

That fast feedback changes how your brain approaches creativity.

Experimentation starts feeling cheap.

And when experimentation becomes cheap, you try far more ideas.

That’s when a subtle mindset shift happens.

You stop saying:

“I wish somebody would build this.”

And start saying:

“I could probably build this myself.”

That sentence changes people.

Because once you feel capable of modifying your environment, you stop seeing daily frustrations as permanent.

You start seeing them as editable.

Neither hobby is really about scale

A lot of people misunderstand both hobbies because they compare them to professional production.

But most hobbyist 3D printing isn’t trying to compete with factories.

People are making:

  • Cable organizers
  • Custom brackets
  • Replacement knobs
  • Tiny desk tools
  • Weird little solutions for oddly specific problems

Factories optimize for scale.

Hobbyists optimize for specificity.

Vibe coding works the same way.

Most people vibe coding are not trying to build billion-dollar startups.

They’re making:

  • Tiny automations
  • Personal dashboards
  • Niche tools
  • Habit trackers
  • Scripts that solve one annoying repetitive task

These are things that would never justify a full software team.

But now they don’t need one.

That’s the real power of modern creative tools.

A thing no longer needs mass-market value to deserve existing.

It only needs to make your life better.

The biggest shift is psychological

3D printing turns ideas into physical objects.

Vibe coding turns ideas into software.

Both shrink the distance between:

“I wish this existed.”

and

“Here, I made it.”

That distance used to feel enormous.

Now it feels surprisingly small.

And once you cross that gap a few times, you stop moving through the world passively.

You start noticing opportunities everywhere.

That annoying app workflow.

That repetitive task.

That missing feature.

That oddly specific object nobody sells.

Instead of accepting friction as permanent, you start wondering if you could simply build your way around it.

That’s the quiet transformation both hobbies create.

Not just better gadgets or smarter scripts — but a stronger sense of agency.

A feeling that the world around you is more editable than it used to be.

And honestly, that might be the most exciting part of all.

Final thought

Hobbyist 3D printing and vibe coding are really the same hobby in different forms.

One lets you reshape the physical world.

The other lets you reshape the digital one.

But both teach the same lesson:

The barrier between having an idea and making it real is collapsing faster than most people realize.

And once you experience that firsthand, it becomes very hard to go back to believing you’re “just a consumer” of the world around you.

why is software development getting MORE complex every year — when the goal never changed?

Every software product ever built is the same three things.
Input → Analyze → Output.
A notepad. A banking system. An ERP. An AI assistant.
All identical in shape. Only the analyzer changes.

So why is software development getting MORE complex every year — when the goal never changed?
I spent time mapping this out. The answer is uncomfortable.

𝗧𝗵𝗲 𝗧𝗿𝗮𝗰𝗸𝗲𝗿’𝘀 𝗣𝗮𝗿𝗮𝗱𝗼𝘅
Every tool built to manage work eventually becomes work itself.

Not because the tool is bad. Because of a systemic pattern that every project tracker, documentation system, and workflow platform follows — regardless of how well it is designed:

  1. Simple human problem: “Who is doing what? Is it done?”
  2. A good tool is built. Solves it at first.
  3. The tool creates its own complexity — config, plugins, training, admin roles
  4. A new role emerges to manage the tool full-time
  5. Teams run “tool cleanup sprints” to manage the tool that manages the work

↺ Then the replacement tool follows the exact same arc.

𝗧𝗵𝗲 𝗻𝘂𝗺𝗯𝗲𝗿𝘀 𝗳𝗼𝗿 𝗮 𝗺𝗶𝗱-𝘀𝗶𝘇𝗲 𝘁𝗲𝗮𝗺:
Tool overhead — licensing, admin salaries, training, and developer time spent managing the tool instead of building.

The original question — “is the work getting done?” — is often harder to answer than ever.

𝗧𝗵𝗲 𝗿𝗼𝗼𝘁 𝗰𝗮𝘂𝘀𝗲:
The Semantic Gap — the distance between human intent (contextual, meaningful, fuzzy) and machine execution (literal, precise, syntactic).

Every layer of tooling complexity exists to bridge this gap manually. Each bridge needs its own bridge. The industry calls this progress.

𝗧𝗵𝗲 𝗽𝗿𝗼𝗽𝗼𝘀𝗲𝗱 𝗲𝘅𝗶𝘁:
One product. Intent in. Value out. Nothing in between.

Not a tool that tracks work — a system that ensures the work gets done.
Not a documentation system — a system that preserves the WHY so nobody ever has to ask.

The loop breaks when the only interface is natural language and the output is the actual value. No config. No admin. No training spiral. No cleanup sprints.

𝗧𝗵𝗶𝘀 𝗶𝘀 𝘄𝗵𝘆 𝗜’𝗺 𝗯𝘂𝗶𝗹𝗱𝗶𝗻𝗴:
→ 𝗗𝗼𝗰𝘂𝗙𝗹𝗼𝘄: captures WHY decisions were made — not static docs, but the living theory of the system. Gives AI the domain context to be genuinely useful.
Github: github/docuflow

→ 𝗪𝗮𝘆𝗺𝗮𝗿𝗸: makes AI agent actions transparent and auditable. Builds the trust that lets humans step back — gradually, verifiably, permanently.
Github: github/waymark

Both are designed with exit conditions. Success means users need them LESS over time — not more.

𝗧𝗵𝗲 𝗼𝗻𝗲 𝗾𝘂𝗲𝘀𝘁𝗶𝗼𝗻 𝗜 𝗮𝘀𝗸 𝗲𝘃𝗲𝗿𝘆 𝘄𝗲𝗲𝗸:
“Does this make the user’s problem easier to solve — or our system easier to build?”

If it’s the second one: accidental complexity. Cut it.

The goal was never the software.
It was always the value on the other side of it.

What pattern have you seen the Tracker’s Paradox play out in?

Check more details on my Linkedin/post

Day 15 of 60: I Built a Multi-Agent Research System That Researches, Writes and Reviews Reports in Under 60 Seconds

Today’s build is the most sophisticated thing I’ve shipped so far.

A single AI call handles one task well. But complex research requires four fundamentally different cognitive tasks; finding information, synthesising it, writing a structured report, and reviewing its quality. Each needs different instructions. Different tools. Different evaluation criteria.

So I stopped using one agent and built three.

## The Real World Problem

When you send a complex research question to a single Claude prompt you get a single pass. The response might be well-written but poorly researched. Or well-researched but poorly structured. Or both good but containing errors nobody checked.

In the real world every high-quality deliverable goes through multiple specialist hands. Research agencies, law firms, consulting firms, and newsrooms all use teams, not because one person cannot do the work, but because specialisation and review produce dramatically better results.

## The Analogy That Made It Click

Think of a newspaper editorial team. When a breaking story comes in three people handle it:

The journalist: gathers facts, interviews sources, finds the data. Does not write the final article.
The writer: takes the journalist’s notes and turns them into a polished, structured article. Was not at the scene.
The editor: reads the finished article before it publishes. Checks facts, identifies gaps, suggests improvements. Did not gather or write anything.

Three specialists. Three distinct roles. One final product better than any one of them could produce alone.

## How the System Works

Topic comes in
↓
Researcher Agent: searches the web via Tavily
                   summarises findings
↓
Writer Agent: takes research output
               produces structured written report
↓
Reviewer Agent: reads the report
                 returns quality score + specific improvements
↓
Final reviewed report delivered in under 60 seconds

The cast:

  • Researcher = The journalist: finds information via Tavily web search
  • Writer = The writer: turns research into a structured report
  • Reviewer = The editor: checks quality and flags improvements
  • Orchestrator = The news desk: coordinates handoffs between agents
  • Claude = The intelligence powering all three with different instructions
  • Tavily = The press pass: gives the Researcher access to live web search

## What I Learned

The biggest shift today was thinking in roles not prompts. Every agent has its own identity, its own instructions, and its own output format. The output of each becomes the input of the next.

This is the architecture behind every serious AI product being built in 2026. Content agencies, research firms, legal teams, and marketing departments all need research-to-report pipelines that don’t require a human at every step.

**🔗 Full project on GitHub → https://github.com/mbuguacessy-glitch

45 more to go.**

ai #multiagent #claudeapi #python #automation #learninpublic #buildinpublic #100DaysOfCode

What We Found Scanning 53 Shopify Stores for App-Theme Conflicts

We built a tool that scans Shopify storefronts for compatibility issues between third-party apps and themes. Over the past two months, we pointed it at 53 live stores — all running at least three third-party apps — and logged every conflict we could detect.

Here’s the raw data:

53 stores scanned

87/100 average compatibility score

147+ critical issues flagged

2.5x more rendering problems in stores running 3+ apps vs. 1–2

An 87 average sounds fine. It’s not. The distribution is bimodal: most stores either score 92+ (minimal issues) or drop below 75 (multiple broken interactions). The middle ground is thin. And the stores below 75? They almost always have the same three problems.

Pattern #1: CSS Specificity Wars (70% of Issues)

This was the single biggest source of conflicts. Roughly 70% of every critical issue we flagged traced back to CSS specificity.

The pattern is predictable. A reviews app injects styles like this:

.review-widget {
  padding: 16px;
  margin-bottom: 24px;
  font-size: 14px;
}

Clean, reasonable CSS. The problem is the theme already has this:

#shopify-section-template--product .product__block .review-widget {
  padding: 0;
  margin: 0;
  font-size: inherit;
}

The theme selector has a specificity of 1-2-1. The app selector has 0-1-0. The theme wins every time, and the review widget renders as a compressed, unreadable block.

What app developers do next makes it worse. They escalate:

.review-widget {
  padding: 16px !important;
  margin-bottom: 24px !important;
  font-size: 14px !important;
}

This “fixes” the reviews — and breaks the store’s carefully tuned spacing on mobile. We found !important declarations in 31 of the 53 stores we scanned. In 19 of those, the !important rules created new layout issues that weren’t present before the app was installed.

The z-index arms race

The second CSS pattern we saw constantly was z-index stacking conflicts. Here’s a real example from our dataset:

/* Theme: sticky header */
.header__wrapper {
  position: sticky;
  z-index: 999;
}

/* App A: notification bar */
.announcement-bar-widget {
  position: fixed;
  z-index: 1000;
}

/* App B: chat widget */
.chat-bubble-container {
  position: fixed;
  z-index: 9999;
}

/* App C: email signup popup */
.signup-modal-overlay {
  position: fixed;
  z-index: 90000;
}

Four apps. Four different z-index strategies. Zero coordination. The result: the signup popup renders above everything (including the close button of the chat widget), the notification bar covers the header on scroll, and the merchant has no idea why their mobile UX feels broken.

We found z-index values exceeding 10,000 in 23 of 53 stores. Five stores had z-index values above 90,000.

What actually works

For app developers building Shopify theme extensions, scope your CSS aggressively:

/* Instead of this: */
.review-widget { ... }

/* Do this: */
[data-app-reviews] .review-widget {
  /* Scoped to your app's container */
  padding: 16px;
  margin-bottom: 24px;
}

If you’re targeting modern browsers (and you should be — Shopify’s audience skews current), CSS @layer is a cleaner solution:

@layer app-reviews {
  .review-widget {
    padding: 16px;
    margin-bottom: 24px;
    font-size: 14px;
  }
}

Layers let your styles participate in the cascade without brute-forcing specificity. The theme’s unlayered styles will still win by default, but you can negotiate precedence without !important.

Pattern #2: JavaScript Event Handler Collisions

This accounted for about 20% of the critical issues we flagged. The pattern: multiple apps binding event listeners to the same DOM elements, usually around cart interactions.

Here’s what we saw in one store running a wishlist app, a reviews app, and an upsell widget:

// App A: Wishlist — intercepts add-to-cart
document.querySelectorAll('form[action="/cart/add"]').forEach(form => {
  form.addEventListener('submit', function(e) {
    e.preventDefault();
    // Track wishlist state, then submit
    trackWishlistConversion(form);
    form.submit();
  });
});

// App B: Upsell — also intercepts add-to-cart
document.querySelectorAll('form[action="/cart/add"]').forEach(form => {
  form.addEventListener('submit', function(e) {
    e.preventDefault();
    // Show upsell modal, then submit
    showUpsellModal(form);
  });
});

Both apps call preventDefault(). Both expect to be the only one controlling the submit flow. The result: the add-to-cart button either fires twice, shows the upsell modal but never adds the product, or silently fails with no feedback.

We found this exact collision pattern — multiple preventDefault() calls on the same form — in 11 of 53 stores.

Script loading order is the other half

Apps that depend on jQuery (yes, still) face a loading order problem. The theme loads jQuery deferred. The app’s inline script executes immediately:

<!-- Theme: deferred jQuery -->
<script src="jquery-3.6.0.js" defer></script>

<!-- App: inline script that assumes jQuery exists -->
<script>
  jQuery(document).ready(function() {
    $('#product-reviews').on('click', '.review-star', function() {
      // This throws: jQuery is not defined
    });
  });
</script>

This fails silently in production. The review stars don’t work. The merchant doesn’t know why. The app developer’s test store (with jQuery loaded synchronously) works fine.

What actually works

Use custom events instead of hijacking native form submissions:

// Emit a custom event instead of intercepting submit
document.addEventListener('cart:before-add', function(event) {
  const { form, product } = event.detail;
  // Do your thing without blocking other listeners
});

// Coordinate with other apps
const cartEvent = new CustomEvent('cart:before-add', {
  detail: { form, product },
  cancelable: true
});
document.dispatchEvent(cartEvent);

And drop jQuery. Vanilla JS event listeners with defer solve the loading order problem entirely:

<script defer src="your-app.js"></script>

// your-app.js — runs after DOM is ready because of defer
document.querySelector('#product-reviews')
  .addEventListener('click', handleReviewClick);

Pattern #3: DOM Mutation Conflicts

The remaining ~10% of critical issues came from apps that make assumptions about DOM structure. When the theme updates, those assumptions break.

A typical example: an app targets a specific class to inject its widget:

// App expects this structure
const container = document.querySelector('.product__info-container');
if (container) {
  container.insertAdjacentHTML('afterend', widgetHTML);
}

Theme updates from Dawn 12.0 to 13.0. The class changes to .product__info-wrapper. The app injects nothing. No error, no fallback — just a missing widget.

We tracked stores across multiple scans and found that theme updates caused at least one app widget to disappear in 8 of the 53 stores during our observation window.

The MutationObserver pileup

Worse, some apps use MutationObserver to watch for DOM changes and re-inject their widgets. When three apps all observe document.body for childList changes:

// Three separate MutationObservers, all watching body
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    // App re-injects widget on every DOM change
    if (!document.querySelector('.my-widget')) {
      injectWidget();
    }
  });
});
observer.observe(document.body, { childList: true, subtree: true });

Each injection triggers the other observers. In the worst case we found, this created a tight re-render loop that pushed page load time from 2.1s to 6.8s.

What actually works

Use Shopify’s app blocks and theme app extensions. They’re designed for exactly this:

{% comment %} blocks/review-widget.liquid {% endcomment %}
{% schema %}
{
  "name": "Product Reviews",
  "target": "section"
}
{% endschema %}

<div class="review-widget" data-product-id="{{ product.id }}">
  <!-- Widget content -->
</div>

App blocks let merchants place your widget in the theme editor. No DOM queries, no fragile class selectors, no MutationObserver hacks. When the theme updates, the block stays where the merchant put it.

The Takeaway

The stores that scored 92+ in our scans had a common thread: they used fewer apps (3–4 vs. 6+), and the apps they did use were built with Shopify’s modern extension architecture.

The stores below 75 were running 5+ apps, most injecting unscoped CSS and attaching event listeners directly to DOM elements. The conflicts weren’t bugs in any single app — they were emergent behavior from apps that were never tested together.

If you’re building Shopify apps:

  • Scope your CSS — use data attributes or CSS layers, not bare class selectors
  • Don’t hijack native events — emit custom events and let other apps coordinate
  • Use app blocks — stop querying the DOM for injection points
  • Test with other apps installed — your app will never run alone in production

We’ve published the aggregate data from our scans — broken down by conflict type, app category, and theme — at preflight.technology/insights. No signup required to browse the patterns. If you’re debugging a specific app’s compatibility, the per-app conflict lookup is there too.

Personal AI Assistant – Setting Up Free Cloud Server – The Complete Guide

quick-wizard-home

Introduction

This is Part 2 of the Zero Dollar AI Assistant series. Part 1- Running a Personal AI Assistant for $0 – Architecture covers the full stack and why it works.

AWS, Google Cloud, and Azure have a free tier. However, they all have one thing in common: a clock ticking. Twelve months, then the bill starts.

Oracle Cloud is different. Their Always Free Tier has no expiry date. The server you provision today will still be running and free, as long as your account stays active. The benefit also includes the block storage capacity and its free tier. That’s the foundation this entire stack is built on.

This article walks you through setting up that foundation: creating a properly configured Oracle Cloud account, provisioning an ARM instance with the right specs, and solving every gotcha along the way.

Understanding Oracle’s Always Free Tier

Before touching the console, know what you’re getting into:

Always Free compute:

  • Up to 4 ARM CPU cores and 24GB RAM total across ARM instances — permanently free
  • 2x VM.Standard.E2.1.Micro (1 OCPU, 1GB RAM each) — also permanently free x86 instances, but avoid these for this stack — 1GB RAM is far too tight for Ollama
  • These are not trial credits. They don’t expire.

Always Free storage:

  • 200GB total block storage
  • Enough for your server’s boot volume with room to spare

What costs money:

  • Going beyond 4 OCPU / 24GB RAM on ARM
  • Reserved public IPs that aren’t attached to a running instance
  • Outbound data beyond 10TB/month — in practice, a personal AI assistant will never get close to this

The shape you want is VM.Standard.A1.Flex — Oracle’s ARM instance. Use the full free allocation: 4 OCPU and 24GB RAM. This gives Ollama maximum headroom and keeps the system responsive under load.

Account Setup

Free Tier vs Pay As You Go

Start by creating an Oracle Cloud account at cloud.oracle.com. You’ll be on the Free Tier by default.

Here’s the catch: ARM capacity on the free tier is frequently exhausted. You’ll often see “Out of Capacity” errors across all availability domains. The fix is upgrading to Pay As You Go (PAYG).

PAYG sounds alarming, but it isn’t:

  • Always Free resources remain free — your ARM instance still costs $0
  • You only pay if you exceed free tier limits
  • Oracle places a temporary $100 authorization hold on your card when you upgrade — this is a verification hold, not a charge, and disappears within 3-5 business days

Upgrade via: Billing → Upgrade and Manage Payment

Set a budget alert immediately after upgrading:

  • Billing → Budgets → Create Budget
  • Amount: $1
  • Alert at 50% and 100%

This ensures you’re notified before any real spend happens. In practice, for this setup, you’ll never see the alert trigger.

Creating the VCN

Before launching an instance, you need a Virtual Cloud Network (VCN) with internet connectivity. This is where most people hit their first obstacle.

The critical mistake to avoid: Don’t use the blue “Create VCN” button on the VCN list page. It creates a bare VCN with nothing attached — no subnets, no internet gateway, no routing. You’ll end up with an instance that has no public IP and no way to SSH in.

The correct path — VCN Wizard:

Option A — From the Console home page:

  • Look for “Set up a network with a wizard” in the build section
  • Click it

quick-wizard-home

Option B — From the VCN list page:

  • Hamburger (☰) → Networking → Virtual Cloud Networks
  • Look for “Start VCN Wizard” — it’s separate from the “Create VCN” button

quick-wizard-ham

Either way, select “Create VCN with Internet Connectivity” and proceed. Give it a name (e.g., openclaw-vcn works), leave CIDR defaults, and click through.

The wizard automatically creates:

  • Public subnet with internet routing
  • Private subnet
  • Internet Gateway
  • NAT Gateway
  • Route tables and security lists — all pre-wired

Add SSH Access

After the VCN is created:

  1. Go into your VCN → Subnets → click the Public Subnet
  2. Click Default Security List → Add Ingress Rules
  3. Add:
    • Source CIDR: 0.0.0.0/0
    • Protocol: TCP
    • Destination port: 22
  4. Save

Provisioning the Instance

Hamburger (☰) → Compute → Instances → Create Instance

Work through each section:

Image

Click Change Image and look carefully. Oracle lists ARM and x86 variants of Ubuntu side by side with nearly identical names:

  • Canonical Ubuntu 24.04 → x86 ❌
  • Canonical Ubuntu 24.04 Minimal aarch64 → ARM ✅

The aarch64 or Minimal aarch64 variant is what you want. Selecting the wrong one produces an “incompatible settings” error when combined with the ARM shape.

Recommended: Canonical Ubuntu 24.04 Minimal aarch64

The Minimal image strips unnecessary packages — smaller attack surface, less memory overhead, cleaner slate. Install any tools you need explicitly.

Shape

  • Shape: VM.Standard.A1.Flex
  • OCPUs: 4
  • Memory: 24GB

If you see “Out of Capacity”:

  • Try each availability domain (AD-1, AD-2, AD-3)
  • Set fault domain to “No preference.”
  • Try a different region
  • If all else fails, this is the primary reason to upgrade to PAYG — capacity is significantly more available

Networking

  • Select your openclaw-vcn
  • Select the public subnet
  • Check “Assign a public IPv4 address” ← easy to miss, critical

Boot Volume

Check “Specify a custom boot volume size” and set it to 100GB.

The default 46.6GB disappears quickly once Ubuntu, Node.js, Ollama, and a 5GB model are installed. 100GB stays well within your 200GB free storage allocation.

Leave in-transit encryption and confidential computing unchecked — both add overhead with no meaningful benefit for this use case.

SSH Keys

Generate a key pair or upload your existing public key. Alternatively, you can download and save the private key — you’ll need it every time you SSH in.

Click Create and wait 2-3 minutes for provisioning.🕛

Initial Server Configuration

Use the previously configured SSH key to SSH into the Oracle cloud server you just provisioned. SSH from your laptop. Get the Public IP from your instance provisioned.

SSH in:

chmod 400 ~/.ssh/id_rsa
ssh -i ~/.ssh/id_rsa ubuntu@YOUR_INSTANCE_PUBLIC_IP

NOTE: Run these steps in order — don’t skip them

System Update

sudo apt update && sudo apt upgrade -y
sudo apt install -y build-essential curl wget tmux

Install tmux immediately — you’ll use it for every long-running command. Starting a download without tmux and having SSH disconnect is a painful lesson.

Disable IPv6

Oracle Cloud’s network doesn’t support IPv6 for outbound connections in most configurations. OpenClaw’s Telegram integration will repeatedly timeout and fail if IPv6 is active:

echo 'net.ipv6.conf.all.disable_ipv6 = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.default.disable_ipv6 = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Verify it worked:

curl -6 https://api.telegram.org 2>&1 | head -3
# Should fail immediately — that's correct
curl -4 https://api.telegram.org 2>&1 | head -3
# Should succeed

Install Node.js

OpenClaw requires Node.js 22+. Install via NVM for flexibility:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
nvm alias default 22
node --version  # should show v22.x.x

Also install system-level Node (used by OpenClaw’s systemd service):

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

Useful Aliases

echo "alias clearmem="sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'"" >> ~/.bashrc
source ~/.bashrc

clearmem clears the page cache when memory gets tight. You’ll use it occasionally, even on 18GB RAM.

Verifying the Setup

Before moving on to Ollama and OpenClaw installation, confirm everything is healthy:

# Verify ARM architecture
uname -m
# Expected: aarch64

# Verify disk space
df -h
# /dev/sda1 should show ~96GB available

# Verify Node.js
node --version
# Should show v22.x.x

# Verify IPv6 is disabled
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
# Should show: 1

# Verify internet connectivity
curl -s https://api.telegram.org | head -3
# Should respond

If all five pass, your foundation is set up. You’re ready for Part 3 — installing Ollama and picking the right model for your hardware.

Common Issues and Fixes

  • This instance has incompatible settings
    You selected the x86 Ubuntu image with the ARM shape. Go back to image selection and choose the aarch64 variant.

  • Out of Capacity for VM.Standard.A1.Flex
    Try all three ADs, set fault domain to “No preference”, and consider upgrading to PAYG. Capacity is the single most common blocker for new Oracle free-tier accounts.

  • Instance created but no public IP showing
    The public subnet wasn’t selected during creation, or “Assign a public IPv4 address” wasn’t checked. Easiest fix: terminate and recreate with the correct networking settings.

  • SSH connection refused
    The SSH ingress rule wasn’t added to the security list. Go to VCN → Public Subnet → Default Security List → add TCP ingress on port 22.

  • SSH times out after a few minutes of inactivity
    Add to ~/.ssh/config on your local machine:

Host *
  ServerAliveInterval 60
  ServerAliveCountMax 10

What’s Next

With your Oracle Cloud instance running, IPv6 disabled, and Node.js installed, the infrastructure layer is done.

Part 3 of this zero-dollar AI Assistant series will cover installing Ollama on ARM, choosing the right model for CPU inference, managing disk space with large model files, and the hybrid local + Gemini fallback architecture that makes the whole thing actually usable.

This article is the second in a five-part series:

  1. Running a Personal AI Assistant for $0 – Architecture
  2. Setting Up Free Cloud Server — VCN, ARM instances, static IPs, the gotchas ← you are here
  3. Running Ollama on ARM — model selection, disk management, CPU inference reality
  4. Installing OpenClaw on Linux — avoiding every trap
  5. The Complete Setup — Telegram, Gemini fallback, end-to-end testing

Stay tuned, all links will be updated as articles are published.

If you have reached this point, I have made a satisfactory effort to keep you reading. Please be kind enough to leave any comments or share any corrections.

My Other Blogs: