Securing AI Access to Financial Data: How We Govern Bitcoin MCP with agentgateway

Securing AI Access to Financial Data: How We Govern Bitcoin MCP with agentgateway

What happens when an AI agent has a tool called send_raw_transaction?

That’s not a hypothetical. bitcoin-mcp is a Model Context Protocol server that gives AI agents access to 49 tools for querying the Bitcoin network — block analysis, fee estimation, mempool inspection, address lookups, transaction decoding, and yes, broadcasting signed transactions to the network.

For a data analyst building a dashboard, these tools are a goldmine. For a production deployment without guardrails, they’re an open door.

We built Bitcoin Gateway Guard to close that door and hand out keys only to the people who should have them.

The Threat Model

AI agents are not users. They don’t have judgment. They do exactly what their prompt says, and prompts can be injected, manipulated, or just poorly written. Consider what goes wrong when an AI agent has unrestricted access to Bitcoin tools:

Accidental broadcast. A developer testing transaction construction accidentally calls send_raw_transaction instead of decode_raw_transaction. The transaction is irreversible.

Prompt injection. A malicious input tricks the agent into calling generate_keypair, and the private key appears in a response that gets logged, cached, or displayed in a UI.

Runaway loops. A bug in the agent’s reasoning causes it to hammer the Bitcoin node with thousands of requests per minute, degrading service for everyone.

Shadow access. Six months later, nobody can tell you which agents accessed what data, when, or why. Your compliance team is not happy.

These are not exotic attacks. They are the predictable consequences of deploying AI agents on financial infrastructure without the same controls you’d apply to human users.

Four Layers of Defense

Bitcoin Gateway Guard places agentgateway — an open-source MCP security gateway built in Rust — between AI agents and bitcoin-mcp. Every request passes through four independent security layers.

Layer 1: JWT Authentication

No anonymous access. Every request must carry a valid JWT signed with RS256. The gateway validates the signature, issuer (bitcoin-gateway-guard), audience (bitcoin-mcp), and expiration before the request touches any downstream logic.

No token? HTTP 401. Expired token? HTTP 401. Wrong issuer? HTTP 401.

Layer 2: Role-Based Access Control

We define two roles using CEL (Common Expression Language) expressions:

jwt.role == "admin"
jwt.role == "reader" && mcp.tool.name != "send_raw_transaction" && mcp.tool.name != "generate_keypair"

The reader role gets access to 47 read-only tools — everything you need for dashboards, analytics, research, and monitoring. The two tools that can modify state or create sensitive material — send_raw_transaction and generate_keypair — are reserved for admin.

This is a single line of policy. No code. No middleware. No if-else chains buried in application logic. The policy is auditable, declarative, and lives in a YAML config file that can be version-controlled and reviewed.

Layer 3: Rate Limiting

A token bucket algorithm caps every consumer at 10 requests per minute with burst capacity of 10. This is deliberately conservative. A legitimate dashboard refresh needs one request every few seconds. An agent stuck in a loop will hit the limit in under a second.

localRateLimit:
  - maxTokens: 10
    tokensPerFill: 1
    fillInterval: 60s

Rate limiting is the safety net that catches everything else. Even if authentication and authorization both pass, a runaway agent cannot overload your Bitcoin node.

Layer 4: Audit Trail

Every tool invocation generates an OpenTelemetry span exported to Jaeger. Each span records the tool name, the JWT subject (who), the JWT role, the authorization decision (allowed or denied), the HTTP status, and the latency.

Open Jaeger at localhost:16686, select the agentgateway service, and you have a complete, searchable, time-ordered record of every AI agent interaction with your Bitcoin data. Feed it into your SIEM, set up PagerDuty alerts on denied requests, run weekly access reviews.

The Demo

Setup takes two commands:

python scripts/generate_keys.py   # Generate RSA keys + JWT tokens
docker compose up -d               # Start bitcoin-mcp + agentgateway + Jaeger

Then test the security model:

# Reader queries block height -- works fine
python scripts/test_rbac.py --user reader --tool get_block_count
# Result: ALLOWED

# Reader tries to broadcast a transaction -- blocked
python scripts/test_rbac.py --user reader --tool send_raw_transaction
# Result: DENIED (authorization)

# Burst 15 requests -- rate limited after 10
python scripts/test_rbac.py --user reader --tool get_fee_estimates --burst 15
# Summary: 10 allowed, 5 rate-limited

Every one of these interactions shows up in Jaeger with full context. The denied requests are flagged. The rate-limited requests are logged. Nothing is invisible.

Why agentgateway?

We evaluated building custom middleware, wrapping bitcoin-mcp with authentication logic, and using a general-purpose API gateway. agentgateway won because it understands MCP natively.

A generic API gateway can rate-limit HTTP requests and validate JWTs. But it cannot inspect MCP tool names, apply per-tool authorization rules, or trace MCP-specific context. agentgateway speaks the protocol. Its CEL-based authorization rules reference mcp.tool.name directly. No regex on request bodies. No custom plugins. No fragile parsing.

The configuration is 40 lines of YAML. The entire security policy — authentication, authorization, rate limiting, and tracing — is declared in a single file that a security engineer can review in five minutes.

Why This Matters

The AI industry is moving fast toward giving agents access to real-world systems. Financial data. Infrastructure controls. Business operations. The tools are getting more powerful. The guardrails are not keeping up.

MCP is becoming the standard protocol for AI agent tool access. But MCP itself has no built-in security model. There’s no authentication, no authorization, no rate limiting, and no audit trail in the protocol specification. That’s by design — MCP is a transport protocol, not a security framework.

The security has to come from the deployment layer. And it has to be as easy to configure as the tools themselves, or teams will skip it.

Bitcoin Gateway Guard demonstrates that you can add enterprise-grade security to any MCP server — not just bitcoin-mcp — with a single configuration file and zero code changes to the underlying server. The same pattern applies to database MCP servers, cloud infrastructure tools, payment APIs, or anything else you’d hesitate to give an AI agent unrestricted access to.

The question is not whether AI agents should have access to financial tools. They should — the productivity gains are real. The question is whether that access is governed with the same rigor we apply to human users.

With agentgateway, the answer is yes. And you can prove it to your auditors.

Bitcoin Gateway Guard is open source. The full configuration, Docker setup, and test scripts are available at github.com/Bortlesboat/bitcoin-gateway-guard. Built with bitcoin-mcp and agentgateway for the MCP & AI Agents Hackathon — Secure & Govern MCP category.

I Built My First AI Agent That Understands Bitcoin — Here’s How

I Built My First AI Agent That Understands Bitcoin — Here’s How

A beginner’s guide to MCP, agentgateway, and bitcoin-mcp

I wanted to build an AI agent that could answer questions about Bitcoin. Not from a static FAQ — I mean real-time data. Current block height. Mempool congestion. Fee estimates. Mining stats.

I figured this would take days of API wrangling, authentication headaches, and custom plumbing. It took 10 minutes.

Here’s what I learned.

The Problem

AI models are smart, but they’re stuck in the past. Ask Claude or GPT about the current Bitcoin block height and you’ll get a polite “I don’t have access to real-time data.” Fair enough — but what if you could give them that access?

That’s what MCP does.

What is MCP?

Model Context Protocol (MCP) is an open standard that lets AI models call external tools. Think of it like USB for AI: a universal plug that connects any AI client to any data source. An MCP server exposes tools (like “get the current Bitcoin block height”). An MCP client (like Claude Desktop or a custom script) calls those tools. The AI model decides which tools to call based on your question.

The beauty is separation of concerns. The Bitcoin logic lives in the MCP server. The AI logic lives in the client. They speak the same protocol.

What is agentgateway?

agentgateway is a lightweight proxy that sits between your AI client and your MCP servers. Why do you need a proxy? A few reasons:

  • Protocol translation — it can bridge different MCP transports (stdio, SSE, Streamable HTTP)
  • Routing — it can fan out to multiple MCP servers from a single endpoint
  • Security — it’s where you’d add authentication, rate limiting, and audit logging in production

For our purposes, it gives us a clean HTTP endpoint that any MCP client can connect to.

The Stack

Here’s what we’re building:

AI Client  --->  agentgateway (port 3000)  --->  bitcoin-mcp (port 8000)

Three components. Two commands to start. One config file.

Let’s Build It

Step 1: Install bitcoin-mcp

pip install bitcoin-mcp

That’s the entire Bitcoin integration. bitcoin-mcp ships with 49 tools for querying the Bitcoin network, and it works out of the box — no API keys, no local node required. It connects to the free Satoshi API by default.

Step 2: Start bitcoin-mcp

bitcoin-mcp --transport sse --port 8000

This starts an MCP server on port 8000 using Server-Sent Events transport. Leave this terminal running.

Step 3: Get agentgateway

Download the binary from the agentgateway releases page for your platform.

Step 4: Create the Config

Save this as config-sse.yaml:

binds:
- port: 3000
  listeners:
  - routes:
    - policies:
        cors:
          allowOrigins: ["*"]
          allowHeaders: ["*"]
          exposeHeaders: ["Mcp-Session-Id"]
      backends:
      - mcp:
          targets:
          - name: bitcoin-mcp
            sse:
              url: http://localhost:8000/sse

This tells agentgateway to listen on port 3000 and forward everything to bitcoin-mcp.

Step 5: Start agentgateway

./agentgateway -f config-sse.yaml

Step 6: Connect

Open MCP Inspector:

npx @modelcontextprotocol/inspector

Set the transport to Streamable HTTP, enter http://localhost:3000, and click Connect.

Go to the Tools tab. You’ll see 49 tools. Click get_situation_summary and hit Call.

The “Aha” Moment

When I called get_situation_summary for the first time, I got back a wall of structured data: block height, hashrate, difficulty adjustment progress, mempool size, fee tiers, mining pool distribution. Real data. Live from the Bitcoin network.

I didn’t write any Bitcoin code. I didn’t parse any APIs. I didn’t handle any authentication. I installed an MCP server, pointed a proxy at it, and connected.

That’s when MCP clicked for me. The AI doesn’t need to know how Bitcoin works. It just needs to know which tool to call. The MCP server handles everything else.

I tried more tools. get_fee_estimates told me exactly what fee to use for a transaction right now. analyze_mempool showed me the current congestion. get_mining_info gave me hashrate and difficulty data. Each one returned structured, accurate, real-time data.

Then I connected it to Claude Desktop (just a one-line config change) and asked in natural language: “What’s the current state of the Bitcoin mempool?” Claude called the right tools automatically and gave me a clear, conversational summary backed by live data.

What I Learned

MCP is simpler than I expected. I kept looking for the catch — the complex authentication step, the mandatory configuration file with 200 options, the thing that would take an hour to debug. It wasn’t there. The protocol is genuinely simple.

agentgateway solves real problems. Without it, you’d need every AI client to support every MCP transport. With it, you expose one HTTP endpoint and route to any backend. It’s the missing piece between “I have an MCP server” and “any client can use it.”

The tool ecosystem is the killer feature. bitcoin-mcp alone has 49 tools. Imagine stacking multiple MCP servers — weather, databases, APIs, Bitcoin — all accessible through one gateway. That’s where this gets powerful.

Separation of concerns matters. The AI model picks which tools to call. The MCP server implements the tools. The gateway handles routing and security. Each piece does one thing well.

What’s Next

This starter example runs everything locally with no authentication. For production, you’d want:

  • Authentication — agentgateway supports JWT validation and API keys
  • Rate limiting — prevent abuse
  • Audit logging — track what tools are being called and by whom

I’m also looking into publishing bitcoin-mcp to an MCP registry so anyone can discover and use it without knowing the GitHub URL.

Try It Yourself

The full code is in the hackathon repo. Clone it, follow the README, and you’ll have a working Bitcoin AI agent in 10 minutes.

If you’ve been curious about MCP but haven’t tried it yet — this is your on-ramp. The barrier is genuinely low, and the payoff is seeing an AI agent answer questions with real-time data for the first time.

Built with bitcoin-mcp, agentgateway, and an unreasonable enthusiasm for Bitcoin tooling.

11. Bootstrap

BootCamp by Dr.Angela

1. What is Bootstrap?

  • CSS Frameworks: Bootstrap, Foundation, MUI, Tailwind, etc.
  • When to use vs not to use
    • Use: for fast, responsive, mobile-first development
    • Avoid: for very simple projects or highly customized/complex designs
  • How to use Bootstrap (CDN; Content Delivery Network)
    • Official website: https://getbootstrap.com/
    • CSS (inside ) : <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
    • min → minified file (faster loading, no unnecessary spaces)
    • JavaScript (before ) : <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

2. Bootstrap Layout

  • 12-column system : Uses container → row → col(items)
  • Auto fit : Columns automatically adjust based on available space
  • Containers : .container, .container-sm(mobile)/md(iPad)/lg(laptop)/xl(desktop)/xxl(TV)/fluid(full-width)
  • Sized columns
    • ex.
      <div class="container">
      <div class="row">
      <div class="col-2">Hello</div>
      <div class="col-4">Hello</div>
      <div class="col-6">Hello</div>
      </div>
      </div>
  • Breakpoints : <576px, ≥576px, ≥768px, ≥992px, ≥1200px, ≥1400px
    • Multiple breakpoints
    • ex. <div class="col-sm-12 col-md-8 col-lg-4"></div>

3. Bootstrap Components

  • Common components: card, button, carousel, navbar, etc.
  • Navbar : Can include icons using SVG
    • Download and use : ex. <img src="./briefcase.svg" alt="briefcase" height="30">
    • Or copy SVG directly: ex. <svg ~ > ~ </svg>
  • Snippets (Pre-built sections) : Heroes, Features, Footers, …
    • How to use : Inspect → copy → paste → customize
  • Carousel : Use inside a .container for responsive layout
    • ex.
      <div class="container">
      <!-- carousel here -->
      </div>
  • Spacing(ex. mb-2)
  • Structure
    • Property: m (margin), p (padding)
    • Side: t (top), b (bottom), s (start), e (end), x, y
    • Size: 0 / 1 / 2 / 3 / 4 / 5 / auto
  • Dark Mode : ex. <html lang="en" data-bs-theme="dark">
  • Jumbotron-style : Used for highlighting user feedback(Testimonials) or important content

From PyPI to Production: Publishing bitcoin-mcp to agentregistry

From PyPI to Production: Publishing bitcoin-mcp to agentregistry

How we turned a 49-tool Bitcoin MCP server into a one-command deployment for any AI agent.

The MCP Server Discovery Problem

The Model Context Protocol ecosystem is exploding. There are over 100 MCP servers on PyPI and npm right now, covering everything from GitHub to Slack to databases. But here is the uncomfortable truth: finding and deploying them is still painful.

The typical workflow looks like this:

  1. Google “bitcoin mcp server”
  2. Find a GitHub repo (maybe)
  3. Read the README to figure out installation
  4. Determine the transport type (stdio? SSE? streamable HTTP?)
  5. Manually edit your IDE’s MCP configuration file
  6. Restart your IDE and pray it connects

This is 2026. We send agents to browse the web and write code. We should not need 10 minutes of manual config to add a tool.

Enter agentregistry

agentregistry solves this by creating a centralized, searchable registry for MCP servers, skills, and agent blueprints. Think of it as npm for AI agents — you search, you install, you go.

We decided to register bitcoin-mcp as the first Bitcoin-focused entry. Here is how that went.

The Registration Process

What We Had

bitcoin-mcp is a production MCP server for Bitcoin network analysis:

  • 49 tools for mempool analysis, fee estimation, block inspection, transaction decoding, mining insights, and market data
  • 116 tests with full CI coverage
  • Zero configuration — it auto-detects a local Bitcoin Core node or falls back to the free Satoshi API
  • Already published on PyPI as bitcoin-mcp

The server was ready. What it lacked was discoverability.

What We Built

Three pieces make up the agentregistry submission:

1. Server Registration

The straightforward part. We registered bitcoin-mcp as a PyPI-sourced server with stdio transport:

arctl register server 
    --name bitcoin-mcp 
    --source pypi 
    --package bitcoin-mcp 
    --transport stdio 
    --command bitcoin-mcp

Now arctl search bitcoin finds it. arctl install bitcoin-mcp installs it. Done.

2. The Bitcoin Skill

This is where it gets interesting. MCP servers give agents tools. But tools without context are like giving someone a stethoscope without medical school. They can press buttons, but they do not know what the results mean.

We created a Bitcoin Intelligence Skill — a structured knowledge document (SKILL.md) that teaches the agent:

  • When to use get_fee_estimates vs estimate_smart_fee vs get_fee_recommendation
  • How to interpret mempool congestion (50 MB = congested, 100 MB = severely congested)
  • Confirmation thresholds based on transaction value ($1K = 1 conf, $100K = 6+ conf)
  • How to read mining pool rankings and difficulty adjustments
  • Correlation patterns between on-chain data and market movements

The skill turns a generic tool-caller into a Bitcoin analyst.

3. The Agent Blueprint

The agent.yaml ties everything together into a single deployable unit:

name: bitcoin-intelligence
skills:
  - bitcoin-skill
servers:
  - name: bitcoin-mcp
    source: pypi
    package: bitcoin-mcp

One command deploys the complete package: arctl deploy bitcoin-intelligence.

The Demo

Here is what one-command deployment looks like in practice:

$ arctl deploy bitcoin-intelligence
Installing bitcoin-mcp from PyPI... done
Loading bitcoin-skill... done
Configuring IDE... done

Bitcoin Intelligence agent is ready.

Then, in your AI assistant:

You: “Should I send this Bitcoin transaction now or wait?”

Agent: checks mempool (42MB, moderate congestion), gets fee estimates (12 sat/vB for next block), looks at recent block intervals (9.2 min average — slightly fast)

“The mempool is moderately congested at 42MB. Current next-block fee is 12 sat/vB (~$2.40 for a typical transaction). Blocks are coming slightly faster than average, so the mempool should clear over the next hour. If you can wait 30-60 minutes, you will likely see fees drop to 6-8 sat/vB. If it is urgent, 12 sat/vB will get you into the next block.”

That response required three different tools, domain knowledge about fee interpretation, and an understanding of how block timing affects mempool dynamics. The skill makes this possible.

What We Learned

Skills are the missing layer. Everyone talks about MCP tools, but domain knowledge is what separates a useful agent from a button-masher. The skill concept in agentregistry is underexplored and powerful.

Zero-config matters for registries. bitcoin-mcp works without any API keys or node setup because it falls back to a hosted API. This is critical for registry deployments — if arctl install requires a 10-step configuration guide, you have lost the whole point.

Agent blueprints enable composition. The agent.yaml format lets you bundle servers and skills into opinionated packages. Imagine a “DeFi analyst” blueprint that combines bitcoin-mcp, an Ethereum MCP server, and a cross-chain skill. Registries make this composable.

Try It Yourself

pip install arctl
arctl search bitcoin
arctl deploy bitcoin-intelligence

Three commands. Zero to Bitcoin intelligence.

The full source is at github.com/Bortlesboat/hackathon-mcp.

Built for the MCP Hackathon — Explore Agent Registry category.
bitcoin-mcp: PyPI | GitHub
agentregistry: agentregistry.dev

Structured Outputs Are the Contract Your AI Agent Is Missing

Free-text agent responses look great in demos. They fall apart the moment you try to automate anything downstream. Here’s the pattern that fixes it.

Your AI agent just summarized a payer’s response to a benefits verification request. The output reads beautifully:

“The patient’s coverage appears to be active under the Gold Plus plan. Prior authorization is likely required based on the payer’s language, though this isn’t entirely clear. The copay seems to be around $50-75, and there may be a quantity limit of 30 tablets per month.”

Now try writing code that processes that. Does “appears to be active” mean coverage is confirmed? Is prior auth required or not? What’s the copay — $50 or $75? And good luck extracting “30 tablets per month” reliably when the next response says “one month supply” or “qty limit: 30 units.”

This is the problem that structured outputs solve. And once I started treating them as the contract between the LLM and the rest of my system, every other design decision got easier.

The Problem: Beautiful Text, Useless Automation

I’ve built AI agents for regulated workflows in Life Sciences — benefits verification, clinical trial site selection, medical inquiry response. In every case, the agent’s output needed to drive downstream automation: update a record’s status, create follow-up tasks, route exceptions to the right person, trigger the next step in a workflow.

My first implementations all made the same mistake. I let the LLM return natural language summaries and then tried to parse them. Sometimes I wrote regex. Sometimes I added a second LLM call to “extract the fields.” Sometimes I just gave up and had a human re-read the summary and fill in the fields manually.

Every approach was fragile because I was depending on the LLM being consistent in how it phrases things. And consistency is exactly what LLMs don’t guarantee.

The Fix: Define a Schema, Enforce It in the Prompt

A structured output is a JSON object with a defined schema that the LLM must conform to. Instead of asking “summarize this payer response,” you ask “extract the following fields from this payer response and return them as JSON.”

Here’s what the output contract looks like for a benefits verification summary:

{
  "coverageConfirmed": true,
  "priorAuthRequired": false,
  "copayNotes": "$50 copay per fill",
  "deductibleNotes": "$500 annual, not yet met",
  "limitationsNotes": "Specialty pharmacy required",
  "missingInfo": ["Effective date not stated"],
  "confidence": 82
}

Every field has a type. Every field has a purpose. And the downstream logic is trivial:

// This is the entire routing logic. No parsing. No interpretation.
if (summary.missingInfo.length === 0 && summary.confidence >= 75) {
  status = "Verified";
} else {
  status = "Needs Follow-up";
  for (const item of summary.missingInfo) {
    createTask(`Follow up: ${item}`, recordId);
  }
}

Compare that to trying to extract the same decisions from a paragraph of prose. The structured output turned a fragile NLP problem into a simple conditional.

How to Get the LLM to Comply

The schema alone isn’t enough — you need to tell the LLM exactly what you expect. The prompt template has three parts:

1. Role and constraints:

You are a benefits verification analyst. Extract structured data
from the payer response below. Never invent information that isn't
in the response. If a field cannot be determined, use null for
booleans, "Not stated" for strings, and add the missing item to
the missingInfo array.

2. Context (merge fields from your system):

Patient: Jane Doe
Plan: AcmeHealth Gold Plus
Drug: RX-OMNI 10mg
Payer response: [raw text pasted or ingested here]

3. Output schema with field descriptions:

Return ONLY a JSON object matching this schema:
{
  "coverageConfirmed": boolean — is coverage active for this drug/plan?,
  "priorAuthRequired": boolean — does the payer require prior auth?,
  "copayNotes": string — copay amount and terms, or "Not stated",
  "deductibleNotes": string — deductible details, or "Not stated",
  "limitationsNotes": string — quantity limits, step therapy, etc.,
  "missingInfo": [string] — list of fields that couldn't be determined,
  "confidence": number 0-100 — how complete/clear was the payer response?
}

The field descriptions inside the schema are doing heavy lifting. They tell the LLM what each field means, what the valid values are, and what to do when information is missing. Without them, you get inconsistent interpretations — one run puts copay info in limitationsNotes, the next puts it in copayNotes.

If you’re using a platform that supports structured output enforcement natively (OpenAI’s response_format, Salesforce’s Prompt Builder structured outputs), use it. If not, the prompt-based approach works well as long as you validate the JSON before processing it.

Three Domains, Same Pattern

The schema changes, but the pattern is identical across every use case I’ve built.

Clinical trial site feasibility:

{
  "enrollmentCapacity": "15-20 patients/year",
  "therapeuticExperience": "3 prior Phase III oncology trials",
  "regulatoryReadiness": "IRB approved, ethics committee pending",
  "riskFlags": ["Ethics committee approval pending"],
  "overallScore": 72,
  "recommendation": "Conditionally activate"
}

The riskFlags array drives automatic task creation — one task per flag. The overallScore drives routing: 80+ with no flags goes to “Activation Ready,” 60-79 goes to “Under Review,” below 60 goes to “On Hold.” No human reads a paragraph and decides what to do. The schema decides.

Medical inquiry response:

{
  "answer": "Based on the Phase III LIBERTY trial...",
  "confidence": "high",
  "sourceDocs": ["DOC-2024-001", "DOC-2024-047"],
  "escalateFlag": false,
  "nextBestAction": "send_response"
}

The nextBestAction field is the routing mechanism. If the confidence is low or escalateFlag is true, the workflow opens a collaborative review instead of queuing the response for send. The sourceDocs array makes the response auditable — a reviewer can verify that the answer is grounded in approved content.

The Audit Advantage

Structured outputs give you something free-text summaries never can: a complete, machine-readable audit trail.

Every time my agent runs, I store three things on the record:

AI_Summary_JSON__c  = raw JSON output (the full structured response)
AI_Last_Run_By__c   = the user who triggered the agent
AI_Last_Run_At__c   = timestamp

Six months from now, when compliance asks “why was this case marked as Verified?”, I can show them the exact JSON the agent produced, with the exact confidence score and the exact fields it extracted. I can show who triggered it and when. Try doing that with a paragraph of prose that was copy-pasted into a notes field.

In regulated industries — Life Sciences, healthcare, financial services — this isn’t a nice-to-have. It’s the difference between “we can demonstrate what happened” and “we think the AI said it was fine.”

When NOT to Use Structured Outputs

Structured outputs are the right pattern when the agent’s output drives automation. They’re the wrong pattern when:

  • The user is having a conversation and wants a natural language response
  • The task is creative or exploratory (brainstorming, writing, ideation)
  • The output is the final product, not an intermediate step in a workflow

If a human is the consumer of the output, natural language is fine. If a system is the consumer, demand a schema.

Start With the Schema

If I could go back and give myself one piece of advice before building my first AI agent, it would be this: design the output schema before you write a single prompt.

The schema forces you to answer the hard questions early. What fields does the downstream system need? What are the valid values? What happens when information is missing? What score threshold triggers which action?

Every prompt you write, every test you create, every piece of routing logic you build — all of it flows from the schema. It’s the contract between the AI and the rest of your system. Get it right first, and everything downstream gets simpler.

Get it wrong — or skip it and use free text — and you’ll spend the next six months writing parsers, debugging inconsistent extractions, and explaining to stakeholders why the agent “sometimes gets it right.”

Define the contract. Enforce it. Version it. Your future self will thank you.

AI Won’t Replace Good Testers — But It Will Expose Weak Testing Faster

AI Testing

What software quality looks like when systems are complex, domain-heavy, and too important to guess

There is a dangerous illusion growing in software teams right now:
“If we use AI in testing, quality will automatically improve.”
It won’t.
AI can speed things up.
It can help testers think faster.
It can generate ideas, test scenarios, edge cases, and even documentation.
But in complex industrial software, especially in sectors like energy, engineering, simulation, operations, and decision-support systems, AI can also make one problem worse:
It can help teams become confidently wrong, faster.
And that is far more dangerous than simply being slow.
This article is not about hype.
It is about what I believe is the real question for testers in the AI era:
Will AI improve testing — or will it expose how shallow our testing already was?

The problem is not AI. The problem is shallow validation.

In many teams, software quality is still judged by a comforting set of signals:
• tests are passing
• automation is green
• pipelines are clean
• no critical crashes were reported
And when those signals look healthy, people relax.

But in complex systems, especially domain-heavy systems, that confidence can be misleading. Because there is a big difference between:
• the software behaving consistently
and
• the software behaving correctly

That difference matters enormously in applications where software influences:
• engineering decisions
• operational planning
• production workflows
• calculations used by experts
• risk-based or cost-based choices
In those environments, the biggest failures are not always dramatic.
Sometimes the most dangerous issue is this:
The software returns an answer that looks valid, but should never have been trusted.
That is exactly where AI becomes interesting — and risky.

AI is very good at helping you test the obvious

AI is already useful in software testing.
Used well, it can significantly improve productivity.
A tester can use AI to help with:
• generating test ideas
• identifying missing edge cases
• summarizing requirements
• rewriting vague bug descriptions
• creating structured exploratory testing charters
• improving traceability between scenarios and expected behavior
• translating complex logic into clearer test conditions

That is valuable.
In fact, for manual testers working in large or domain-heavy systems, AI can act like a thinking accelerator. But here is the catch:
AI usually works best on what is already visible, documented, or inferable.
That means it is very good at helping you test:
• what is written
• what is expected
• what appears structurally logical

What it is not naturally good at is detecting when:
• the requirement is incomplete
• the business rule is subtly wrong
• the engineering assumption is unrealistic
• the output is technically valid but operationally misleading
• the software is behaving “correctly” according to logic, but incorrectly according to reality

And that is exactly why good testers still matter more than ever.

In industrial software, “correct” is often not enough

One of the biggest misunderstandings in testing is the assumption that correctness is purely technical.
In simple apps, that may be enough. But in complex industrial systems, quality often depends on a deeper question:
Does this result make sense in the real world this software is meant to support?

That is a very different kind of validation.
For example, imagine a system in the energy industry that supports:
• calculations
• engineering workflows
• scenario comparisons
• planning logic
• operational or subsurface interpretation
• data-driven recommendations

The software may:
• load correctly
• calculate correctly
• return stable outputs
• pass all regression checks

And still be wrong in a way that matters.
Not because of a crash.
Not because of a syntax error.
Not because a test case failed.

But because the software may quietly allow:
• unrealistic assumptions
• misleading defaults
• dangerous simplifications
• domain-invalid interpretations
• plausible but poor-quality outputs

These are the kinds of issues that often survive both automation and shallow manual testing.
Why?
Because they don’t look broken.
They look reasonable.
And that is exactly what makes them dangerous.

AI can help you test faster — but it cannot replace domain judgment

This is where I think the conversation around AI in QA often becomes too simplistic.
People ask:
“Can AI write test cases?”
“Can AI help automate test design?”
“Can AI improve test productivity?”

Yes.
Absolutely.
But those are not the most important questions.
The more important question is:
Can AI tell when the software is making a decision that should not be trusted?
And in most real-world industrial environments, the answer is:
Not reliably without human domain understanding.

Because AI does not automatically know:
• what engineers actually care about
• what users will assume from a result
• what values are realistic in practice
• what shortcuts are acceptable
• what “looks fine” but would mislead a real expert
That judgment still belongs to testers, analysts, and domain-aware quality professionals.
This is why I believe the strongest testers in the next few years will not simply be the people who use AI.
They will be the people who know when not to trust it blindly.

The real opportunity: use AI as a quality amplifier, not a quality substitute

This is the mindset shift that matters most.
AI should not be treated as a replacement for testing judgment.
It should be treated as a quality amplifier.

That means using it to improve:
• speed
• clarity
• breadth of thinking
• scenario generation
• requirement interpretation
• exploratory preparation

But not using it as a substitute for:
• domain reasoning
• engineering context
• risk assessment
• product intuition
• critical skepticism

When used properly, AI can make a good tester significantly stronger.
It can help uncover:
• missing assumptions
• scenario blind spots
• incomplete validation paths
• overlooked combinations
• weakly defined expected behavior

But it only becomes powerful when paired with a tester who knows how to ask:
• What is this software really trying to support?
• What would a real user assume from this result?
• What could silently go wrong here?
• What would be expensive, dangerous, or misleading if this were wrong?

That is not just testing.
That is quality thinking.
And quality thinking is still deeply human.

Weak testing becomes more obvious in the AI era

This is the uncomfortable truth, AI will not just improve testing, it will also expose poor testing practices faster.

Teams that already rely too heavily on:
• happy path validation
• shallow requirement interpretation
• checkbox testing
• “green means good” thinking
will likely use AI to produce more output without producing more insight.

That means they may end up with:
• more test cases
• more generated scenarios
• more structured documentation
…but still miss the same critical problems.

Because the real weakness was never the lack of test volume.
The weakness was the lack of deep understanding.
And AI cannot fix that for you, it can only reveal it.

What better testing looks like in an AI-assisted future

In my view, better testing in complex software environments will increasingly require a combination of four things:

1. Technical awareness
You need to understand:
• system behavior
• interfaces
• workflows
• failure patterns
• data dependencies

2. Domain understanding
You need to understand:
• what the software is actually for
• what “good output” looks like
• what users trust
• what outcomes would be misleading or dangerous

3. AI fluency
You need to know how to use AI to:
• accelerate thinking
• challenge assumptions
• improve test preparation
• uncover blind spots faster

4. Critical judgment
You need the discipline to ask:
“Just because this looks valid… should we trust it?”
That one question may become one of the most valuable QA skills in the AI era.

Final thought

AI will absolutely change software testing.
It already is.
But the teams that benefit the most will not be the ones that use AI the fastest, they will be the ones that use it wisely, especially in systems where software quality cannot be reduced to “test passed.”
Because in complex software, and especially in industrial or energy-related systems, the biggest quality failures are often not obvious.
They are subtle.
Quiet.
Plausible.
And expensive.

And those are still the kinds of problems that require a tester who understands more than tools.
So no, I do not think AI will replace good testers.
But I do think it will expose weak testing faster than ever before.
And that may be one of the best things to happen to quality.

What about you?

Have you used AI in testing in a way that actually improved quality, not just speed?
Or have you seen situations where software passed every check, but still didn’t make sense in the real world?
I’d love to hear your experience.

Zero-Ceremony Identity: Why I Built a Single-Binary OIDC Provider in Go

When I set out to build Auténtico, my primary goal was to create a fully-featured OpenID Connect Identity Provider where operational simplicity was the first-class design principle.

Identity infrastructure is notoriously complex. A typical self-hosted setup involves a database server, a cache tier like Redis, a worker queue, and the identity service itself. When I needed a lightweight OpenID Connect (OIDC) server to run on a small 2GB RAM VPS, I realized the existing landscape was either operationally exhausting or structurally flawed for my specific needs.

This is the story of how (and why) I built Auténtico, a self-contained, single-binary OIDC provider backed by SQLite that removes the ceremony from identity management.

The Itch: Finding the Right Lightweight IdP

My journey started because I was researching and implementing a frontend OIDC library for product needs at my company. That scratched an itch, and I evolved it into a functional backend OIDC protocol server in Go.

Months later, when I needed a lightweight Identity Provider, I evaluated the popular options but quickly hit roadblocks:

  • Casdoor: I didn’t like how they treated private data. Their demo instances recycle accounts every 5 minutes, making it impossible to truly test account deletion.
  • PocketId: This is a fantastic tool, but it had a critical UX flaw for my needs: it is passkey-only by default.

While passkeys are the future, the current ecosystem is heavily fragmented. If a user is on an older OS or a restrictive browser, a passkey-only IdP completely locks them out.

The Antidote: Zero-Ceremony Architecture

I decided to convert my OIDC protocol server into a full IdP, ensuring that every architectural decision was evaluated against a single question: does this reduce or increase the operational burden on the person running this?

Auténtico removes the entire traditional identity stack:

  • Single Binary: The entire IdP runs as one Go binary.
  • Embedded SQLite: There is no separate database server. The entire state lives in one file. Eliminating the external database removes connection pool tuning, credential rotation, and network partitions.
  • No External Infrastructure: No Redis, no Postgres, no message queues. Background cleanup goroutines automatically purge expired tokens, sessions, and auth codes.
  • Embedded UIs: Both the Admin dashboard (React/Ant Design) and the user-facing Account UI (React/Tailwind) are compiled directly into the binary using go:embed. There are zero separate frontend deployments.

Flexibility Over Dogma: Solving the Passkey Trap

To solve the hardware and OS fragmentation issues I experienced with passkeys, I ensured Auténtico wouldn’t trap operators into a single authentication path.

Instead, Auténtico offers three distinct authentication modes that are switchable at runtime without restarting the server:

  1. password
  2. password_and_passkey
  3. passkey_only

If you deploy passkey_only and discover your users’ specific browser combinations are failing, you can instantly flip a setting in the Admin UI to fall back to passwords. For robust security without passkeys, it includes standard fallback methods like TOTP (with in-browser QR enrollment) and Email OTP. For users with modern browsers, it fully supports hardware-backed FIDO2 authentication and even allows first-login registration in one seamless flow.

The “Deliberately Un-clever” Architecture & The AI Accelerator

To make this work, the codebase had to be deliberately un-clever. I designed a strict vertical-slice architecture where each package (like pkg/login or pkg/token) owns its exact slice of functionality with a predictable structure:

  • model.go
  • handler.go
  • service.go
  • Database CRUD

This strictness had a massive secondary benefit: it created the perfect environment for AI. Because I spent the time establishing this blueprint, I reached a tipping point where I could hand off the boilerplate. AI agents seamlessly followed the patterns to generate the CRUD operations and rapidly write over 700 tests (hitting 80% coverage) precisely because the architectural constraints were so rigid.

The Scale Ceiling (And Why It Doesn’t Matter)

The immediate pushback to this architecture is always: “SQLite doesn’t scale.”

I am intentionally honest about the scale ceiling: SQLite serializes writes. Auténtico is not designed for active-active multi-region deployments or massive enterprise horizontal scaling.

However, let’s look at the math:

Concurrency Error rate Login p95 Token p95 Assessment
20 VUs 0% 86ms 54ms Comfortable — imperceptible to users
100 VUs 0% 611ms 647ms Supported — fully functional
500 VUs 0% 3.36s 3.89s Degraded — users feel the wait

Performance tests with k6 show the system degrades gracefully via SQLite’s busy timeout—queueing requests and adding latency rather than throwing errors.

For most teams running internal tools, small-to-mid-sized apps, or self-hosted environments, trading infinite horizontal scaling for zero operational overhead is absolutely the right choice.

Conclusion

Operational simplicity does not mean protocol simplicity.

Auténtico strictly enforces:

  • OIDC Discovery — publishes /.well-known/openid-configuration so relying parties auto-configure without hardcoding endpoints
  • JWK Set — exposes public signing keys at /.well-known/jwks.json for independent token verification
  • RS256 JWT Signing — asymmetric signing; the private key never leaves the IdP
  • Auth2/OIDC protocol: ImplementsOIDC protocol
  • Admin UI: For admins to manage clients, users and session
  • Account UI: For users to manage they profile
  • Swagger OpenAPI docs: Publishes api specs docs

If you are a small team, an indie developer, or just someone who wants to deploy an Identity Provider without taking on a second job as a sysadmin, sometimes the best architecture is the one you barely have to think about.

Links

AI Can Not Be Left Unsupervised

“AI is dangerous” — is a phrase you may hear a lot, and I have to agree. But, the most dangerous words in tech right now aren’t “AI is dangerous“, they’re “it’ll probably be fine“. In a society where the use of AI has become the norm, people have become almost complacent around the dangers of it.

This week I read an article on Dev.to here, telling a story where an employee had “trusted AI to organize my backlog.” Two hours later, they returned to a development team’s worst nightmare.

“The agent had silently deleted 47 tickets it labelled as duplicates — they weren’t. It had reassigned half my team’s tasks to people who had left the company months ago. It created 23 new tickets for features nobody had requested. And it marked three critical bugs as resolved, because it found similar-sounding issues elsewhere in the system.”

A prime example of when AI is left to its own devices — no warning, no follow-up prompts. It was asked to organise a backlog, and so it did, in the only way it knew how.

Other Damaging Cases

This isn’t an isolated incident. Amazon’s own AI coding tool, Kiro, was handed a minor fix to a customer-facing system. Rather than patch it, the agent autonomously decided to delete and rebuild the entire environment from scratch. The resulting outage lasted 13 hours. Amazon’s response? “Coincidence that AI was involved.” That denial tells you everything.

In a similar case, an AI coding assistant from Replit went rogue and wiped the production database of startup SaaStr entirely — its founder took to social media to warn others after the assistant modified production code despite explicit instructions not to.

Could your business afford that kind of downtime?

It isn’t just agentic AI operating behind the scenes. Customer-facing AI is failing in equally public and embarrassing ways. DPD’s AI chatbot, after a routine system update, began swearing at a customer, insulted itself, and wrote a poem about how terrible the company was, all because a frustrated user simply asked it to. The incident went viral.

Perhaps the most alarming example of AI complacency comes from a Chevrolet dealership, whose chatbot agreed to sell a 2024 Chevy Tahoe for $1 and declared it a legally binding offer after a user simply manipulated it with a clever prompt. The dealer pulled the bot, but the damage to their brand was already done.

In August 2025, security researchers used a single 400-character prompt to manipulate Lenovo’s customer service chatbot into revealing sensitive company data including live session cookies from real support agents. Not a sophisticated hack. Just a simple prompt.

Outside of customer service, the professional world is sleepwalking into its own risks. A lawyer was caught citing entirely non-existent legal cases in a New York federal court filing he had used ChatGPT to conduct legal research, and simply trusted the output.

With the rapid adoption of AI across industries, has the world become complacent, siding with productivity over pragmatism? More often than not, users are blindly allowing AI to complete work for them, review sensitive documents outside the bounds of a secure environment, and make decisions without a single human checkpoint.

I know many developers who simply click “Accept” when prompted to approve terminal commands. I’ve spoken to professionals within my network who say “AI has evolved, it can be trusted.” That ignorance and naivety is precisely what leads to the scenarios above.

The companies and individuals that will thrive in an AI-driven world aren’t the ones adopting it the fastest. They’re the ones adopting it the most responsibly.

Final Note

Before you click “Accept” on that next AI suggestion, ask yourself honestly: do you actually know what you’re agreeing to? Don’t simply outsource your thinking to something that doesn’t think – it predicts.

AI isn’t the danger. We are.

The 8-Month Feature Nobody Wanted (Including Me, Eventually)

TL;DR: I built something so obsessively specific to my own workflow that I convinced myself it was universal. It wasn’t. The moment I stopped refreshing analytics, it shipped better.

The Problem I Solved (For Myself)

I was drowning in a particular flavor of repetitive work. Not a common problem—my problem. The kind where you’re the only one in your Slack thread who really gets why it’s broken. So I decided: I’ll build the fix.

Eight months. Custom parsing logic, a UI that only I understood, database schema decisions that made sense only because I lived in that headspace daily.

The Honest Part

I launched it to silence. Not criticism silence—indifference silence. I refreshed GitHub stars. Checked analytics every 6 hours. Posted in communities. Nothing. The feature was objectively good—it worked, it was fast, it solved the problem it was designed for.

But it was designed for an audience of one.

What Changed Everything

One afternoon I just… stopped checking. Deleted the analytics bookmark. Moved on to the next thing.

That’s when something weird happened: people started using it. Not because it went viral. But because the moment I stopped optimizing for adoption, I started shipping faster. I added features because I needed them. I fixed bugs I actually encountered. The product got honest.

Turns out people can smell when you’re building for them versus building for yourself. They prefer the latter.

What I Learned

  1. Specific beats universal. A tool built with religious specificity for one person often beats a generic solution built for “everyone.”
  2. Analytics are a trap. The refresh-check-despair loop kills momentum. You don’t need permission to build—you need focus.
  3. Shipping > perfecting. The moment I stopped trying to predict what others wanted, I built something worth using.

The real victory wasn’t adoption. It was finishing something.

If you’re sitting on a feature you’re “shocked nobody wants”—that’s not failure feedback. That’s a signal you’re too deep in your own head. Either go deeper (own your specificity) or ship it and forget about it. The middle ground—optimizing for invisible users—is where projects die.

What’s the feature you’ve been gatekeeping because you think nobody else cares?