What Nobody Tells You About Building a Multi-Provider MCP Server

Every MCP server tutorial follows the same script: install the SDK, define a tool, return a response. Ship it.

That works for a single API. It does not work when you need an agent to reliably choose between, authenticate to, and call 645+ different APIs across 86 categories — and handle everything that goes wrong at 3am with no human.

We built Rhumb, an MCP server that proxies hundreds of real APIs for AI agents. Here’s what actually broke, why the tutorials don’t cover it, and what you need to know if you’re building anything beyond hello-world.

Bug #1: The slug aliasing problem

First surprise: APIs don’t have stable identifiers.

Brave’s search API appears as both brave-search-api and brave-search depending on which documentation page you read. When an agent asks to “search with Brave,” your MCP server needs to know these are the same service.

This isn’t unique to Brave. We found alias collisions in payment providers (same company, multiple API versions with different names), communication platforms (SMS vs messaging vs voice — same provider, different “APIs”), and analytics tools (legacy vs v2 naming).

The fix isn’t a lookup table. It’s a canonical slug system with alias resolution that treats identity as a first-class problem.

Why tutorials skip this: They show one API. You never hit naming collisions with one API.

Bug #2: Authentication is not a solved problem

The tutorials say: “Add your API key to the header.” That covers maybe 40% of real APIs.

What we actually encountered across providers:

  • Bearer token (Authorization: Bearer {key}) — ~45% of services
  • Custom header (X-API-Key, X-Subscription-Token, Api-Key) — ~25%
  • Basic Auth (base64 encoded credentials) — ~15%
  • OAuth2 with token refresh — ~10%
  • Query parameter (?api_key=…) — ~5%

The problem isn’t supporting all five patterns. It’s that your MCP server needs to know which pattern each API uses before the agent’s first call. If the agent sends a Bearer token to an API expecting X-API-Key, you get a 401 that tells the agent nothing useful.

Worse: some APIs accept the wrong auth method silently and return empty results instead of errors.

Bug #3: The payload translation trap

Your agent constructs a JSON payload. The API expects multipart form data.

This hits hardest with document processing APIs. An agent wants to send a file for parsing. It constructs a reasonable JSON body with the file content. The API returns 400 because it only accepts multipart uploads with specific field names.

The gap between “what the agent naturally produces” and “what the API actually accepts” is wider than anyone admits:

  • Parameter naming: query vs q vs search_query vs prompt
  • Body format: JSON vs form-encoded vs multipart
  • Array handling: tags=a,b,c vs tags[]=a&tags[]=b vs {"tags": ["a","b"]}
  • Date formats: ISO 8601 vs Unix timestamps vs custom strings
  • Pagination: cursor vs offset vs page-number vs link-header

Why this matters for agents: A human developer reads the docs and adapts. An agent will retry the same malformed request until it hits rate limits.

Bug #4: Error messages that lie

Here’s a real error response from a production API:

{"error": "An error occurred. Please try again later."}

An agent receiving this will try again later. The actual problem? Invalid API key format. Retrying will never help.

The quality gap in error responses across 645+ APIs is staggering:

Good (Stripe-class):

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "param": "amount",
    "message": "Missing required param: amount"
  }
}

Bad (more common than you’d think):

{"status": "error", "message": "Bad Request"}

In our scoring of 645+ APIs, structured error responses are a minority. Most APIs return human-readable error strings that agents can’t reliably parse.

Bug #5: Rate limits without information

Good APIs tell you exactly where you stand:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1616000000
Retry-After: 30

Bad APIs return 429 and nothing else.

Some APIs have undocumented secondary rate limits. GitHub’s REST API has a primary rate limit (5,000/hour) and a secondary rate limit on “content-creating” endpoints that’s lower and not reflected in headers. An agent creating issues or comments will hit the secondary limit and get a 403 with a message about “secondary rate limits” that doesn’t appear in any getting-started guide.

Real numbers from our data:

  • Stripe (AN 8.1): Rate limit headers, Retry-After, burst limit documented
  • GitHub (AN 7.8): Primary headers yes, secondary limits documented but not in headers
  • PayPal (AN 4.9): Headers inconsistent, sandbox limits undocumented

Bug #6: The sandbox illusion

“Just use the sandbox.” Every API says this. Few deliver.

Real sandbox problems we encountered:

  • Sandbox requires production credentials — defeats the purpose
  • Sandbox has different behavior — you test against a lie
  • Sandbox has stricter rate limits — can’t performance test
  • Sandbox doesn’t support all endpoints — partial testing only
  • Sandbox requires CAPTCHA to create — agents can’t self-provision

PayPal’s sandbox requires CAPTCHA verification to create accounts. That one detail drops it from “agent-friendly” to “requires a human for setup.”

Bug #7: The versioning time bomb

APIs change. Versioning is supposed to protect you.

In practice:

  • Stripe: Explicit API version in every request header. Pin a version, get consistent responses forever.
  • Most APIs: Unversioned endpoints that change without notice. Your agent’s response parser breaks silently when email_address becomes emailAddress.

The insidious part: breaking changes often affect edge cases first. Your happy-path tests pass. Your agent hits the edge case at 3am.

What we learned

After building through all of this, we distilled the problems into a scoring framework. Every API gets evaluated on 20 dimensions:

Execution (70%): Error handling, schema stability, idempotency, latency, rate limit transparency.

Access Readiness (30%): Signup friction, auth complexity, docs quality, sandbox, rate limits.

Some results that surprised us:

  • Stripe (8.1) and Twilio (8.0) are genuinely built for automation
  • GitHub (7.8) is excellent but has sneaky secondary rate limits
  • Resend (7.8) — newer email API that got the details right from day one
  • SendGrid (6.4) — dominant but showing age in error handling
  • PayPal (4.9) — the CAPTCHA sandbox alone is disqualifying for autonomous use
  • Salesforce (4.8) — powerful but the OAuth dance is hostile to agents

The full leaderboard across 86 categories is at rhumb.dev/leaderboard.

If you’re building an MCP server

  1. Treat service identity as a first-class problem. You will hit naming collisions.
  2. Build an auth resolution layer. Don’t make the agent know which header format each API uses.
  3. Expect payload translation. What the agent sends and what the API wants are rarely the same shape.
  4. Parse errors defensively. Most APIs don’t return structured errors.
  5. Implement rate limit tracking per-provider. Don’t share a single backoff strategy.
  6. Test against production, not just sandboxes. Many sandboxes are incomplete.
  7. Pin API versions where possible. If the API doesn’t support versioning, monitor for breaking changes.

The MCP protocol gives you a great transport layer. It tells you nothing about what happens when your tools hit real APIs. That part is on you.

Rhumb scores 645+ APIs across 86 categories on 20 dimensions. Methodology at rhumb.dev/methodology. MCP server is open source: github.com/supertrained/rhumb.

We scanned 4,162 MCP servers. 73% are invisible to AI agents.

There are 4,162 MCP servers registered on Smithery right now. The Python and TypeScript SDKs see 97 million monthly downloads. Every major AI provider has adopted MCP.

But nobody had measured the quality of these tools.

We built ToolRank, an open-source scoring engine that analyzes MCP tool definitions across four dimensions. Then we pointed it at the entire Smithery registry.

The biggest finding wasn’t about quality. It was about visibility.

73% of MCP servers are invisible

Out of 4,162 registered servers, only 1,122 expose tool definitions that agents can read.

The remaining 3,040 — 73% — have no tool definitions at all. They’re registered, but when an AI agent searches for tools, these servers don’t exist. They have no name, no description, no schema. They are invisible.

This is the equivalent of having a website with no indexable content. Google can’t rank what it can’t read. Agents can’t select what they can’t see.

Among the visible: average score 84.7/100

For the 1,122 servers that do expose tool definitions, we scored each one across four dimensions:

Dimension Weight What it measures
Findability (25%) Can agents discover you? Registry presence, naming
Clarity (35%) Can agents understand you? Description quality, purpose, context
Precision (25%) Is your schema precise? Types, enums, required fields
Efficiency (15%) Are you token-efficient? Definition size, tool count

Average ToolRank Score: 84.7/100

Level Score Count %
Dominant 85-100 677 60%
Preferred 70-84 406 36%
Selectable 50-69 39 3.5%
Visible 25-49 0 0%
Absent 0-24 0 0%

The average is higher than expected. But there’s a survivorship bias: servers that bother to expose tool definitions tend to be better maintained overall. The real quality problem is the 73% that expose nothing.

Top scoring servers

Rank Server Score
1 microsoft/learn_mcp 96.5
2 docfork/docfork 96.5
3 brave (Brave Search) 94.7
4 LinkupPlatform/linkup-mcp-server 93.5
5 smithery-ai/national-weather-service 93.3

What do top servers do differently? They start descriptions with clear action verbs. They include usage context (“Use this when…”). They define required fields, enums, and defaults. They keep tool count under 15.

The most common defects

Among the 1,122 scored servers, the most frequent issues:

  1. Missing usage context — Description says what the tool does, but not when to use it. Agents need “Use this when…” to decide between competing tools.

  2. No return value described — Agents can’t predict what they’ll get back. This leads to incorrect downstream handling.

  3. Missing parameter descriptions — Schema has types but no explanations. An agent sees "query": {"type": "string"} but doesn’t know what format the query should be in.

  4. No required fields defined — Agents guess which parameters are mandatory, leading to failed executions.

  5. Description too short — Under 50 characters. Not enough information for reliable selection.

What does this mean?

Academic research backs up why this matters. A study of 10,831 MCP servers (arXiv 2602.18914) found that tools with quality-compliant descriptions achieve 72% selection probability vs 20% for non-compliant ones. That’s a 3.6x advantage.

The fixes are trivial. Adding “Use this when…” to a description. Defining required fields. Starting the description with a verb. These are 5-minute changes with measurable impact.

We’re calling this ATO

ATO (Agent Tool Optimization) is the practice of optimizing tools so AI agents autonomously discover, select, and execute them.

SEO optimized for search engines. LLMO optimized for LLM citations. ATO optimizes for agent tool selection.

The key difference: SEO and LLMO result in mentions and links. ATO results in your API being called and transactions occurring. LLMO is Stage 1 of ATO — necessary but not sufficient.

Full ATO framework →

Try it yourself

Score your tools: toolrank.dev/score — paste your tool definition JSON. Includes “Try example” buttons so you can see how scoring works instantly.

Ecosystem ranking: toolrank.dev/ranking — live rankings updated weekly.

Scoring engine: github.com/imhiroki/toolrank — fully open source. The scoring logic is transparent.

Methodology

  • Data source: Smithery Registry API (registry.smithery.ai)
  • Scan date: March 28, 2026
  • Servers in registry: 4,162
  • Servers with tool definitions: 1,122 (27%)
  • Scoring level: Level A (rule-based, 14 checks, zero LLM cost)
  • Request interval: 2 seconds (respectful to Smithery infrastructure)
  • Full scan time: ~2 hours
  • Code: Open source

Data updates weekly via automated scanning. Daily differential scans catch new servers.

Built by @imhiroki. Questions, feedback, or want to improve your score? Open an issue on GitHub.

DAG vs Langraph Nodes

When we have DAG that represents our tasks and its dependencies do we still need Langraph nodes?

LangGraph nodes are most valuable when your flow has cycles — loops, retries, HITL resume, conditional branching back to earlier steps. A pure DAG has none of that, so LangGraph’s core value proposition doesn’t apply.

So are they mutually exclusive? No! They can complement each other. See the beautiful project to understand why.

LangGraph nodes for the orchestration layer: intent_parser → entity_extractor → plan_builder → hitl_plan → cot_builder → hitl_confirm. This is the stateful, cyclic part.

Your own DAG executor for task execution: takes the confirmed TaskGraph, runs a topological sort, fans out independent tasks with asyncio, collects results. Twenty to thirty lines of plain Python. No framework.

fixing two bugs stacked on top of each other in ProseMirror

heading joins with par

i’m building a markdown editor built on Milkdown (which wraps ProseMirror). i hit a bug in how marks behave when you join blocks together.

the bug

type a bold heading. move your cursor to the start of it. press Backspace. the heading joins into the paragraph above, but the bold marks bleed into the previously-unstyled paragraph text.

heading joins with par

i assumed this was a bug in joinTextblockBackward. it wasn’t.

what’s actually going on

when you press Backspace at position 0 of a textblock, joinTextblockBackward fires. under the hood, joinTextblocksAround calls:

replaceStep(state.doc, beforePos, afterPos, Slice.empty)

this deletes the boundary between the two blocks. the Fitter algorithm stitches the content of the second block into the first. it doesn’t touch inline marks. the text nodes transfer as-is, bold and all.

there’s a clearIncompatible function that strips marks disallowed by the target node type, but paragraphs allow bold, so nothing gets stripped. there’s also splitBlockKeepMarks which handles the reverse operation (Enter), but that’s about storedMarks — cursor behavior for the next typed character. different problem.

so the marks survive the join, and they’re supposed to. consider two paragraphs:

She said the results were
**statistically significant** and could not be ignored.

press Backspace at the start of the second paragraph. they merge into one. the bold must survive because you put it there intentionally. if ProseMirror stripped marks on join, it would destroy user content.

the heading case feels wrong for a different reason. in a markdown editor, # **Bold Heading** has explicit bold marks on the text nodes because that’s what the markdown source says. you see the boldness as part of the heading, like a visual property of the block. but ProseMirror sees it as an inline mark that happens to sit inside a heading node. the markdown editor stores the heading’s visual weight in two places: the node type and the inline marks. when the heading unwraps into a paragraph, only the node type changes. the marks stay because ProseMirror doesn’t know they were “heading-ness” and not “the user explicitly wanted bold.”

not a ProseMirror bug. a modeling problem specific to markdown editors.

the fix

i intercept joinTextblockBackward‘s dispatch. before the transaction reaches the editor state, i snapshot the heading content range, then append removeMark calls for the region that used to be the heading. i also clear storedMarks so the cursor doesn’t inherit marks either. everything lands in one transaction, so undo reverses it atomically.
heading joins correctly with no mark bleed

here’s the standalone plugin:

import { Plugin, PluginKey } from "prosemirror-state";
import { joinTextblockBackward } from "prosemirror-commands";

const headingBackspacePlugin = new Plugin({
  key: new PluginKey("heading-backspace"),
  props: {
    handleKeyDown(view, event) {
      if (event.key !== "Backspace") return false;

      const { state, dispatch } = view;
      const headingType = state.schema.nodes.heading;
      if (!headingType) return false;

      const { $from, empty } = state.selection;
      if (
        !empty ||
        $from.parentOffset !== 0 ||
        $from.node().type !== headingType
      )
        return false;

      // Snapshot before the join mutates anything.
      const headingContentSize = $from.node().content.size;
      const prevEnd = $from.before();

      const wrappedDispatch = (tr: typeof state.tr) => {
        // Map prevEnd through the join's ReplaceStep to find where
        // the heading content now sits inside the merged paragraph.
        const from = tr.mapping.map(prevEnd);
        const to = from + headingContentSize;

        // Strip every mark type from the former-heading range.
        for (const markType of Object.values(state.schema.marks)) {
          tr.removeMark(from, to, markType);
        }

        // Clear storedMarks so the cursor doesn't inherit marks.
        tr.setStoredMarks([]);

        dispatch(tr);
      };

      return joinTextblockBackward(state, wrappedDispatch, view);
    },
  },
});

$from.before() gives the position just before the heading’s opening token — after the join’s ReplaceStep deletes the block boundary, tr.mapping.map(prevEnd) gives the new position where the heading content starts inside the merged paragraph. headingContentSize is stable because the join doesn’t change the heading’s content, just moves it. tr.removeMark is safe to call multiple times on the same transaction and tr.setStoredMarks([]) clears cursor marks so typing after the join doesn’t inherit bold/italic. the whole thing is one transaction — Ctrl+Z undoes the join and the mark removal together.

if you’re using Milkdown or Tiptap, register this through the framework’s plugin API. the ProseMirror logic is identical.

related discussion

I Built a Private Voice Chat App Because I Was Done Giving Discord My Conversations

description: Squawk is a self-hosted, open source voice and text chat app for gaming groups. Here’s why I built it and what I learned along the way.

 —

I play games with a small group of friends. We’ve been using Discord for years. It works fine — but at some point I started thinking about what “works fine” actually means.

Every conversation we have passes through Discord’s servers. Every voice session, every message, every “we’re planning this at this time” — all of it sitting in a database I have no visibility into. For most people that’s a totally acceptable trade-off. For me it started to feel like an unnecessary one.

So I built something.

What is Squawk?

Squawk is a self-hosted voice and text chat app built for small private groups. You run it on your own machine. Your friends connect through Tailscale — a zero-config private VPN. Nobody else can reach the server. No accounts needed, no data leaving your network.

The core features:

  • 🎤 Peer-to-peer voice chat via WebRTC (audio never touches a server)
  • 💬 Real-time text chat with typing indicators
  • 👑 Channel ownership and permissions (kick, rename, delete)
  • 🔔 Sound notifications for joins, leaves, messages
  • 📱 Mobile-friendly with a voice/chat tab switcher
  • 🐳 Docker support — two commands to get running

You can find it here: github.com/shynsec/squawk

Why build it instead of just using something else?

Mumble exists. TeamSpeak exists. I know.

But I wanted something that felt modern — a clean UI, text chat alongside voice, mobile support — without the overhead of running a full Mumble server and without sending my friends through a sign-up flow.

I also wanted to actually build something. There’s a difference between knowing how WebRTC works and having shipped something that uses it. This project closed that gap for me.

What I learned

WebRTC is surprisingly approachable, until it isn’t

The basics of WebRTC — creating a peer connection, exchanging offers and answers, adding ICE candidates — clicked pretty quickly. The browser APIs are well documented and the happy path is straightforward.

Where it gets interesting is the signaling layer. WebRTC handles the audio transport but it doesn’t handle how peers find each other. You need a signaling server to broker that initial handshake. I used Socket.io for this, which made it clean to implement but also introduced an interesting security question: what stops a client from relaying WebRTC signals to peers in a completely different room?

The answer is nothing, by default. You have to explicitly enforce it. I ended up checking that both the sender and recipient are in the same room before relaying any offer, answer, or ICE candidate. Simple fix, but easy to miss.

Security on a “private” app still matters

It’s tempting to think that because Squawk runs behind a private VPN, you don’t need to worry much about security. That thinking is wrong for a few reasons.

First, anyone on your Tailscale tailnet can connect — and you might invite people you don’t fully trust. Second, if you ever open the app to a wider audience, you want the security foundations already in place. Third, good habits are good habits regardless of the threat model.

Things I ended up implementing that I hadn’t originally planned:

  • Rate limiting per socket connection (30 events per 5 seconds)
  • Input sanitisation on all usernames, channel names, and messages
  • Prototype pollution protection using Object.create(null) for state objects
  • Strict CORS limited to localhost and Tailscale IP ranges
  • Security headers (CSP, X-Frame-Options, Referrer-Policy)
  • Non-root Docker user

None of these are complicated individually. But thinking through all the surfaces that need covering took longer than I expected.

Ownership is harder than it sounds

The permissions system in Squawk is simple: whoever creates a channel owns it. They can kick users, rename the channel, delete it. But even that simple model has edge cases.

What happens when the owner leaves? Ownership has to transfer to someone. What if the owner renames themselves mid-session? The ownership record tied to their old name breaks. What if two people have the same display name — does one accidentally inherit the other’s permissions?

These are the kinds of details that only surface when you actually try to use the thing with real people. Designing permissions on paper is one thing. Watching them break in practice teaches you something different.

Ship it before it’s perfect

The first version of Squawk was a single HTML file and about 100 lines of server code. It worked. It was ugly. It did the one thing I needed it to do.

Every feature I added after that — text chat, typing indicators, mobile layout, Docker support, security hardening, the permissions system — came from actually using it and noticing what was missing. If I’d tried to design all of that upfront I’d probably still be planning.

What’s next

Squawk is open source under MIT. If you want to self-host your own private voice chat, the setup is genuinely quick — especially with Docker:

git clone https://github.com/shynsec/squawk.git
cd squawk
# Edit Caddyfile with your IP or domain
docker compose up -d

Things I’m thinking about adding next:

  • User avatars
  • Push-to-talk mode
  • Channel passwords
  • Persistent message history

If any of that sounds interesting, or you have ideas, the repo is open. Issues and pull requests welcome.

Built by shynsec

Gas Fees Killed Our First Demo. Here’s What We Did About It.

cover

cover

The investor meeting was going perfectly. We had built an AI agent that could autonomously pay for API calls, route micro-transactions across services, and settle in real time. Then we ran the live demo.

The transaction confirmed in 14 seconds. The room was impressed. Then our lead investor leaned forward and said: “What did that just cost?”

The answer was $8.40 in gas fees. For a $0.15 API call. The demo was dead.

The Gas Fee Math Nobody Talks About

Ethereum mainnet gas fees are priced in gwei per unit of gas. A simple ETH transfer uses 21,000 gas units. An ERC-20 token transfer uses 45,000-65,000 gas units. During the demo, mainnet was running at 80 gwei base fee with a 10 gwei priority fee.

# Gas cost formula
gas_units = 65000
base_fee_gwei = 80
priority_fee_gwei = 10
eth_price_usd = 3200

total_gwei = gas_units * (base_fee_gwei + priority_fee_gwei)
total_eth = total_gwei / 1e9
gas_cost_usd = total_eth * eth_price_usd

print(f"Gas cost: ${gas_cost_usd:.2f}")   # Gas cost: $18.72

Now scale that to an AI agent fleet doing 10,000 transactions per day. At $8-10 per transaction, you are spending $80,000-$100,000 per day just on gas. Before you pay for a single actual service.

agent fleet

Why Gas Fees Are Especially Brutal for AI

Human users can abort a transaction before it confirms. If a person sees the gas estimate is absurd, they cancel and wait. AI agents do not work that way.

An autonomous agent has a task to complete. It has been authorized to spend up to a certain amount. If the gas fee eats 98% of the transaction value, the agent does not know to stop. It just executes.

We built a fee monitor that would pause transactions when mainnet gas exceeded a threshold. It helped, but it introduced a new problem: the agent would get stuck mid-workflow, waiting for gas to drop, with no way to continue or fail gracefully.

The Fix: Move the Problem Somewhere It Does Not Exist

L2 solution

The real solution was not to manage gas better. It was to stop using a network where gas is a first-class variable cost.

We moved to Base (Coinbase’s L2) and the math changed completely:

# Same transaction on Base L2
gas_units = 65000
base_fee_gwei = 0.005
priority_fee_gwei = 0.001
eth_price_usd = 3200

total_gwei = gas_units * (base_fee_gwei + priority_fee_gwei)
total_eth = total_gwei / 1e9
l2_cost_usd = total_eth * eth_price_usd

print(f"Gas cost: ${l2_cost_usd:.6f}")   # Gas cost: $0.001248

Sub-cent transaction costs. Consistent. Predictable. An AI agent running 10,000 transactions per day now spends $12.48 in gas instead of $84,000.

How We Handle Gas Abstraction

After moving to L2, we still had a developer experience problem. Our team was spending engineering time managing gas wallets, estimating fees, and handling gas-related failures.

Rosud (rosud.com) handles gas abstraction so developers never think about it. The SDK manages gas estimation, L2 routing, and fee sponsorship automatically.

abstraction

// Before: manual gas management
const gasEstimate = await provider.estimateGas(txData);
const feeData = await provider.getFeeData();
const gasPrice = feeData.gasPrice * BigInt(120) / BigInt(100);

await wallet.sendTransaction({
  gasLimit: gasEstimate,
  maxFeePerGas: gasPrice,
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
});

// After: Rosud handles it
await rosud.pay({
  amount: 0.15,
  currency: 'USDC',
  to: serviceAddress,
});

Under the hood, Rosud routes to the optimal L2, manages gas wallets, sponsors fees when appropriate, and retries on failure. Your agent just calls pay().

The Broader Lesson

The investor who killed our demo did us a favor. It forced us to confront the core question: can you build reliable autonomous payment infrastructure on top of variable-cost networks?

The answer is yes, but only if you abstract away the variability. Gas fees are an implementation detail. They should never surface as a business cost that makes a $0.15 API call cost $8.40.

Rosud is payment infrastructure for AI agents. Gas abstraction, multi-chain routing, and sub-cent transaction costs. Learn more at rosud.com.

Is WordPress dead? (Or just too stubborn to notice?)

I’ve been building and maintaining WordPress sites for years. Clients, side projects, quick landing pages — WordPress was always the default answer. Need a website? WordPress. Need a blog? WordPress. Need a portfolio? WordPress with a theme and three plugins.

And for a long time, that made sense. WordPress powered a massive share of the web, had a plugin for everything, and you could hand it off to a non-technical client who could update their own content. The ecosystem was unmatched.

But lately, every time I spin up a new WordPress instance, I can’t shake the feeling that I’m solving a 2026 problem with a 2006 tool. And I don’t think I’m alone.

The weight problem

Let’s start with the thing nobody wants to say out loud: WordPress is slow.

Not theoretically slow. Not “if you don’t optimize it” slow. Slow by default. A fresh WordPress install with a commercial theme and a handful of essential plugins — contact form, SEO, caching, security — already ships more PHP, more database queries, and more JavaScript than most sites actually need.

And here’s the painful number: fewer than half of WordPress sites on mobile pass Google’s Core Web Vitals. For a platform that powers over 42% of the web, that’s not a minor issue — that’s a systemic design problem.

The response from the WordPress community is always the same: use a caching plugin, optimize your images, pick a lightweight theme, configure your CDN. In other words, spend hours undoing the bloat that WordPress introduced in the first place. You’re not building a website at that point. You’re performing surgery on one.

The plugin trap

WordPress’s greatest strength — its plugin ecosystem — has quietly become its biggest liability.

There are over 70,000 plugins available. That sounds impressive until you realize that up to 97% of WordPress security vulnerabilities come from plugins. Every plugin you install is a dependency you now have to maintain, update, and trust. And while most site owners do keep WordPress itself updated, the plugin ecosystem is where things fall apart — outdated, abandoned, or poorly maintained plugins are the norm, not the exception.

But even setting security aside, the plugin model creates a weird kind of dependency. Want a contact form? Plugin. Want SEO metadata? Plugin. Want your site to load in under three seconds? Plugin that undoes what the other plugins did. Want to back up your site? Plugin. The average WordPress site runs somewhere between 20 and 30 plugins, each adding its own CSS, JavaScript, and database queries.

At some point you have to ask: if you need a dozen plugins just to make a basic site work properly, is the platform actually solving your problem — or is it the problem?

The hosting illusion

One of WordPress’s selling points has always been cheap hosting. And it’s true — you can get WordPress hosting for a few euros a month. What nobody mentions is why it’s so cheap.

Budget shared hosting means your site shares a server with hundreds of others. Your PHP processes compete for CPU time. Your database queries queue up behind everyone else’s. The result is a site that technically works but loads like it’s on dial-up — especially under any kind of traffic spike.

The alternative is managed WordPress hosting, which is faster and more reliable but costs €20–50/month or more. At that point, you’re paying cloud-tier prices for a CMS that still needs plugins, still needs updates, and still fails Core Web Vitals half the time.

Meanwhile, a static site built with any modern tool deploys to Cloudflare Pages, Netlify, or Vercel for free. Zero servers to manage. Global CDN included. Load times measured in milliseconds, not seconds.

The elephant in the room: the Mullenweg drama

I can’t write about the state of WordPress in 2026 without mentioning this.

The ongoing conflict between Matt Mullenweg — WordPress co-creator and CEO of Automattic — and WP Engine has shaken the community in ways that go beyond a corporate lawsuit. Mullenweg blocked WP Engine customers from accessing WordPress.org updates, leaving sites unable to receive security patches. He expelled long-time contributors from the project. The legal battle dragged on through 2025 and into 2026.

Whether you side with Mullenweg or WP Engine isn’t really the point. The point is that this episode exposed something uncomfortable: a huge chunk of the web’s infrastructure depends on one person’s goodwill and one company’s business decisions. That’s a governance risk that many developers and businesses hadn’t fully considered — and it’s pushing people to look at alternatives more seriously than before.

The real question: what do you actually need?

Here’s where it gets interesting.

Think about the WordPress sites you’ve built or managed. How many of them were truly dynamic? How many actually needed a database, server-side rendering, user authentication, or a CMS backend?

In my experience, the answer is: far fewer than you’d expect. The vast majority of WordPress sites I’ve worked on were essentially static. A homepage, an about page, a services page, a contact form, maybe a blog. Content that changes once a month, if that.

For that kind of site, WordPress is not just overkill — it’s the wrong architecture entirely. You’re running a PHP application backed by a MySQL database, processing every page request on the server, just to deliver content that hasn’t changed since last Tuesday.

The AI shift nobody’s talking about

And this is where things get genuinely different from any “WordPress is dying” take you’ve read before.

We now have AI coding tools — Claude Code, OpenAI Codex, Google Gemini CLI — that can generate a complete, production-ready website from a conversation. Not a template. Not a wireframe. An actual, deployable site.

I’m not talking about toy demos. I mean: you describe what you want — a portfolio site with these sections, this color scheme, a contact form, responsive layout — and in a few minutes you have clean HTML, CSS, and JavaScript. No plugins. No database. No server. No security patches to install next month.

The output is a static site that loads instantly, scores 100 on Lighthouse, costs nothing to host, and does exactly what 80% of WordPress sites do — except faster, lighter, and with zero maintenance overhead.

And here’s the part that makes WordPress developers uncomfortable: the non-technical client who used to need WordPress because they couldn’t code? They can now describe what they want in plain language and get a working site. The abstraction layer that WordPress provided — “you don’t need to know how to code” — is being replaced by a better abstraction: “you don’t even need to know what a CMS is.”

What this looks like in practice

Let me be concrete. Here’s what a typical “WordPress project” looks like when you replace it with an AI coding tool and a static site approach.

The old way (WordPress): Buy hosting. Install WordPress. Pick a theme. Customize the theme (fight the theme). Install 10–15 plugins. Configure each plugin. Set up caching. Set up backups. Set up security. Write content. Hope nothing breaks on update day. Total setup time: a weekend, maybe more. Ongoing maintenance: constant.

The new way: Open Claude Code, Codex, or Gemini. Describe the site. Review the output. Deploy to Cloudflare Pages or Netlify. Done. Total setup time: an afternoon, probably less. Ongoing maintenance: near zero — there’s nothing to update because there’s nothing running.

The site is faster, more secure (there’s nothing to hack), cheaper to host (free, in most cases), and easier to modify (ask the AI to change it).

Where WordPress still wins

I promised honesty, so here it is.

WordPress is still the right choice in some scenarios. If you need a site where multiple non-technical users publish content daily — a news site, a multi-author blog, a magazine — the CMS workflow is hard to replace. WordPress’s editorial interface, revision history, and role management are mature and battle-tested.

If you’re running WooCommerce with thousands of products, complex inventory, and payment integrations, you’re in territory where static sites and AI prompts genuinely can’t compete. E-commerce at scale is a different beast.

And if you already have a WordPress site that works, that’s maintained, and that serves your needs — there’s no reason to burn it down. Migration for its own sake is just engineering vanity.

But for everything else?

For the freelancer building a client’s brochure site. For the startup that needs a landing page. For the developer who wants a portfolio. For the small business that needs an online presence with a contact form and a map. For the blogger who posts once a week.

WordPress is not the answer anymore. It’s the habit.

The tools have changed. A static site generated by AI, deployed to a global CDN, with no database, no plugins, and no server to maintain, will outperform a WordPress site in every measurable way — speed, security, cost, and reliability.

Final thoughts

WordPress isn’t dead. But it’s no longer the default. And the fact that we keep reaching for it anyway says more about our habits than about the technology.

The next time someone asks you to build a simple website, before you type wp-admin into a browser, ask yourself: does this project actually need a CMS, a database, a server, and thirty plugins? Or does it need five static pages that load in 200 milliseconds?

If the answer is the latter — and it usually is — you have better options now. Options that didn’t exist two years ago. And they’re getting better every month.

WordPress had a great run. For a lot of use cases, that run is over.

Day 23: Agentic AI in Product Management 🧠📦

Executive Summary

Product Management is fundamentally an information synthesis problem.

PMs constantly:

  • ingest noisy signals 🗣️
  • balance competing constraints ⚖️
  • make decisions with incomplete data ❓

Agentic AI does not replace product managers.

It augments them by:

  • continuously sensing inputs
  • structuring ambiguity
  • accelerating decision cycles

This chapter explains how agentic systems can be embedded into real product workflows without turning PMs into passive reviewers.

Why Product Management Is Agent-Friendly 🧩

Product work involves:

  • open-ended questions
  • multi-stakeholder inputs
  • iterative refinement

These are classic agent traits:

Observe → Interpret → Decide → Act → Learn

Static dashboards fail because product decisions are:

  • contextual
  • temporal
  • value-laden

Agents thrive where spreadsheets break.

What Product Agents Should (and Should NOT) Do 🚦

What They Should Do

  • monitor signals continuously 📊
  • surface trade-offs explicitly
  • summarize user feedback at scale
  • propose options, not answers

What They Should NOT Do

  • decide product direction autonomously ❌
  • override PM judgment
  • optimize for vanity metrics

Product strategy is a human responsibility.

Core Product Signals Agents Can Ingest 📥

Signal Type Examples
User feedback Tickets, NPS, surveys
Usage data Funnels, cohorts
Market intel Competitor launches
Engineering Velocity, incidents
Business Revenue, churn

Agents unify signals humans rarely see together.

Canonical Product Agent Architecture 🏗️

Signals (Users, Data, Market)
            ↓
     Ingestion Agents
            ↓
     Normalization Layer
            ↓
     Insight Agents
            ↓
     Trade-off Analyzer
            ↓
     PM Review & Decision

The PM remains the decision-maker.

Use Case 1: Continuous User Feedback Synthesis 🗣️🧠

Problem

Thousands of feedback items across:

  • tickets
  • app reviews
  • sales notes

Agent Behavior

  • cluster feedback by theme
  • track sentiment over time
  • flag emerging issues

Outcome

PMs see patterns, not anecdotes.

Use Case 2: Roadmap Impact Analysis 🛣️📊

Example Question

“If we delay Feature X by one quarter, what breaks?”

Agent Tasks

  • scan dependencies
  • evaluate customer commitments
  • estimate revenue risk

This turns gut feel into structured debate.

Use Case 3: PRD Drafting & Validation ✍️

Agents can:

  • draft PRD sections
  • identify unclear requirements
  • flag scope creep

But humans must:

  • approve trade-offs
  • own final decisions

Multi-Agent Product Setup 🤝

Typical configuration:

  • Signal Agent: monitors inputs
  • Insight Agent: synthesizes trends
  • Risk Agent: flags downsides
  • Narrative Agent: drafts PM-facing summaries

A manager agent orchestrates the workflow.

Example: Product Insight Agent Loop 🔁

collect_signals()
cluster_feedback()
identify_trends()
quantify_impact()
propose_options()

Note: propose options — not conclusions.

Practical Example: Feature Prioritization Agent 🧮

Inputs

  • user demand
  • effort estimates
  • revenue impact

Agent Output

Feature Impact Effort Risk
A High Medium Low
B Medium Low Medium

This supports — not replaces — prioritization frameworks.

Tools & Integrations 🔧

Common integrations:

  • Jira / Linear
  • Product analytics tools
  • Support platforms
  • CRM systems

Agents become useful only when wired into real data.

Failure Modes in Product Agents 🚨

Failure Impact
Metric fixation Short-term optimization
Feedback bias Loud users dominate
False precision Overconfident insights

Product agents must surface uncertainty.

Guardrails for Product Agents 🚧

  • human approval required
  • confidence indicators
  • explainability of insights
  • audit trails

Trust comes from transparency.

Case Study: Product Discovery Agent at Scale 📚

Context

  • B2B SaaS platform

Agent Role

  • weekly insight synthesis

Result

  • faster roadmap discussions
  • better cross-team alignment

Key takeaway

Agents improved conversation quality, not decision authority.

Measuring Success 📏

Track:

  • decision turnaround time
  • stakeholder alignment
  • PM trust score

Ignore:

  • number of summaries generated

Organizational Impact 🏢

When Done Well

  • PMs think more strategically 🧭
  • teams align faster
  • product debates improve

When Done Poorly

  • PMs disengage
  • trust erodes

Final Takeaway

Agentic AI in Product Management is about clarity, not control.

The best systems:

  • amplify judgment
  • expose trade-offs
  • respect human ownership

A great product agent doesn’t tell PMs what to build.

It helps them understand why a decision matters.

Test Your Skills

🚀 Continue Learning: Full Agentic AI Course

👉 Start the Full Course: https://quizmaker.co.in/study/agentic-ai

Etiqueta : El Universo de tu Web

Si tu sitio web fuera un organismo vivo, la etiqueta <html> sería la piel que lo contiene todo. Es el elemento raíz (root) y absolutamente nada en tu página puede existir fuera de sus fronteras.

El Rey de la Jerarquía

El navegador utiliza esta etiqueta para saber dónde empieza y dónde termina el documento. Es el ancestro común de todos los elementos (el “padre de todos”).

🛠️ Los Atributos que te hacen PRO

Un desarrollador novato deja la etiqueta vacía: <html>. Un Arquitecto Web como tu utilizas los atributos estratégicos:

  1. lang="es": (Obligatorio para Seniors) Le dice a los motores de búsqueda y a los lectores de pantalla en qué idioma está el contenido. Evita que el navegador pregunte “¿Quieres traducir esta página?”.
  2. dir="ltr": Define la dirección del texto (de izquierda a derecha). Fundamental si algún día trabajas con idiomas como el árabe (rtl).

Código : La Raíz Perfecta

<html lang="es">
  <head>
    </head>
  <body>
    </body>
</html>

Tip para el Futuro Senior:

En CSS, puedes seleccionar este elemento usando el selector :root. Es la mejor práctica para declarar tus Variables CSS (colores, tamaños, fuentes) porque garantiza que estarán disponibles para absolutamente todos los elementos de tu sitio.

How I Fixed a Race Condition in a Live Seat Booking System (And Lost Sleep Over It)

𝗧𝘄𝗼 𝘂𝘀𝗲𝗿𝘀. 𝗢𝗻𝗲 𝘀𝗲𝗮𝘁. 𝗧𝗼𝘁𝗮𝗹 𝗰𝗵𝗮𝗼𝘀.
It was a regular Tuesday on a client project — a bus ticket booking platform. Everything looked fine until a support ticket landed: two passengers had been assigned the same seat on the same trip.

My first instinct? “That can’t happen — I have a check before booking.” Famous last words.

𝗪𝗵𝗮𝘁 𝘄𝗮𝘀 𝗮𝗰𝘁𝘂𝗮𝗹𝗹𝘆 𝗵𝗮𝗽𝗽𝗲𝗻𝗶𝗻𝗴:

User A and User B both queried the seat at nearly the same millisecond. Both saw it as available. Both proceeded to book. The database updated twice — same seat, two owners.

𝗠𝘆 𝗳𝗶𝗿𝘀𝘁 (𝘄𝗿𝗼𝗻𝗴) 𝗮𝘁𝘁𝗲𝗺𝗽𝘁𝘀

I tried a simple if check before updating — reading the seat status, then writing. Seemed logical. Broke immediately under any real concurrency. Read-then-write is not atomic. The window between those two operations is exactly where race conditions live.

Then I tried a client-side flag. Also wrong. Socket.io events can arrive out of order, and the UI state is not a source of truth.

The fix: atomic update + real-time lock broadcast

The solution was to collapse the read and the write into a single atomic MongoDB operation — and pair it with a Socket.io broadcast so every connected client sees the lock instantly.

// Atomic Updates + Socket.io Real-Time Lock

async function reserveSeat(tripId, seatNo, userId) {

// Prevents Race Conditions
const trip = await Trip.findOneAndUpdate(
{ _id: tripId, “seats.no”: seatNo, “seats.status”: “available” },
{ $set: { “seats.$.status”: “locked”, “seats.$.heldBy”: userId } },
{ new: true }
);

if (!trip) throw new Error(“Seat already taken!”);

// Broadcast the ‘Lock’ to all travelers
io.emit(“seat_locked”, { tripId, seatNo });
console.log(Seat ${seatNo} secured for User ${userId});
}

𝗪𝗵𝘆 𝘁𝗵𝗶𝘀 𝘄𝗼𝗿𝗸𝘀 — 𝗹𝗶𝗻𝗲 𝗯𝘆 𝗹𝗶𝗻𝗲

1️⃣ 𝙏𝙝𝙚 𝙦𝙪𝙚𝙧𝙮 𝙘𝙤𝙣𝙙𝙞𝙩𝙞𝙤𝙣 𝙞𝙨 𝙩𝙝𝙚 𝙜𝙪𝙖𝙧𝙙
“seats.status”: “available” means MongoDB only updates if the seat is still available at the exact moment of the write. If two requests arrive simultaneously, only one will match — the other gets null.

2️⃣ 𝙛𝙞𝙣𝙙𝙊𝙣𝙚𝘼𝙣𝙙𝙐𝙥𝙙𝙖𝙩𝙚 𝙞𝙨 𝙖𝙩𝙤𝙢𝙞𝙘 𝙖𝙩 𝙩𝙝𝙚 𝙙𝙤𝙘𝙪𝙢𝙚𝙣𝙩 𝙡𝙚𝙫𝙚𝙡
MongoDB guarantees that the find-and-update happens as a single operation. No other write can slip in between. This is the core of the fix.

3️⃣ 𝙏𝙝𝙚 𝙣𝙪𝙡𝙡 𝙘𝙝𝙚𝙘𝙠 𝙞𝙨 𝙮𝙤𝙪𝙧 𝙚𝙧𝙧𝙤𝙧 𝙗𝙤𝙪𝙣𝙙𝙖𝙧𝙮
If trip is null, the seat was already taken. Throw a clear error — don’t silently fail. The client can catch this and show the user a friendly message.

4️⃣ 𝙎𝙤𝙘𝙠𝙚𝙩.𝙞𝙤 𝙗𝙧𝙤𝙖𝙙𝙘𝙖𝙨𝙩𝙨 𝙩𝙝𝙚 𝙡𝙤𝙘𝙠 𝙞𝙢𝙢𝙚𝙙𝙞𝙖𝙩𝙚𝙡𝙮
Once the DB confirms the lock, we emit seat_locked to all connected clients. Every user’s seat map updates in real time — no polling, no stale UI.

𝗧𝗵𝗲 𝗯𝗶𝗴𝗴𝗲𝘀𝘁 𝗹𝗲𝘀𝘀𝗼𝗻
Race conditions don’t announce themselves. They hide behind low traffic and happy paths. This bug only surfaced because two users happened to book at the same moment — something that’s guaranteed to happen at scale.

The fix isn’t just about MongoDB. It’s a mindset shift: never trust a read before a write in a concurrent system. Push your guard logic into the database operation itself, where it’s atomic.

𝐻𝑎𝑣𝑒 𝑦𝑜𝑢 𝑟𝑢𝑛 𝑖𝑛𝑡𝑜 𝑟𝑎𝑐𝑒 𝑐𝑜𝑛𝑑𝑖𝑡𝑖𝑜𝑛𝑠 𝑖𝑛 𝑦𝑜𝑢𝑟 𝑜𝑤𝑛 𝑝𝑟𝑜𝑗𝑒𝑐𝑡𝑠? 𝐼’𝑑 𝑙𝑜𝑣𝑒 𝑡𝑜 ℎ𝑒𝑎𝑟 ℎ𝑜𝑤 𝑦𝑜𝑢 ℎ𝑎𝑛𝑑𝑙𝑒𝑑 𝑖𝑡.