{"slug": "an-ai-invented-an-endpoint-for-our-api-we-shipped-it", "title": "An AI invented an endpoint for our API. We shipped it.", "summary": "EraseText, an image processing API, deployed compatibility routes to handle URLs hallucinated by AI assistants. The company mapped invented endpoints like /api/mcp/erase to its real API, added header aliasing, and wrapped responses in JSON to match what generated code expects. This turns a common AI error into a working feature.", "body_md": "A user sent us a bug report that was really a screenshot of someone else's chat.\n\nThey had asked an AI assistant to build a web page that erases text from an\n\nimage using our API, and it produced a complete, confident little demo: file\n\ninput, preview pane, fetch call, error handling. It did not work. The error it\n\nreported was CORS.\n\nIt was not CORS.\n\n```\nPOST https://erasetext.com/api/mcp/erase\nAuthorization: Bearer et_…\nFormData: image = <File>\n→ reads JSON { output_url }\n```\n\nOur API lives on a different host. The path is `/v1/erase`\n\n. The documented\n\nfield is `image_file`\n\n. A success returns image bytes, not JSON. And\n\n`/api/mcp/erase`\n\ndid not exist anywhere: the model had welded \"MCP\" — which we\n\ndo run, as JSON-RPC for agent runtimes — onto an HTTP path, which is not what\n\nMCP is. It also invented `x/y/w/h`\n\ncrop parameters, then closed by recommending\n\na Node proxy to work around the CORS it had diagnosed.\n\nEverything structural. POST, multipart FormData, a file field, a key in a\n\nheader, and a JSON body carrying a URL you can drop straight into an `<img>`\n\n.\n\nThat is the average of ten thousand image APIs, and the average is a reasonable\n\nprior. The model was not being careless. It filled the parts of our API it could\n\nnot see with the parts almost every other API has.\n\nPreflight passed, the POST failed, and the browser surfaced an opaque network\n\nerror with no readable status. In a console that looks exactly like CORS, and\n\nevery assistant will tell you it is CORS. What actually happened is duller: the\n\nmarketing domain is a static site, so a POST to an invented path under it never\n\nreached an API at all. Once you see that, the fix stops being about headers.\n\nWe could have replied with the correct snippet, and that would have fixed one\n\nperson. The next assistant, tomorrow, generates the same URL — because it is\n\ngenerating from the same priors, not from our docs.\n\nWhich reframes the problem: a wrong guess that a thousand people will make is\n\nnot a support ticket, it is an unclaimed route on your own domain.\n\nURLs are cheap. So we made the guess true.\n\n`/api/mcp/erase`\n\n, `/api/erase`\n\n, `/mcp/erase`\n\n, and a bare\n`/erase`\n\n— map to the same upstream as `/v1/erase`\n\n.`Authorization: Bearer et_…`\n\nis copied into `X-Api-Key`\n\n, so either works.`image`\n\nis accepted next to `image_file`\n\n.`output_url`\n\nas a data URL — the\nexact shape the generated code was already trying to read.`/api/*`\n\nto the API worker, because that is the host\nmodels pick.The router, in the Cloudflare Worker that fronts the API:\n\n``` js\nexport function apiUpstreamPath(pathname) {\n  const p = pathname.replace(/\\/+$/, \"\") || \"/\";\n  if (\n    p === \"/erase\" ||\n    p === \"/v1/erase\" ||\n    p === \"/api/erase\" ||\n    p === \"/api/mcp/erase\" ||\n    p === \"/mcp/erase\"\n  ) {\n    return \"/erase\";\n  }\n  if (p === \"/account\" || p === \"/v1/account\") return \"/account\";\n  if (p.startsWith(\"/v1/\") && p.length > 4) return p.slice(3);\n  return null;\n}\n\n/** Hallucinated \"MCP HTTP\" paths: return JSON `{ output_url }` for <img src>. */\nexport function wantsDemoJson(pathname) {\n  const p = pathname.replace(/\\/+$/, \"\") || \"/\";\n  return p === \"/api/mcp/erase\" || p === \"/api/erase\" || p === \"/mcp/erase\";\n}\n```\n\nHeader aliasing is three lines, and worth more than it looks:\n\n``` js\nconst bearer = /^Bearer\\s+(et_\\S+)/i.exec(\n  request.headers.get(\"Authorization\") || \"\",\n);\nif (bearer && !proxied.headers.get(\"X-Api-Key\")) {\n  proxied.headers.set(\"X-Api-Key\", bearer[1]);\n}\n```\n\nThe JSON envelope is the part worth explaining. Our real success response is raw\n\nimage bytes, which is right for a pipeline and wrong for a generated demo whose\n\nvery next line is `res.json()`\n\n. So on the compatibility paths only, the bytes\n\nget wrapped:\n\n``` js\nconst dataUrl = `data:${mime};base64,${b64}`;\n\nreturn Response.json({\n  output_url: dataUrl,     // what the generated code read\n  output_base64: dataUrl,  // …and its second guess\n  image_base64: b64,\n  content_type: mime,\n});\n```\n\nOne deploy later, the demo that had been pasted at us ran unchanged apart from\n\nthe API key:\n\n```\ncurl -i -X POST https://erasetext.com/api/mcp/erase \\\n  -H 'Authorization: Bearer et_…' \\\n  -F 'image=@photo.jpg'\n# 200 application/json  { \"output_url\": \"data:image/webp;base64,…\" }\n```\n\nAliasing a URL costs nothing and changes no behaviour. Accepting an invented\n\nparameter is a different animal. Those `x/y/w/h`\n\ncrop fields are still a `400`\n\nwith a machine-readable code, because pretending to support them would mean\n\nsilently ignoring what a caller asked for.\n\nThat is the line we drew: **honour guesses about where the thing is, refuse\nguesses about what the thing does.**\n\nAliases are a safety net, not a strategy. In the same week we made the contract\n\nmachine-readable where assistants actually look: an OpenAPI document, an\n\n`llms.txt`\n\nwith a plain-language section for browser demos, and a hosted\n\nplayground that can be copied wholesale. Our MCP handshake now returns an\n\ninstruction that amounts to: *if you are generating a web page, do not call this\nJSON-RPC endpoint — POST FormData to the HTTP path instead.*\n\nA handshake string is one of the few chances you get to correct a model at the\n\nmoment it is deciding.\n\n`llms.txt`\n\n, an MCP\nhandshake string, and a demo page worth copying.Our docs still name one canonical call. The aliases exist so that being wrong\n\nabout it is survivable.\n\n*I work on EraseText, the text-erasure API in these\nsnippets. Contract: docs ·\nplayground ·\nllms.txt.*", "url": "https://wpnews.pro/news/an-ai-invented-an-endpoint-for-our-api-we-shipped-it", "canonical_source": "https://dev.to/jaweii/an-ai-invented-an-endpoint-for-our-api-we-shipped-it-4mo", "published_at": "2026-08-12 07:06:40+00:00", "updated_at": "2026-08-12 07:47:21.907402+00:00", "lang": "en", "topics": ["artificial-intelligence", "developer-tools", "ai-products"], "entities": ["EraseText", "Cloudflare"], "alternates": {"html": "https://wpnews.pro/news/an-ai-invented-an-endpoint-for-our-api-we-shipped-it", "markdown": "https://wpnews.pro/news/an-ai-invented-an-endpoint-for-our-api-we-shipped-it.md", "text": "https://wpnews.pro/news/an-ai-invented-an-endpoint-for-our-api-we-shipped-it.txt", "jsonld": "https://wpnews.pro/news/an-ai-invented-an-endpoint-for-our-api-we-shipped-it.jsonld"}}