Three Hand-Maintained CORS Lists, Four Missed Lessons

Browser Agents fail silently due to omitted CORS headers; lists must be derived from code and mechanically verified.
Starting from four CORS incidents the minia2a gateway hit within four months, the article explains that browser-side Agent paid calls produce "blank server logs" because the preflight fails on a missing header entry. It outlines the three hand-maintained CORS lists (Allow-Headers, Expose-Headers, Allow-Methods) and their failure consequences, stressing such failures leave no server-side trace and health checks stay green. Since x402 and MCP put pricing, receipts, and trial identity in headers, active protocol evolution keeps the lists drifting. The durable fix: stop trusting the list, use grep to derive the universe from code that writes and reads headers, classify every name into "listed / same-origin only / non-client signal / known open," and use fixture tests with reverse assertions to keep the table honest; finally verify the online binary matches source with an OPTIONS preflight.
A browser-side Agent initiates a paid call, yet the server log is completely blank—no 402, no 400, no timeout. The request never left the browser: the preflight request failed on a header name missing from the list. This is the fourth time the minia2a gateway team has encountered the same problem in their own production environment, and it is the origin of this article.

What Each of the Three CORS Lists Governs
CORS is enforced by the browser; the server does not participate. Three response headers answer three different questions, each a hand-maintained list:
| Response Header | Question Answered | Consequence of a Missing Entry |
|---|---|---|
| Access-Control-Allow-Headers | Which request headers can the browser send? | Preflight fails, the call is never sent |
| Access-Control-Expose-Headers | Which response headers can JavaScript read? | The call succeeds, but the client cannot see pricing, receipts, or refund status |
| Access-Control-Allow-Methods | Which HTTP methods can the browser use? | PUT/PATCH/DELETE fail at the preflight stage |
None of these three failure types produces an error on the server. This is the crux of the problem: the server either receives a well-formed request or receives nothing at all, so all server-side health checks remain green. The only visible alarm is the browser console message "Request header field X is not allowed by Access-Control-Allow-Headers," and the person who sees it is often not the person maintaining the list.
Preflight request is an OPTIONS probe that the browser automatically sends before a "non-simple request" to confirm with the server: is this origin, this method, and these request headers allowed? A non-simple request is one that uses methods like PUT/DELETE, custom request headers (such as X-Payer-Signature), or a non-standard Content-Type. In the preflight, the browser declares "what I intend to send next" via Access-Control-Request-Method and Access-Control-Request-Headers, and the server responds with "what I allow" via Access-Control-Allow-Methods and Access-Control-Allow-Headers. If even one item does not match, the browser terminates the process before the actual request is sent—this is also the root cause of the "server log is completely blank" described in the article: what was blocked is the preflight, and the business request never arrived. For Agent calls with custom headers, preflight is almost inevitably triggered, making it the first and most easily missed gatekeeper on the entire chain.
Same-origin policy is the premise for CORS's existence: by default, the browser prohibits scripts from one origin from reading responses from another origin. CORS is not a server mechanism that relaxes this restriction; rather, it is a protocol layer where the server "actively declares willingness to be accessed cross-origin"; the enforcer is always the browser, and the server merely responds accordingly. This explains a counterintuitive phenomenon—server-side code, unit tests, and curl all work fine, but calls fail only in a real browser. The article's emphasis that "CORS is enforced by the browser; the server does not participate" is precisely a reminder that when troubleshooting, you cannot look only at the server side: the server can only observe CORS-related issues when it actually receives the preflight and has a chance to log it, and most gateways do not log preflights separately.
Why Agent APIs Are Particularly Prone to Pitfalls
The x402 and MCP protocols put all the interesting parts in headers: price quotes are PAYMENT-REQUIRED, receipts are PAYMENT-RESPONSE, trial identity is a signature plus timestamp plus agent id, and asynchronous tasks are tokens. Every one of these must appear in the list—and a protocol still evolving will continuously add new headers.
Server-to-server Agents are completely unaware of this, while browser-side Agents expose the problem immediately: the preflight request fails before the 402 challenge is even retrieved. The more active the protocol, the heavier the maintenance burden of hand-maintained lists, and the higher the probability of omissions.
x402 is a paid-call protocol based on the HTTP 402 Payment Required status code: on the client's first request, the server returns 402 with pricing terms (such as the PAYMENT-REQUIRED header); after completing payment, the client retries with payment credentials, and the server then returns a receipt via the PAYMENT-RESPONSE header. Its characteristic is that it places negotiation information in HTTP headers rather than the request body, making it highly sensitive to CORS's Expose-Headers and Allow-Headers. MCP (Model Context Protocol) is a protocol for interaction between models and tools/data sources, often carrying identity, session, and task tokens in request headers. The common engineering consequence of both is: each evolution of the protocol may add new headers, while the CORS list is a static manual whitelist, creating an inherent time lag between the two. Server-to-server Agents construct HTTP requests directly, bypassing the browser, and thus are completely unaware of the whitelist's existence; once the same Agent logic is moved into the browser (or the gateway is called by in-page scripts), missing entries are amplified into complete failure at the preflight stage. The more active the protocol and the more headers there are, the more frequent this drift of "code already uses it, list has not added it" becomes.
The Complete Record of Four Omissions
The minia2a team disclosed the dates and specific missing items of four incidents:
- 2026-10-02: The response list omitted
PAYMENT-REQUIREDandPAYMENT-RESPONSE. The browser client could get the 402 but could not parse the acceptable payment terms. - 2026-10-14: The request list omitted
X-Payer-Signature. The preflight failed before the 402 challenge was retrieved. - 2026-10-17: The request list omitted
X-Trial-Identity. The browser client fell back to putting the wallet address in the query string—exactly the parameter conflict this header was introduced to eliminate. - 2026-10-18: A triple hit.
X-Minia2a-Refundwas not in the response list,X-Agent-IDwas not in the request list (even though it was written in their own published quickstart), andPUTwas not in the method list, despite the route being registered.
Each fix was just adding one word to the list. What is truly alarming is that after each fix, the same class of problem recurred, because there is no connection between the list and the code that generates the headers.
Replace Patching with a Census
The durable fix is to stop trusting the list and instead derive the universe from the code that emits or reads headers. On the minia2a gateway, this means three grep scans:
# 1. Response headers written by code
grep -rhoE 'c\.Header\("[A-Za-z0-9-]+"' internal/httpapi/
# 2. Request headers read by code (literals, and parsed constant names)
grep -rnE '(c\.GetHeader|Header\.Get)\(' internal/
# 3. Registered HTTP methods
grep -rhoE '\.(GET|POST|PUT|PATCH|DELETE)\(' internal/httpapi/router.go
The census itself is also prone to the same kind of error as the list. The first version only matched direct c.Header("…") calls, while three trial-related headers—X-Trial-Remaining, X-Trial-Max, X-Trial-Almost-Exhausted—were emitted in a loop from a helper function returning map[string]string. All three were written into the documentation but invisible to the check, and the check still printed clean. The current census reads map literal keys and index assignments, and prints the location of every c.Header(variable), making dynamic edges visible rather than assumed away.
Next, classify each client-facing name into exactly one bucket: listed, same-origin only (with a rationale for not needing a list entry), non-client signal. Any unclassified name makes the check fail. Intentionally missing names go into a fourth bucket, "known open," which carries a reverse assertion: on the day that name appears in the live list, the check fails until the entry is removed. Otherwise the table will lie in the other direction.
There is another control worth keeping: run the judge with fixtures rather than only against production. minia2a's test suite has 17 cases, including "a known-open entry that is now allowed must fail" and "a header that was once in the live list and then disappeared must fail." Without reverse cases, the census can only report the past.
Confirm the Source You Read Is the Binary Being Served
Reading the source is only half the work. The compiled binary may lag behind the source: someone changes the middleware, no one rebuilds, and all static checks still pass. A single preflight request can answer this question:
curl -s -D - -o /dev/null -X OPTIONS https://minia2a.uk/x402/gas \
-H 'Origin: https://minia2a.uk' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: X-Agent-ID'
The response is 204 plus three response headers. Compare these three lists with those in the source. If they differ, the binary is older than the code tree you are reading. For minia2a, the same probe also became the acceptance test for every fix: send the headers you think are allowed, read back the echoed allowed list, and stop guessing.
General Rules
A hand-maintained list is a promise that the runtime cannot check for you. Every header an Agent sends or reads is part of the API surface, and every list entry is a place where the surface and the promise may silently drift. Derive the universe from the code, classify every member, and make the table fail when a known gap closes—the failures the server never sees are exactly the ones worth a mechanical check.
According to minia2a's disclosure, the checks described in this article were run against its deployed gateway source and first-party preflights; the header names and route paths all come from its production configuration and do not involve any customer data.
One-line Summary
Browser Agents fail silently due to omissions in CORS header lists; lists must be derived from code and mechanically verified.
Paragraph Summary
Starting from four CORS incidents that the minia2a gateway encountered within four months, the article points out that the reason paid calls from browser-side Agents produce "blank server logs" is that the preflight request fails on a missing header entry. The author outlines the problems governed by each of CORS's three hand-maintained lists (Allow-Headers, Expose-Headers, Allow-Methods) and the consequences of failure, emphasizing that such failures leave no trace on the server and health checks always stay green. Because x402 and MCP place critical information such as pricing, receipts, and trial identity in headers, the continuous evolution of active protocols causes the hand-maintained lists to drift constantly. The durable solution is to stop trusting the list and use grep to derive the universe from the code that writes and reads headers, classify every name into one of four categories—"listed / same-origin only / non-client signal / known open"—and use fixture tests with reverse assertions to prevent the table from lying in either direction; finally, use an OPTIONS preflight to verify that the online binary matches the source.
Related articles

Random Pruning of the DPO Computation Graph: 1.4x Training Speedup and Lower Peak Memory
DPO training too slow and memory-hungry? This paper proposes a random pruning framework for the DPO computation graph, sampling independently across position, sequence, and branch dimensions; branch pruning achieves a 1.426x step-time speedup and cuts training time by about 30%, with in-depth attribution of memory and time gains.

Why Does JEPA Underperform MLM in Longitudinal EHR Representation Learning?
New study compares JEPA and MLM for longitudinal EHR representation learning on MIMIC-IV: three JEPA variants train stably without collapse but underperform MLM; target size, not heterogeneity, drives prediction error.

Transfer Learning + Visible Light Communication: A New Approach to Energy-Efficient Indoor Positioning
A study proposes a transfer learning-based visible light communication indoor positioning method, achieving 47% accuracy improvement on the BOSCH factory dataset, 67.5% model efficiency under high noise, and introducing the ME composite metric for energy-efficient positioning in energy-constrained industrial IoT scenarios.