Identifying Necessary Transparency Moments In Agentic AI (Part 1)

Designing for autonomous agents presents a unique frustration. We hand a complex task to an AI, it vanishes for 30 seconds (or 30 minutes), and then it returns with a result. We stare at the screen. Did it work? Did it hallucinate? Did it check the compliance database or skip that step?

We typically respond to this anxiety with one of two extremes. We either keep the system a Black Box, hiding everything to maintain simplicity, or we panic and provide a Data Dump, streaming every log line and API call to the user.

Neither approach directly addresses the nuance needed to provide users with the ideal level of transparency.

The Black Box leaves users feeling powerless. The Data Dump creates notification blindness, destroying the efficiency the agent promised to provide. Users ignore the constant stream of information until something breaks, at which point they lack the context to fix it.

We need an organized way to find the balance. In my previous article, “Designing For Agentic AI”, we looked at interface elements that build trust, like showing the AI’s intended action beforehand (Intent Previews) and giving users control over how much the AI does on its own (Autonomy Dials). But knowing which elements to use is only part of the challenge. The harder question for designers is knowing when to use them.

How do you know which specific moment in a 30-second workflow requires an Intent Preview and which can be handled with a simple log entry?

This article provides a method to answer that question. We will walk through the Decision Node Audit. This process gets designers and engineers in the same room to map backend logic to the user interface. You will learn how to pinpoint the exact moments a user needs an update on what the AI is doing. We will also cover an Impact/Risk matrix that will help to prioritize which decision nodes to display and any associated design pattern to pair with that decision.

Transparency Moments: A Case Study Example

Consider Meridian (not real name), an insurance company that uses an agentic AI to process initial accident claims. The user uploads photos of vehicle damage and the police report. The agent then disappears for a minute before returning with a risk assessment and a proposed payout range.

Initially, Meridian’s interface simply showed Calculating Claim Status. Users grew frustrated. They had submitted several detailed documents and felt uncertain about whether the AI had even reviewed the police report, which contained mitigating circumstances. The Black Box created distrust.

To fix this, the design team conducted a Decision Node Audit. They found that the AI performed three distinct, probability-based steps, with numerous smaller steps embedded:

  • Image Analysis
    The agent compared the damage photos against a database of typical car crash scenarios to estimate the repair cost. This involved a confidence score.
  • Textual Review
    It scanned the police report for keywords that affect liability (e.g., fault, weather conditions, sobriety). This involved a probability assessment of legal standing.
  • Policy Cross Reference
    It matched the claim details against the user’s specific policy terms, searching for exceptions or coverage limits. This also involved probabilistic matching.

The team turned these steps into transparency moments. The interface sequence was updated to:

  • Assessing Damage Photos: Comparing against 500 vehicle impact profiles.
  • Reviewing Police Report: Analyzing liability keywords and legal precedent.
  • Verifying Policy Coverage: Checking for specific exclusions in your plan.

The system still took the same amount of time, but the explicit communication about the agent’s internal workings restored user confidence. Users understood that the AI was performing the complex task it was designed for, and they knew exactly where to focus their attention if the final assessment seemed inaccurate. This design choice transformed a moment of anxiety into a moment of connection with the user.

Applying the Impact/Risk Matrix: What We Chose to Hide

Most AI experiences have no shortage of events and decision nodes that could potentially be displayed during processing. One of the most critical outcomes of the audit was to decide what to keep invisible. In the Meridian example, the backend logs generated 50+ events per claim. We could have defaulted to displaying each event as they were processed as part of the UI. Instead, we applied the risk matrix to prune them:

  • Log Event: Pinging Server West-2 for redundancy check.
    • Filter Verdict: Hide. (Low Stakes, High Technicality).
  • Log Event: Comparing repair estimate to BlueBook value.
    • Filter Verdict: Show. (High Stakes, impacts user’s payout).

By cutting out the unnecessary details, the important information — like the coverage verification — was more impactful. We created an open interface and designed an open experience.

This approach uses the idea that people feel better about a service when they can see the work being done. By showing the specific steps (Assessing, Reviewing, Verifying), we changed a 30-second wait from a time of worry (“Is it broken?”) to a time of feeling like something valuable is being created (“It’s thinking”).

Let’s now take a closer look at how we can review the decision-making process in our products to identify key moments that require clear information.

The Decision Node Audit

Transparency fails when we treat it as a style choice rather than a functional requirement. We have a tendency to ask, “What should the UI look like?” before we ask, “What is the agent actually deciding?”

The Decision Node Audit is a straightforward way to make AI systems easier to understand. It works by carefully mapping out the system’s internal process. The main goal is to find and clearly define the exact moments where the system stops following its set rules and instead makes a choice based on chance or estimation. By mapping this structure, creators can show these points of uncertainty directly to the people using the system. This changes system updates from being vague statements to specific, reliable reports about how the AI reached its conclusion.

In addition to the insurance case study above, I recently worked with a team building a procurement agent. The system reviewed vendor contracts and flagged risks. Originally, the screen displayed a simple progress bar: “Reviewing contracts.” Users hated it. Our research indicated they felt anxious about the legal implications of a missing clause.

We fixed this by conducting a Decision Node Audit. I’ve included a step-by-step checklist for conducting this audit at the conclusion of this article.

We ran a session with the engineers and outlined how the system works. We identified “Decision Points” — moments where the AI had to choose between two good options.

In standard computer programs, the process is clear: if A happens, then B will always happen. In AI systems, the process is often based on chance. The AI thinks A is probably the best choice, but it might only be 65% certain.

In the contract system, we found a moment when the AI checked the liability terms against our company rules. It was rarely a perfect match. The AI had to decide if a 90% match was good enough. This was a key decision point.

Once we identified this node, we exposed it to the user. Instead of “Reviewing contracts,” the interface updated to say: “Liability clause varies from standard template. Analyzing risk level.”

This specific update gave users confidence. They knew the agent checked the liability clause. They understood the reason for the delay and gained trust that the desired action was occurring on the back end. They also knew where to dig in deeper once the agent generated the contract.

To check how the AI makes decisions, you need to work closely with your engineers, product managers, business analysts, and key people who are making the choices (often hidden) that affect how the AI tool functions. Draw out the steps the tool takes. Mark every spot where the process changes direction because a probability is met. These are the places where you should focus on being more transparent.

As shown in Figure 2 below, the Decision Node Audit involves these steps:

  1. Get the team together: Bring in the product owners, business analysts, designers, key decision-makers, and the engineers who built the AI. For example,

    Think about a product team building an AI tool designed to review messy legal contracts. The team includes the UX designer, the product manager, the UX researcher, a practicing lawyer who acts as the subject-matter expert, and the backend engineer who wrote the text-analysis code.

  2. Draw the whole process: Document every step the AI takes, from the user’s first action to the final result.

    The team stands at a whiteboard and sketches the entire sequence for a key workflow that involves the AI searching for a liability clause in a complex contract. The lawyer uploads a fifty-page PDF → The system converts the document into readable text. → The AI scans the pages for liability clauses. → The user waits. → Moments or minutes later, the tool highlights the found paragraphs in yellow on the user interface. They do this for many other workflows that the tool accommodates as well.

  3. Find where things are unclear: Look at the process map for any spot where the AI compares options or inputs that don’t have one perfect match.

    The team looks at the whiteboard to spot the ambiguous steps. Converting an image to text follows strict rules. Finding a specific liability clause involves guesswork. Every firm writes these clauses differently, so the AI has to weigh multiple options and make a prediction instead of finding an exact word match.

  4. Identify the ‘best guess’ steps: For each unclear spot, check if the system uses a confidence score (for example, is it 85% sure?). These are the points where the AI makes a final choice.

    The system has to guess (give a probability) which paragraph(s) closely resemble a standard liability clause. It assigns a confidence score to its best guess. That guess is a decision node. The interface needs to tell the lawyer it is highlighting a potential match, rather than stating it found the definitive clause.

  5. Examine the choice: For each choice point, figure out the specific internal math or comparison being done (e.g., matching a part of a contract to a policy or comparing a picture of a broken car to a library of damaged car photos).

    The engineer explains that the system compares the various paragraphs against a database of standard liability clauses from past firm cases. It calculates a text similarity score to decide on a match based on probabilities.

  6. Write clear explanations: Create messages for the user that clearly describe the specific internal action happening when the AI makes a choice.

    The content designer writes a specific message for this exact moment. The text reads: Comparing document text to standard firm clauses to identify potential liability risks.

  7. Update the screen: Put these new, clear explanations into the user interface, replacing vague messages like “Reviewing contracts.”

    The design team removes the generic Processing PDF loading spinner. They insert the new explanation into a status bar located right above the document viewer while the AI thinks.

  8. Check for Trust: Make sure the new screen messages give users a simple reason for any wait time or result, which should make them feel more confident and trusting.

The Impact/Risk Matrix

Once you look closely at the AI’s process, you’ll likely find many points where it makes a choice. An AI might make dozens of small choices for a single complex task. Showing them all creates too much unnecessary information. You need to group these choices.

You can use an Impact/Risk Matrix to sort these choices based on the types of action(s) the AI is taking. Here are examples of impact/risk matrices:

First, look for low-stakes and low-impact decisions.

Low Stakes / Low Impact

  • Example: Organizing a file structure or renaming a document.
  • Transparency Need: Minimal. A subtle toast notification or a log entry suffices. Users can undo these actions easily.

Then identify the high-stakes and high-impact decisions.

High Stakes / High Impact

  • Example: Rejecting a loan application or executing a stock trade.
  • Transparency Need: High. These actions require Proof of Work. The system must demonstrate the rationale before or immediately as it acts.

Consider a financial trading bot that treats all buy/sell orders the same. It executes a $5 trade with the same opacity as a $50,000 trade. Users might question whether the tool recognizes the potential impact of transparency on trading on a large dollar amount. They need the system to pause and show its work for the high-stakes trades. The solution is to introduce a Reviewing Logic state for any transaction exceeding a specific dollar amount, allowing the user to see the factors driving the decision before execution.

Mapping Nodes to Patterns: A Design Pattern Selection Rubric

Once you have identified your experience’s key decision nodes, you must decide which UI pattern applies to each one you’ll display. In Designing For Agentic AI, we introduced patterns like the Intent Preview (for high-stakes control) and the Action Audit (for retrospective safety). The decisive factor in choosing between them is reversibility.

We filter every decision node through the impact matrix in order to assign the correct pattern:

High Stakes & Irreversible: These nodes require an Intent Preview. Because the user cannot easily undo the action (e.g., permanently deleting a database), the transparency moment must happen before execution. The system must pause, explain its intent, and require confirmation.

High Stakes & Reversible: These nodes can rely on the Action Audit & Undo pattern. If the AI-powered sales agent moves a lead to a different pipeline, it can do so autonomously as long as it notifies the user and offers an immediate Undo button.

By strictly categorizing nodes this way, we avoid “alert fatigue.” We reserve the high-friction Intent Preview only for the truly irreversible moments, while relying on the Action Audit to maintain speed for everything else.

Reversible Irreversible
Low Impact Type: Auto-Execute
UI: Passive Toast / Log
Ex: Renaming a file
Type: Confirm
UI: Simple Undo option
Ex: Archiving an email
High Impact Type: Review
UI: Notification + Review Trail
Ex: Sending a draft to a client
Type: Intent preview
UI: Modal / Explicit Permission
Ex: Deleting a server

Table 1: The impact and reversibility matrix can then be used to map your moments of transparency to design patterns.

Qualitative Validation: “The Wait, Why?” Test

You can identify potential nodes on a whiteboard, but you must validate them with human behavior. You need to verify whether your map matches the user’s mental model. I use a protocol called the “Wait, Why?” Test.

Ask a user to watch the agent complete a task. Instruct them to speak aloud. Whenever they ask a question, “Wait, why did it do that?” or “Is it stuck?” or “Did it hear me?” — you mark a timestamp.

These questions signal user confusion. The user feels their control slipping away. For example, in a study for a healthcare scheduling assistant, users watched the agent book an appointment. The screen sat static for four seconds. Participants consistently asked, “Is it checking my calendar or the doctor’s?”

That question revealed a missing Transparency Moment. The system needed to split that four-second wait into two distinct steps: “Checking your availability” followed by “Syncing with provider schedule.”

This small change reduced users’ expressed levels of anxiety.

Transparency fails when it only describes a system action. The interface must connect the technical process to the user’s specific goal. A screen displaying “Checking your availability” falls flat because it lacks context. The user understands that the AI is looking at a calendar, but they do not know why.

We must pair the action with the outcome. The system needs to split that four-second wait into two distinct steps. First, the interface displays “Checking your calendar to find open times.” Then it updates to “Syncing with the provider’s schedule to secure your appointment.” This grounds the technical process in the user’s actual life.

Consider an AI managing inventory for a local cafe. The system encounters a supply shortage. An interface reading “contacting vendor” or “reviewing options” creates anxiety. The manager wonders if the system is canceling the order or buying an expensive alternative. A better approach is to explain the intended result: “Evaluating alternative suppliers to maintain your Friday delivery schedule.” This tells the user exactly what the AI is trying to achieve.

Operationalizing the Audit

You have completed the Decision Node Audit and filtered your list through the Impact and Risk Matrix. You now have a list of essential moments for being transparent. Next, you need to create them in the UI. This step requires teamwork across different departments. You can’t design transparency by yourself using a design tool. You need to understand how the system works behind the scenes.

Start with a Logic Review. Meet with your lead system designer. Bring your map of decision nodes. You need to confirm that the system can actually share these states. I often find that the technical system doesn’t reveal the exact state I want to show. The engineer might say the system just returns a general “working” status. You must push for a detailed update. You need the system to send a specific notice when it switches from reading text to checking rules. Without that technical connection, your design is impossible to build.

Next, involve the Content Design team. You have the technical reason for the AI’s action, but you need a clear, human-friendly explanation. Engineers provide the underlying process, but content designers provide the way it’s communicated. Do not write these messages alone. A developer might write “Executing function 402,” which is technically correct but meaningless to the user. A designer might write “Thinking,” which is friendly but too vague. A content strategist finds the right middle ground. They create specific phrases, such as “Scanning for liability risks”, that show the AI is working without confusing the user.

Finally, test the transparency of your messages. Don’t wait until the final product is built to see if the text works. I conduct comparison tests on simple prototypes where the only thing that changes is the status message. For example, I show one group (Group A) a message that says “Verifying identity” and another group (Group B) a message that says “Checking government databases” (these are made-up examples, but you understand the point). Then I ask them which AI feels safer. You’ll often discover that certain words cause worry, while others build trust. You must treat the wording as something you need to test and prove effective.

How This Changes the Design Process

Conducting these audits has the potential to strengthen how a team works together. We stop handing off polished design files. We start using messy prototypes and shared spreadsheets. The core tool becomes a transparency matrix. Engineers and the content designers edit this spreadsheet together. They map the exact technical codes to the words the user will read.

Teams will experience friction during the logic review. Imagine a designer asking the engineer how the AI decides to decline a transaction submitted on an expense report. The engineer might say the backend only outputs a generic status code like “Error: Missing Data”. The designer states that this isn’t actionable information on the screen. The designer negotiates with the engineer to create a specific technical hook. The engineer writes a new rule so the system reports exactly what is missing, such as a missing receipt image.

Content designers act as translators during this phase. A developer might write a technically accurate string like “Calculating confidence threshold for vendor matching.” A content designer translates that string into a phrase that builds trust for a specific outcome. The strategist rewrites it as “Comparing local vendor prices to secure your Friday delivery.” The user understands the action and the result.

The entire cross-functional team sits in on user testing sessions. They watch a real person react to different status messages. Seeing a user panic because the screen says “Executing trade” forces the team to rethink their approach. The engineers and designers align on better wording. They change the text to “Verifying sufficient funds” before buying stock. Testing together guarantees the final interface serves both the system logic and the user’s peace of mind.

It does require time to incorporate these additional activities into the team’s calendar. However, the end result should be a team that communicates more openly, and users who have a better understanding of what their AI-powered tools are doing on their behalf (and why). This integrated approach is a cornerstone of designing truly trustworthy AI experiences.

Trust Is A Design Choice

We often view trust as an emotional byproduct of a good user experience. It is easier to view trust as a mechanical result of predictable communication.

We build trust by showing the right information at the right time. We destroy it by overwhelming the user or hiding the machinery completely.

Start with the Decision Node Audit, particularly for agentic AI tools and products. Find the moments where the system makes a judgment call. Map those moments to the Risk Matrix. If the stakes are high, open the box. Show the work.

In the next article, we will look at how to design these moments: how to write the copy, structure the UI, and handle the inevitable errors when the agent gets it wrong.

Appendix: The Decision Node Audit Checklist

Phase 1: Setup and Mapping

✅ Get the team together: Bring in the product owners, business analysts, designers, key decision-makers, and the engineers who built the AI.

Hint: You need the engineers to explain the actual backend logic. Do not attempt this step alone.

✅ Draw the whole process: Document every step the AI takes, from the user’s first action to the final result.

Hint: A physical whiteboard session often works best for drawing out these initial steps.

Phase 2: Locating the Hidden Logic

✅ Find where things are unclear: Look at the process map for any spot where the AI compares options or inputs that do not have one perfect match.

✅ Identify the best guess steps: For each unclear spot, check if the system uses a confidence score. For example, ask if the system is 85 percent sure. These are the points where the AI makes a final choice.

✅ Examine the choice: For each choice point, figure out the specific internal math or comparison being done. An example is matching a part of a contract to a policy. Another example involves comparing a picture of a broken car to a library of damaged car photos.

Phase 3: Creating the User Experience

✅ Write clear explanations: Create messages for the user that clearly describe the specific internal action happening when the AI makes a choice.

Hint: Ground your messages in concrete reality. If an AI books a meeting with a client at a local cafe, tell the user the system is checking the cafe reservation system.

✅ Update the screen: Put these new, clear explanations into the user interface. Replace vague messages like Reviewing contracts with your specific explanations.

✅ Check for Trust: Make sure the new screen messages give users a simple reason for any wait time or result. This should make them feel confident and trusting.

Hint: Test these messages with actual users to verify they understand the specific outcome being achieved.

ChromeFlash

I built a Chrome extension to track where Chrome’s RAM actually goes.
Chrome uses a lot of memory. We all know this. But when I actually tried to figure out which tabs were eating my RAM, I realized Chrome doesn’t make it easy.

Task Manager gives you raw process IDs. chrome://memory-internals is a wall of text. Neither tells you “your 12 active tabs are using ~960 MB and your 2 YouTube tabs are using ~300 MB.”

So I built ChromeFlash — a Manifest V3 extension that estimates Chrome’s memory by category and gives you tools to reclaim it.

What it looks like

The popup shows a breakdown of Chrome’s estimated RAM:

  • Browser Core — ~250 MB for Chrome’s internal processes
  • Active Tabs — ~80 MB each
  • Pinned Tabs — ~50 MB each (lighter footprint)
  • Media Tabs — ~150 MB each (audio/video)
  • Suspended Tabs — ~1 MB each
  • Extensions — estimated overhead

A stacked color bar visualizes the proportions at a glance.

The honest caveat

Chrome’s extension APIs in Manifest V3 don’t expose per-tab memory. The chrome.processes API exists but is limited to dev channel. So these are estimates based on real-world averages — not exact measurements.

If you know a better approach, I’d genuinely love to hear it.

Tab suspension

The biggest win. Calling chrome.tabs.discard() on an inactive tab drops it from ~80 MB to ~1 MB. The tab stays in your tab bar, and when you click it, Chrome reloads it.

ChromeFlash lets you:

  • Suspend inactive tabs manually or on a timer (1–120 min)
  • Protect pinned tabs and tabs playing audio
  • Detect and close duplicate tabs

The auto-suspend runs via chrome.alarms since MV3 service workers can’t use setInterval.

// The core of tab suspension
chrome.alarms.create('tab-audit', { periodInMinutes: 5 });

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'tab-audit') {
    const tabs = await chrome.tabs.query({});
    for (const tab of tabs) {
      if (shouldDiscard(tab)) {
        await chrome.tabs.discard(tab.id);
      }
    }
  }
});

Optimization profiles

Four presets that configure tab suspension + Chrome settings in one click:

Profile Suspend Services Off Est. RAM Saved
Gaming 1 min 5 (DNS, spell, translate, autofill, search) ~500–2000 MB
Productivity 15 min 0 ~200–600 MB
Battery Saver 5 min 4 (DNS, spell, translate, search) ~400–1500 MB
Privacy 30 min 7 (+ Topics, FLEDGE, Do Not Track ON) ~150–400 MB

Each profile shows the exact numbers and what changes — no vague “optimizes your browser” marketing.

Hidden settings via chrome.privacy

Chrome exposes several settings through the chrome.privacy API that most users never touch:

// Toggle DNS prefetching
chrome.privacy.network.networkPredictionEnabled.set({ value: false });

// Disable cloud spell check
chrome.privacy.services.spellingServiceEnabled.set({ value: false });

// Disable Topics API (ad tracking)
chrome.privacy.websites.topicsEnabled.set({ value: false });

// Disable FLEDGE / Protected Audiences
chrome.privacy.websites.fledgeEnabled.set({ value: false });

The extension exposes 8 of these as toggle switches. Disabling background services like spell check and translation reduces both network calls and CPU usage — not dramatically, but it adds up.

Chrome Flags guide

Chrome flags (chrome://flags) can meaningfully improve performance, but extensions can’t modify them programmatically — Chrome blocks this for security reasons.

So ChromeFlash includes a curated database of 21 performance-relevant flags:

  • Rendering — GPU Rasterization, Zero-Copy, Skia Graphite
  • Network — QUIC Protocol, WebSocket over HTTP/2
  • Memory — Automatic Tab Discarding, High Efficiency Mode
  • JavaScript — V8 Sparkplug, V8 Maglev compilers
  • Loading — Back/Forward Cache, Parallel Downloads

Each flag shows a risk level, impact rating, and a button that opens it directly in chrome://flags/#flag-name.

Architecture

ChromeFlash/
  manifest.json
  src/
    background/service-worker.js    # Alarms, tab audit, memory pressure
    modules/
      tab-manager.js                # Suspend, discard, duplicates
      memory-optimizer.js           # Chrome RAM breakdown
      network-optimizer.js          # DNS prefetch toggle
      privacy-optimizer.js          # 8 privacy setting toggles
      performance-monitor.js        # CPU/memory stats, score
      profiles.js                   # 4 profiles with detailed stats
      flags-database.js             # 21 curated flags
      settings.js / storage.js      # Persistence
    popup/                          # Main UI
    pages/                          # Dashboard + Flags Guide

No build step. No framework. No bundler. ES modules loaded natively by Chrome. The entire extension is under 50 KB.

What I learned

MV3 service workers are stateless. Every alarm fires into a fresh context. You can’t store state in module-level variables — it has to go in chrome.storage. This tripped me up early.

chrome.tabs.discard() is underrated. It’s the single highest-impact thing an extension can do for memory. 85–92% reduction per tab with zero user friction — the tab just reloads when you click it.

chrome.privacy is powerful but underdiscovered. Most developers don’t know you can programmatically toggle DNS prefetching, Topics API, or FLEDGE from an extension. The API surface is small but useful.

Flags can’t be automated. I spent time looking for workarounds before accepting that chrome://flags is intentionally walled off. The guide approach works well enough.

Try it

ChromeFlash is free, open source, and collects zero data. No analytics, no remote servers, no tracking. Everything stays in chrome.storage.local

The Google Play 12-Tester Wall: A Solo Dev’s Guide (and a Plea for Help)

The Problem Every Solo Dev Hits

You build your app. You polish it. You upload it to Google Play Console. And then… you discover the 12-tester requirement.

Google requires new developer accounts to have at least 12 unique testers opted into your internal testing track for 14 continuous days before you can publish to production. For big companies with QA teams, this is nothing. For solo devs? It’s a wall.

I’m currently stuck behind that wall with two apps and just 1 tester (myself). I’ve been stuck for two weeks.

What I Built

FocusForge 🎯

A minimal focus timer. No account required, no cloud sync, no ads. Just a clean Pomodoro-style timer that tracks your deep work sessions locally. I built it because every other focus app wanted me to create an account and pay $5/month for what is essentially a countdown timer.

NoiseLog 🔊

Measures and logs ambient noise levels using your phone’s microphone. I originally built this to document a noisy neighbor situation, but it turned out useful for:

  • Finding the quietest seat in coffee shops
  • Tracking if construction noise exceeds legal limits
  • Understanding your daily noise exposure patterns

Both apps are free, no ads, no data collection, no weird permissions.

What I’m Asking

If you have an Android phone (any recent version), joining takes about 60 seconds:

  1. Click one of the internal testing links below
  2. Sign in with your Google account
  3. Install the app from the Play Store page
  4. Keep it installed for ~14 days

That’s it. You don’t even need to actively use it (though feedback is welcome). You’re just helping me get past Google’s gatekeeper.

Join links:

  • FocusForge internal test
  • NoiseLog internal test

Tips for Other Devs in the Same Boat

If you’re also stuck at the 12-tester wall, here’s what I’ve learned:

  1. Start recruiting testers early — don’t wait until your app is “ready.” The 14-day clock only starts when people opt in.
  2. Family and friends are your fastest path — but many won’t have Android.
  3. r/betatesting and r/playmyapp on Reddit are purpose-built for this.
  4. FeatureGate.de is a free mutual-testing platform — test 3 apps, earn the right to post your own.
  5. TestersCommunity.com charges $15 for 25 testers with a production access guarantee if you want the fast track.
  6. Dev.to and IndieHackers — you’re reading this, so you know these communities exist. Post your story.

The requirement exists to filter spam, and I get that. But it’s one of those things where the cure is worse than the disease for legitimate solo devs.

Help a Dev Out

If you joined one of the tests above: thank you. Seriously. Every tester gets me one step closer to actually shipping these apps.

If you’ve been through the same struggle, drop a comment — I’d love to hear how you solved it.

Building a database to understand databases

Databases always felt like a black box to me. You call INSERT, data goes in. You call SELECT, data comes back out. Something crashes, and somehow your data is still there. I wanted to know how all of that actually works.

InterchangeDB is a database I’m building from scratch in Rust to learn how each subsystem works by implementing it myself. The project plan has been heavily inspired by CMU BusTub, mini-lsm, and ToyDB. The internals are interchangeable. Different components can be swapped in and out so I can see how they compare directly, running the same stress tests against different combinations of components on the same data.

Right now there are two storage engines behind a generic trait.

The B+Tree sits on top of a buffer pool manager that handles reading and writing pages to disk. The buffer pool has six cache eviction policies (FIFO, Clock, LRU, LRU-K, 2Q, and ARC) that can be hot-swapped at runtime. I benchmarked all six across five different workload patterns and the results were not what I expected. More on that soon.

The LSM-Tree writes go to a memtable first, then flush to sorted string tables on disk. Bloom filters cut down unnecessary reads. I ran head-to-head benchmarks between the two engines on identical workloads. The write performance gap was orders of magnitude larger than I anticipated, and the read gap was surprisingly small. More on that soon too.

Both engines are swappable at compile time through Rust generics. Same test suite, same benchmarks, same data, different engine underneath.

Underneath the engines there’s a write-ahead log with checkpointing, crash recovery, a lock manager, deadlock detection, and strict two-phase locking. The database is ACID today for single-version concurrency.

The next step is MVCC so readers never block writers. After that, garbage collection for old versions, and a verification phase of crash recovery and concurrency stress tests. The end goal is a working database where I know the ins and outs of every subsystem, what real databases use which components, and why.

Check out the project here: InterchangeDB

I’m currently looking for roles in databases, data infrastructure, and search. If your team is building in this space, I’d love to talk.

Context Switching or Multitasking as a human

Ever catch yourself trying to juggle writing code, checking messages and reading documentation all at once? We call it multitasking but actually our brains just like CPUs doing “context switching.” 🧠💻

In Operating Systems, a single CPU core can technically only execute one process at a time. To give the illusion that all our apps are running simultaneously, the OS rapidly pauses one process and starts another. But this jump isn’t free.

Before the CPU can switch tasks, it has to perform a Context Switch.
Here is how:
🔹 The Pause: The currently running process is halted.
🔹 The Save: The OS takes a “snapshot” of the process’s state (CPU registers, program counter, memory limits) and saves it into a data structure called the Process Control Block (PCB). The PCB is like a highly detailed bookmark.
🔹 The Load: The OS then grabs the PCB of the next process in line, reloads its saved state into the hardware and execution resumes exactly where it left off.

𝐎𝐯𝐞𝐫𝐡𝐞𝐚𝐝
Context switching is pure overhead. While the OS is busy saving and loading PCBs, absolutely no user work is getting done. OS designers put massive amounts of effort optimizing this because every microsecond wasted on a switch is a microsecond lost from actual execution.

Next time you lose 15 minutes of focus because you switched tabs to scroll linkedin just remember, we humans have a pretty expensive context switch overhead too! 😅

ComputerScience #OperatingSystems #ContextSwitching #EmbeddedEngineering #TechConcepts #CProgramming

Part 2 – Connecting AI Agents to Microservices with MCP

Connecting AI Agents to Microservices with MCP (No Custom SDKs)

In the previous post, I showed how LangChain4j lets you build agents with a Java interface and a couple of annotations. But those agents were using @Tool, methods defined in the same JVM. Fine for a monolith, but I’m running 5 microservices.

I needed the AI agent in service A to call business logic in service B, C, D, and E. Without writing bespoke HTTP clients for each one.

That’s where MCP comes in, and it changed how I think about exposing business logic.

The Problem: @Tool Doesn’t Scale Across Services

In my saga orchestration system, I have:

  • order-service (port 3000): MongoDB, manages orders and events
  • product-validation-service (port 8090): PostgreSQL, validates catalog
  • payment-service (port 8091): PostgreSQL, handles payments and fraud scoring
  • inventory-service (port 8092): PostgreSQL, manages stock
  • orchestrator (port 8050): coordinates the saga via Kafka

And then there’s the ai-saga-agent (port 8099), the service that hosts my AI agents. It needs to query data from ALL other services.

With @Tool, I’d have to write HTTP clients and DTOs for each service. Error handling, retry logic, the whole nine yards. Every time a service adds a new capability, I’d update the agent’s code. Tight coupling everywhere.

MCP: One Protocol for Everything

MCP (Model Context Protocol) is basically USB for AI. Instead of writing custom integrations per service, you expose tools via a standard JSON-RPC protocol over HTTP/SSE. Any agent can connect, discover available tools, and call them.

The before/after in my codebase was dramatic.

Before (without MCP): Agent needs stock data, write InventoryHttpClient. Agent needs payment status, write PaymentHttpClient. Agent needs order details, write OrderHttpClient. New tool in inventory? Update the client, update the agent.

After (with MCP): Each service exposes an MCP server. Agent connects to http://localhost:8092/sse and automatically discovers getStockByProduct, getLowStockAlert, checkReservationExists. New tool? Just add it to the MCP server. The agent sees it on next connection.

Making a Microservice an MCP Server

Let me show you the actual code from my payment-service. It already had a PaymentService and a FraudValidationService, real business logic with database queries. I just needed to expose some of those methods as MCP tools.

Add the Dependency

implementation 'io.modelcontextprotocol.sdk:mcp:0.9.0'

Set Up the Transport

@Bean
public HttpServletSseServerTransportProvider mcpTransport() {
    return HttpServletSseServerTransportProvider.builder()
        .objectMapper(new ObjectMapper())
        .messageEndpoint("/mcp/message")
        .build();
}

@Bean
public ServletRegistrationBean<HttpServletSseServerTransportProvider> mcpServlet(
        HttpServletSseServerTransportProvider transport) {
    return new ServletRegistrationBean<>(transport, "/sse", "/mcp/message");
}

Register Your Tools

Here’s the key part. I’m reusing the same PaymentService and FraudValidationService beans that already exist:

@Bean
public McpSyncServer mcpServer(
        HttpServletSseServerTransportProvider transport,
        PaymentService paymentService,
        FraudValidationService fraudService) {

    return McpServer.sync(transport)
        .serverInfo("payment-mcp", "1.0.0")
        .capabilities(ServerCapabilities.builder().tools(true).build())
        .tools(
            getPaymentStatus(paymentService),
            getRefundRate(paymentService),
            getFraudRiskScore(fraudService)  // same business logic, now via MCP
        )
        .build();
}

Each tool needs four things. A name and description so the LLM understands what it does. A JSON schema for parameters. And a handler function that runs your actual business logic:

private SyncToolSpecification getPaymentStatus(PaymentService paymentService) {
    return tool(
        "getPaymentStatus",
        "Returns the current payment status for a given transaction. " +
        "Use to verify whether a payment was processed, pending, or refunded.",
        """
        {
          "type": "object",
          "properties": {
            "transactionId": {
              "type": "string",
              "description": "Transaction ID associated with the saga"
            }
          },
          "required": ["transactionId"]
        }
        """,
        args -> {
            String txId = (String) args.get("transactionId");
            return paymentService.findByTransactionId(txId)
                .map(p -> "status=" + p.getStatus()
                    + " | totalAmount=" + p.getTotalAmount()
                    + " | totalItems=" + p.getTotalItems())
                .orElse("No payment found for transactionId=" + txId);
        }
    );
}

Notice: no new code. The paymentService.findByTransactionId() method already existed. I’m just wrapping it with a description so the LLM knows when to call it.

What Each Service Exposes

I did this for all 4 services:

Service MCP Tools
order-service getOrderById, listRecentEvents, getLastEventByOrder
payment-service getPaymentStatus, getRefundRate, getFraudRiskScore
inventory-service getStockByProduct, getLowStockAlert, checkReservationExists
product-validation checkProductExists, checkValidationExists, listCatalog

Each service keeps full ownership of its data. The MCP layer is just a thin exposure.

The Agent Side: Connecting as an MCP Client

Now on the ai-saga-agent, I connect to all these servers:

@Bean
public McpToolProvider mcpToolProvider() {
    return McpToolProvider.builder()
        .mcpClients(List.of(
            buildClient("http://localhost:3000/sse"),     // order
            buildClient("http://localhost:8091/sse"),     // payment
            buildClient("http://localhost:8092/sse"),     // inventory
            buildClient("http://localhost:8090/sse")      // product-validation
        ))
        .build();
}

private McpClient buildClient(String sseUrl) {
    return new DefaultMcpClient.Builder()
        .transport(new HttpMcpTransport.Builder()
            .sseUrl(sseUrl)
            .build())
        .build();
}

Then when I build an agent, I just pass the mcpToolProvider:

DataAnalystAgent agent = AiServices.builder(DataAnalystAgent.class)
    .chatModel(gemini)
    .toolProvider(mcpToolProvider)   // discovers tools from all 4 services
    .build();

That’s it. The agent now has access to 12+ tools across 4 services, without a single HTTP client written by hand.

The Saga Architecture (Quick Context)

For those not familiar with the Saga Pattern: it’s how you handle distributed transactions without two-phase commit. Instead of one big transaction, you have a chain of local transactions. If any step fails, you run compensating transactions to undo the previous steps.

My flow looks like this:

Order Service → Orchestrator → Product Validation → Payment → Inventory → Success
                                    ↑                  ↑          ↑
                                    └──── Rollback ←───┴──────────┘

Everything communicates via Kafka topics. The orchestrator listens for results and decides what to publish next. There’s a state transition table that maps (source, status) to the next topic:

Source Status → Next Topic
ORCHESTRATOR SUCCESS product-validation-success
PRODUCT_VALIDATION SUCCESS payment-success
PAYMENT SUCCESS inventory-success
INVENTORY SUCCESS finish-success
INVENTORY FAIL payment-fail (rollback)
PAYMENT FAIL product-validation-fail (rollback)

The beauty of this setup is that the saga flow is deterministic and auditable. Every event is stored, every transition is logged.

@Tool vs MCP Tool: When to Use Each

After building this, my rule of thumb is simple:

Use @Tool when the logic lives in the same JVM as the agent. No network overhead, tightly coupled, only that agent can use it.

Use MCP when the logic lives in another service. Any agent can connect. The protocol is language-agnostic (just JSON-RPC), and adding new tools doesn’t require changes on the agent side.

In practice, my agents use MCP for everything. The only @Tool I still use is for utility functions that don’t belong in any microservice, like formatting helpers or date calculations.

Testing MCP Endpoints Manually

You can test MCP without an AI agent. It’s just HTTP:

# 1. Open an SSE session
curl http://localhost:8092/sse
# Returns a sessionId

# 2. List available tools
curl -X POST "http://localhost:8092/mcp/message?sessionId=YOUR_SESSION" 
  -H "Content-Type: application/json" 
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# 3. Call a tool
curl -X POST "http://localhost:8092/mcp/message?sessionId=YOUR_SESSION" 
  -H "Content-Type: application/json" 
  -d '{
    "jsonrpc":"2.0","id":3,"method":"tools/call",
    "params":{"name":"getStockByProduct","arguments":{"productCode":"COMIC_BOOKS"}}
  }'

This is super useful for debugging. When an agent does something unexpected, I test the tool directly to check if it’s the tool or the prompt that’s wrong.

What’s Next

With MCP in place, the infrastructure was ready. But the interesting part is what the agents actually do with all these tools. In the next post, I’ll walk through the 3 agents I built. The OperationsAgent listens for failed sagas on Kafka and auto-diagnoses them using RAG. The SagaComposerAgent periodically rewrites the saga execution plan based on real failure data. And the DataAnalystAgent answers natural language questions like “list the 5 most recent failed sagas and assess their fraud risk.”

The code is all open source: github.com/pedrop3/sagaorchestration

This is part 2 of a 3-part series on integrating AI into a distributed saga system:

  1. Part 1 – Why I Picked LangChain4j Over Spring AI
  2. Part 2 – Connecting AI Agents to Microservices with MCP
  3. Part – Agents That Diagnose, Plan, and Query a Distributed Saga

How to Train Your First TensorFlow Model in PyCharm

This is a guest post from Iulia Feroli, founder of the Back To Engineering community on YouTube.

How to Train Your First TensorFlow Model in PyCharm

TensorFlow is a powerful open-source framework for building machine learning and deep learning systems. At its core, it works with tensors (a.k.a multi‑dimensional arrays) and provides high‑level libraries (like Keras) that make it easy to transform raw data into models you can train, evaluate, and deploy.

TensorFlow helps you handle the full pipeline: loading and preprocessing data, assembling models from layers and activations, training with optimizers and loss functions, and exporting for serving or even running on edge devices (including lightweight TensorFlow Lite models on Raspberry Pi and other microcontrollers). 

If you want to make data-driven applications, prototyping neural networks, or ship models to production or devices, learning TensorFlow gives you a consistent, well-supported toolkit to go from idea to deployment.

If you’re brand new to TensorFlow, start by watching the short overview video where I explain tensors, neural networks, layers, and why TensorFlow is great for taking data → model → deployment, and how all of this can be explained with a LEGO-style pieces sorting example. 

In this blog post, I’ll walk you through a first, stripped-down TensorFlow implementation notebook so we can get started with some practical experience. You can also watch the walkthrough video to follow along.

We’ll be exploring a very simple use case today: load the Fashion MNIST dataset, build two very simple Keras models, train and compare them, then dig into visualizations (predictions, confidence bars, confusion matrix). I kept the code minimal and readable so you can focus on the ideas – and you’ll see how PyCharm helps along the way.

Training TensorFlow models step by step

Getting started in PyCharm

We’ll be leveraging PyCharm’s native Notebook integration to build out our project. This way, we can inspect each step of the pipeline and use some supporting visualization along the way. We’ll create a new project and generate a virtual environment to manage our dependencies. 

If you’re running the code from the attached repo, you can install directly from the requirements file. If you wish to expand this example with additional visualizations for further models, you can easily add more packages to your requirements as you go by using the PyCharm package manager helpers for installing and upgrading.

Load Fashion MNIST and inspect the data

Fashion MNIST is a great starter because the images are small (28×28 pixels), visually meaningful, and easy to interpret. They represent various garment types as pixelated black-and-white images, and provide the relevant labels for a well-contained classification task. We can first take a look at our data sample by printing some of these images with various matplotlib functions:

Load Fashion MNIST and inspect the data
```
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i, ax in enumerate(axes.flat):
    ax.imshow(x_train[i], cmap='gray')
    ax.set_title(class_names[y_train[i]])
    ax.axis('off')
plt.show()
```
# Two simple models (a quick experiment)
```
model1 = models.Sequential([
    layers.Flatten(input_shape=(28, 28)),
    layers.Dense(128, activation='relu'),
    layers.Dense(10, activation='softmax')
])
model2 = models.Sequential([
    layers.Flatten(input_shape=(28, 28)),
    layers.Dense(128, activation='relu'),
    layers.Dense(128, activation='relu'),
    layers.Dense(10, activation='softmax')
])
```

Compile and train your first model

From here, we can compile and train our first TensorFlow model(s). With PyCharm’s code completion features and documentation access, you can get instant suggestions for building out these simple code blocks.

For a first try at TensorFlow, this allows us to spin up a working model with just a few presses of Tab in our IDE. We’re using the recommended standard optimizer and loss function, and we’re tracking for accuracy. We can choose to build multiple models by playing around with the number or type of layers, along with the other parameters. 

```
model1.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)
model1.fit(x_train, y_train, epochs=10)
model2.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)
model2.fit(x_train, y_train, epochs=15)
```

Evaluate and compare your TensorFlow model performance

```
loss1, accuracy1 = model1.evaluate(x_test, y_test)
print(f'Accuracy of model1: {accuracy1:.2f}')
loss2, accuracy2 = model2.evaluate(x_test, y_test)
print(f'Accuracy of model2: {accuracy2:.2f}')
```

Once the models are trained (and you can see the epochs progressing visually as each cell is run), we can immediately evaluate the performance of the models.

In my experiment, model1 sits around ~0.88 accuracy, and while model2 is a little higher than that, it took 50% longer to train. That’s the kind of trade‑off you should be thinking about: Is a tiny accuracy gain worth the additional compute and complexity? 

We can dive further into the results of the model run by generating a DataFrame instance of our new prediction dataset. Here we can also leverage built-in functions like `describe` to quickly get some initial statistical impressions:

Evaluate and compare your TensorFlow model performance
```
predictions = model1.predict(x_test)
import pandas as pd
df_pred = pd.DataFrame(predictions, columns=class_names)
df_pred.describe()
```

However, the most useful statistics will compare our model’s prediction with the ground truth “real” labels of our dataset. We can also break this down by item category:

```
y_pred = model1.predict(x_test).argmax(axis=1)
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(8,6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
print('Classification report:')
print(classification_report(y_test, y_pred, target_names=class_names))
```

From here, we can notice that the accuracy differs quite a bit by type of garment. A possible interpretation of this is that trousers are quite a distinct type of clothing from, say, t-shirts and shirts, which can be more commonly confused. 

This is, of course, the type of nuance that, as humans, we can pick up by looking at the images, but the model only has access to a matrix of pixel values. The data does seem, however, to confirm our intuition. We can further build a more comprehensive visualization to test this hypothesis. 

```
import numpy as np
import matplotlib.pyplot as plt
# pick 8 wrong examples
y_pred = predictions.argmax(axis=1)
wrong_idx = np.where(y_pred != y_test)[0][:8]  # first 8 mistakes
n = len(wrong_idx)
fig, axes = plt.subplots(n, 2, figsize=(10, 2.2 * n), constrained_layout=True)
for row, idx in enumerate(wrong_idx):
    p = predictions[idx]
    pred = int(np.argmax(p))
    true = int(y_test[idx])
    axes[row, 0].imshow(x_test[idx], cmap="gray")
    axes[row, 0].axis("off")
    axes[row, 0].set_title(
        f"WRONG  P:{class_names[pred]} ({p[pred]:.2f})  T:{class_names[true]}",
        color="red",
        fontsize=10
    )
    bars = axes[row, 1].bar(range(len(class_names)), p, color="lightgray")
    bars[pred].set_color("red")
    axes[row, 1].set_ylim(0, 1)
    axes[row, 1].set_xticks(range(len(class_names)))
    axes[row, 1].set_xticklabels(class_names, rotation=90, fontsize=8)
    axes[row, 1].set_ylabel("conf", fontsize=9)
plt.show()
```

This table generates a view where we can explore the confidence our model had in a prediction: By exploring which weight each class was given, we can see where there was doubt (i.e. multiple classes with a higher weight) versus when the model was certain (only one guess). These examples further confirm our intuition: top-types appear to be more commonly confused by the model. 

Conclusion

And there we have it! We were able to set up and train our first model and already drive some data science insights from our data and model results. Using some of the PyCharm functionalities at this point can speed up the experimentation process by providing access to our documentation and applying code completion directly in the cells. We can even use AI Assistant to help generate some of the graphs we’ll need to further evaluate the TensorFlow model performance and investigate our results.

You can try out this notebook yourself, or better yet, try to generate it with these same tools for a more hands-on learning experience.

Where to go next

This notebook is a minimal, teachable starting point. Here are some practical next steps to try afterwards:

  • Replace the dense baseline with a small CNN (Conv2D → MaxPooling → Dense).
  • Add dropout or batch normalization to reduce overfitting.
  • Apply data augmentation (random shifts/rotations) to improve generalization.
  • Use callbacks like EarlyStopping and ModelCheckpoint so training is efficient, and you keep the best weights.
  • Export a SavedModel for server use or convert to TensorFlow Lite for edge devices (Raspberry Pi, microcontrollers).

Frequently asked questions

When should I use TensorFlow?

TensorFlow is best used when building machine learning or deep learning models that need to scale, go into production, or run across different environments (cloud, mobile, edge devices). 

TensorFlow is particularly well-suited for large-scale models and neural networks, including scenarios where you need strong deployment support (TensorFlow Serving, TensorFlow Lite). For research prototypes, TensorFlow is viable, but it’s more commonplace to use lightweight frameworks for easier experimentation.

Can TensorFlow run on a GPU?

Yes, TensorFlow can run GPUs and TPUs. Additionally, using a GPU can significantly speed up training, especially for deep learning models with large datasets. The best part is, TensorFlow will automatically use an available GPU if it’s properly configured.

What is loss in TensorFlow?

Loss (otherwise known as loss function) measures how far a model’s predictions are from the actual target values. Loss in TensorFlow is a numerical value representing the distance between predictions and actual target values. A few examples include: 

  • MSE (mean squared error), used in regression tasks.
  • Cross-entropy loss, often used in classification tasks.

How many epochs should I use?

There’s no set number of epochs to use, as it depends on your dataset and model. Typical approaches cover: 

  • Starting with a conservative number (10–50 epochs).
  • Monitoring validation loss/accuracy and adjusting based on the results you see.
  • Using early stopping to halt training when improvements decrease.

An epoch is one full pass through your training data. Not enough passes through leads to underfitting, and too many can cause overfitting. The sweet spot is where your model generalizes best to unseen data. 

About the author

Iulia Feroli

Iulia’s mission is to make tech exciting, understandable, and accessible to the new generation.

With a background spanning data science, AI, cloud architecture, and open source, she brings a unique perspective on bridging technical depth with approachability.

She’s building her own brand, Back To Engineering, through which she creates a community for tech enthusiasts, engineers, and makers. From YouTube videos on building robots from scratch, to conference talks or keynotes about real, grounded AI, and technical blogs and tutorials Iulia shares her message worldwide on how to turn complex concepts into tools developers can use every day.

Adversarial AI: Understanding the Threats to Modern AI Systems

Adversarial AI.

Whether you’re in “fan” or “fear” mode (or somewhere in-between), there’s no denying that Artificial Intelligence changed how we build products, and do business. We’ve always had cybersecurity threats but now the landscape is more complex and we have to consider new forms of fraud detection, customer support, autonomous systems and generative AI hazards.

Plus, as AI capabilities grow, so do the threats targeting them. One of the most critical emerging risk areas is adversarial AI – the use of malicious techniques to exploit, manipulate, and/or compromise AI systems.

Understanding adversarial AI is important for protecting the integrity, reliability, and security of our “AI-powered” products – a phrase we’re all too familiar with. Why? Because, these threats can directly impact business outcomes, leading to financial losses, reputational damage, and the absolute erosion of customer trust, which we’re already seeing.

In this article, we introduce adversarial AI, explore its two primary forms, and outline the main attack surfaces that organizations and software development teams must secure.

Table of Contents

The two faces of adversarial AI

Adversarial AI threats generally fall into two broad categories.

1. AI used as a weapon

In the first category, attackers use AI itself to amplify malicious activities. These include:

  • Deepfake generation: Creating realistic fake images, videos, or audio to spread misinformation, commit fraud, or damage reputations.
  • Automated phishing: Using AI to craft highly personalized phishing emails at scale, increasing success rates while reducing attacker effort.
  • AI-generated malware: Developing malware that can identify vulnerabilities and adapt faster than traditional attack techniques.

These attacks aren’t theoretical. They’re already being used to bypass defenses, deceive users, and exploit organizations – often at unprecedented speed and scale. We’ll show you some examples further on but it can be as alarming as it sounds.

2. Attacks targeting AI systems directly

The second category focuses on attacking AI models and systems themselves. These attacks are especially dangerous because they undermine how AI makes decisions, potentially leading to misleading outputs, biased behavior, or unsafe actions.

For organizations relying on AI-driven decisions, compromised models can quietly introduce systemic risk, often without obvious signs until there’s significant damage.

Where attackers focus their efforts

When targeting AI systems, adversaries typically concentrate on three main areas.

1. Attacks on AI algorithms

These attacks target the core learning and decision-making mechanisms of AI systems. By interfering with how models are trained or how they interpret inputs, attackers can influence predictions and outcomes.

This category includes some of the most impactful adversarial techniques, which we explore in detail later in this article.

2. Attacks on generative AI filters

Generative AI systems rely on filters and safeguards to prevent people misusing them – like content moderation filters or information that identifies people personally (email addresses), etc. Attackers exploit weaknesses in these controls using techniques like prompt injection or code injection, helping them get past restrictions.

These filters are applied during input and output and unfortunately provide plenty of opportunity for attackers to get creative to get and use this sensitive information.

When successful, these attacks help adversaries generate harmful content, and/or execute actions that aren’t intended – often leaving the user none the wiser until it’s too late.

Adversarial AI attacks.

3. Supply chain attacks on AI artifacts

AI systems depend a lot on third-party components, including datasets, pre-trained models, APIs, and open-source libraries. Supply chain attacks target these dependencies.

For example, an attacker may compromise an open-source library that’s used during model training or embed malicious code into a dataset. Once it’s already integrated, the compromised component can enable unauthorized access, data exfiltration, or system disruption.

Because these attacks exploit trusted dependencies, they’re especially difficult to detect and can have far-reaching consequences.

Attacks on AI algorithms: real-world examples

Attacks on AI algorithms strike at the foundation of AI systems. When successful, they can cause models to behave incorrectly, unpredictably, or maliciously. Three attack types dominate this category: data poisoning, evasion attacks, and model theft.

Data poisoning attacks

Data poisoning occurs during the training phase of an AI model. Attackers manipulate training data to corrupt the model’s learning process, causing it to internalize false or harmful patterns.

For example, consider a fraud detection model trained to identify suspicious transactions. If an attacker gains access to the training pipeline, they could inject fraudulent transactions labeled as legitimate. As a result, the model becomes less effective at detecting real fraud, exposing the organization to financial risk.

A well-known real-world example is Microsoft’s Tay chatbot, launched in 2016. Tay learned directly from user interactions on Twitter (if you’re a Millennial) or X (If you’re a Gen-Z), and was quickly manipulated into producing offensive and harmful content. This incident highlighted the risks of unmonitored data pipelines and insufficient safeguards during training.

Search engine manipulation offers another example, where poisoned data has been used to surface false information, which breaks user trust in AI-driven systems.

Adversarial AI threats.

Evasion attacks

Evasion attacks occur after a model has been deployed. Instead of modifying the model, attackers subtly manipulate inputs to cause incorrect predictions.

In fraud detection, this might involve changing spending behavior just enough to avoid triggering alerts, such as breaking large transactions into smaller ones. Each transaction appears legitimate in isolation, allowing fraud to go undetected.

Evasion attacks have also been demonstrated in autonomous driving systems. Researchers have shown that placing small stickers on a stop sign can cause a self-driving car to misinterpret it as a speed limit sign. These changes are often barely noticeable, or even not noticeable at all, to humans but sufficient to confuse the model. This could lead to catastrophic outcomes depending on the different use cases.

Similar techniques can bypass facial recognition systems or other biometric controls, enabling unauthorized access and data theft.

Evasion attacks in adversarial AI

Model theft

Model theft involves stealing or replicating an AI model by repeatedly querying it and analyzing its outputs. Over time, attackers can infer the model’s structure, parameters, or even training data, effectively cloning proprietary intellectual property.

In 2019, researchers demonstrated that a commercial AI model could be replicated with approximately 90% accuracy using only its public interface. By observing how the model responded to carefully chosen inputs, they reconstructed its internal behavior.

A more recent example emerged in 2023 with the Alpaca and OpenLLaMA projects. Attackers queried Meta’s Llama model extensively and analyzed its outputs to reverse-engineer its functionality. This process enabled them to create Alpaca, a model that closely mimicked LLaMA’s performance without direct access to its source code or training data.

Model theft undermines the competitive advantage of proprietary AI systems and enables adversaries to reuse or resell stolen capabilities.

Why this matters for businesses

Data poisoning, evasion attacks, and model theft all compromise the integrity and reliability of AI systems. For businesses, the consequences can include:

  • Operational disruptions
  • Financial losses
  • Intellectual property theft
  • Regulatory and compliance risks
  • Loss of customer trust

Protecting AI systems requires more than traditional application security. Organizations must design AI with resilience in mind, implementing access controls, monitoring model behavior, validating data pipelines, and securing dependencies throughout the AI supply chain.

What’s next?

Understanding these attack vectors is the first step toward securing AI-powered products. In the next session, we’ll explore attacks on filters in generative AI, where adversaries bypass safeguards to misuse AI capabilities.

By proactively addressing adversarial AI threats, organizations can protect their models, their users, and their business outcomes.

As Qodana continues to develop for a new age of security and quality threats, we’re releasing new features that help protect your codebase so you can focus on quality and debt. Speak to the team to find out how we can help.

Get A Qodana Demo