ECO-639: Document the X-Agent-Info attribution header - #1144
Conversation
Adds a dedicated page covering the optional X-Agent-Info request header (decision_origin, integration_name, version), the rules that mark it malformed, and the x-agent-info-status response contract used to confirm it parsed. Cross-linked from the swapping API integration guide, troubleshooting page, and the Uniswap AI overview. ECO-639
|
Thanks for contributing to Uniswap Docs! 🦄 Quick heads up on how this repo works: the docs you see on developers.uniswap.org are built from a separate source repo. When we accept a change here, we port it over there, so your fix might show up on the live site before it lands in this repo. Updates to this repo come in batches on a regular cadence. When yours is included, you'll be credited as a co-author on the commit, so the contribution counts on your GitHub profile and in Uniswap/docs. We review every PR but can't merge everything. Either way, someone from the docs team will follow up here once we've taken a look. One legal bit: by submitting a PR you agree your contribution is licensed under the repo's MIT license. |
wkoutre
left a comment
There was a problem hiding this comment.
Reviewed every documented rule against the shipped parser on Uniswap/backend main (f839c7996d) — packages/lib/golang/middlewares/server/xagentinfo.go and packages/services/entry-gateway-go/src/libs/apikeymetrics/emitter.go.
The headline: this page was written against Uniswap/backend#11003, which was closed unmerged on 2026-08-18. What actually shipped is #11941, a Go-only gateway parser with no response header and no TypeScript parser. So the request-header half of the page is largely accurate, and the response-contract half documents behavior that does not exist. Details in the inline comments.
What matched the parser
- 1024-byte cap on the raw value, checked before parsing — matches (unit wording aside).
decision_originmust be exactlyautonomousorhuman_mediated, case-sensitive — matches the literal constants.- Payload must be a JSON object; arrays, scalars and
nullare malformed — matches. - Unknown keys dropped rather than rejected — matches.
- Optional fields present but not strings are malformed, including explicit
null— matches, and the parser goes out of its way to makenulldistinguishable from absent. - Empty header value is absent, not malformed — matches.
- A malformed header carries no payload and never echoes input — matches.
- Both worked examples parse to
ok.
What did not
Enumerated inline: the response header does not exist; the printable-US-ASCII rule, the authenticated-key gate, and the duplicate-header-line joining are all absent from the page; the 256 cap is UTF-16 code units rather than characters; the disallowed-character set is incomplete; free-text fields land in a warehouse with no guidance on what not to put in them.
llms.txt — Medium, unanchorable. The ticket's "Done when" requires the content to reach llms.txt, and no such file exists in this repo. The PR description infers the private publishing pipeline regenerates it from content/ the way the archived Docusaurus setup did, which is plausible but unverified from here. Since the whole premise of the ticket is that agents discover this header through agent-readable indexes, "probably generated downstream" is a thin basis for calling the distribution requirement done. Worth one question to whoever owns that pipeline before this is marked ready — a confirmed yes closes the criterion, and a no means this PR does not actually satisfy the ticket.
House conventions — checked and clean, no findings. <Callout title="…" type="info"> matches existing usage (type="info" appears 37 times across content/), tables are normal in the Trading pages, the /docs/trading/... link prefix matches every neighbouring link, and frontmatter shape matches. The nav insertion in meta.json is valid JSON and sits sensibly after Integration Guide. Giving this its own page rather than folding it into the integration guide was the right call at this size.
Recommendation. Keep it in draft. The cleanest split is to ship the request-header half now — it is genuinely useful and mostly correct — with the parse rules corrected, and hold the response contract until a backend PR actually lands x-agent-info-status. That change currently has no owner and no open PR, which is worth surfacing on the project rather than leaving inside a docs ticket. I have updated ECO-639 to record the sequencing.
Not flagging anything as blocking beyond that; no REQUEST_CHANGES since the fix depends on a scope decision that is yours.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 63ac4c52e9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| | `integration_name` | string | No | Name of your integration or agent, e.g. `my-trading-bot`. Up to 256 characters. | | ||
| | `version` | string | No | Version identifier for your integration. Up to 256 characters. | |
There was a problem hiding this comment.
Document optional field limits in bytes
When integration_name or version contains multibyte UTF-8 characters, the backend parser applies the 256 limit to the encoded byte length, not the character count documented here. For example, a 200-character CJK name satisfies this table but is silently marked malformed, so specify a maximum of 256 UTF-8 bytes for both fields.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The cap is 256 UTF-16 code units (String#length), not bytes: the parser's utf16Len counts units and its comment gives the counter-examples for a byte count. Fixed the table to say so. Your CJK example does get marked malformed, but by two other rules that are now documented: the raw header must be printable ASCII, and the whole raw value is capped at 1024 bytes, so 200 escaped CJK characters exceed it.
…avior Adds the printable-ASCII, duplicate-header, and full disallowed-character rules to the malformed list. Corrects the field cap to UTF-16 code units and the size cap to raw bytes. Replaces the extra-keys guidance with an explicit rule against identifying values. Fixes the claim that x-agent-info-status only appears on successful responses; it also rides the gateway's own error responses. Makes the curl example's request body valid JSON.
The header is set after routing and before any response is written, not before validation. Note that an unrouted 404 does not carry it.
…lippage autoSlippage is optional in the published OpenAPI spec. The required set is type, amount, tokenInChainId, tokenOutChainId, tokenIn, tokenOut, swapper.
…list is not Uniswap-only The gateway writes no 404 of its own. Unknown paths fall through to the default upstream, which answers after the status header is stamped, so a 404 carries it. Verified against production: GET /nope returns 404 with x-agent-info-status: malformed. Only a CORS preflight skips it. The CORS allow-list also holds a few partner domains, so "Uniswap origins" overstated it.
| @@ -0,0 +1,94 @@ | |||
| --- | |||
| title: Agent Attribution (X-Agent-Info) | |||
| description: Send the optional X-Agent-Info header to attribute agent-driven Trading API traffic, and check x-agent-info-status to confirm it was recognized. | |||
There was a problem hiding this comment.
Frontmatter claims a response header that doesn't appear to exist. This description (and the "Confirming it was received" section below) documents an x-agent-info-status response header. Per the PR's own description, this header shipped in Uniswap/backend#11003, which was closed unmerged and superseded by #11941 (gateway parser only) — a git grep x-agent-info-status on backend main reportedly returns nothing. If that's accurate, this page documents a feature that was never shipped. Please confirm against the current backend implementation before merging (the PR author flagged this as "not ready to merge" for this exact reason).
|
|
||
| The gateway sets this header after it routes your request, and before it writes its own response. Error responses carry it too — a 400, a 401, a 403, a 429, and a 404 for a path that does not exist. So you can debug the header without first getting a working quote. A CORS preflight carries no status header, because the gateway answers it before it looks at `X-Agent-Info`. | ||
|
|
||
| Call the Trading API server-to-server. The gateway sends CORS headers only to a small allow-list of origins, so a browser page on your own domain cannot read this header. Check it from a server or with curl. | ||
|
|
||
| ```typescript | ||
| const response = await fetch('https://trade-api.gateway.uniswap.org/v1/quote', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'x-api-key': 'YOUR_API_KEY', | ||
| 'Content-Type': 'application/json', | ||
| 'X-Agent-Info': JSON.stringify({ | ||
| decision_origin: 'autonomous', | ||
| integration_name: 'my-trading-bot', | ||
| version: '1.4.0', | ||
| }), | ||
| }, | ||
| body: JSON.stringify({ | ||
| tokenIn: '0x...', | ||
| tokenOut: '0x...', | ||
| tokenInChainId: 1, | ||
| tokenOutChainId: 1, | ||
| type: 'EXACT_INPUT', | ||
| amount: '1000000', | ||
| swapper: '0x...', | ||
| slippageTolerance: 0.5, | ||
| }), | ||
| }); | ||
|
|
||
| if (response.headers.get('x-agent-info-status') === 'malformed') { | ||
| // Received but dropped — check field names, decision_origin value, and length limits above. | ||
| console.warn('X-Agent-Info was sent but not recognized.'); | ||
| } | ||
|
|
||
| const quote = await response.json(); | ||
| ``` |
There was a problem hiding this comment.
This entire section describes an x-agent-info-status response header, including exact values (malformed), timing guarantees relative to CORS preflight and error responses (400/401/403/429/404), and a worked TypeScript example that branches on it. If this header was never actually shipped (see comment on the frontmatter above — the PR description says the implementing PR was closed unmerged and superseded by a gateway-only parser), this whole section is fabricated behavior that will mislead integrators: they'll write code that checks a header that never arrives and silently always treat requests as unattributed. This should be corrected or removed before merge, not just flagged in the PR description.
| - `integration_name` or `version` contains a disallowed character. Those are control characters (C0 `0x00`–`0x1F`, DEL `0x7F`, C1 `0x80`–`0x9F`), the Unicode line separators U+2028 and U+2029, and the replacement character U+FFFD. An escaped unpaired surrogate decodes to U+FFFD, so it is rejected too. Text pasted from a PDF, or left behind by a lossy re-encode, often carries one of these invisibly. | ||
|
|
||
| Not sending the header at all, or sending it with an empty value, isn't an error condition — both are simply "no attribution," the same outcome as a header that's out of scope for your client. | ||
|
|
||
| ## Confirming it was received | ||
|
|
||
| Because the request succeeds regardless of whether `X-Agent-Info` parsed, check the `x-agent-info-status` response header to confirm your header was actually recognized: |
There was a problem hiding this comment.
Per the PR description, some of these parse rules ("Several parse rules are also missing or stated in the wrong unit") may not match the shipped parser (xagentinfo.go in Uniswap/backend). I can't independently check that private repo from here, so I can't confirm which specific bullet(s) are off (e.g. the 1024-byte cap, the ASCII range check, the 256 UTF-16 code unit limit, or the disallowed-character set). Someone with access to the current backend implementation should diff each bullet against the real validation logic before merging — a wrong limit here (e.g. byte cap vs. character cap) would cause integrators' otherwise-valid headers to silently fail attribution.
|
|
||
| The API is specific about request header validation. In particular, ensure that your `accept` and `content-type` headers only include the value `application/json`. For a complete example of properly formatted request headers, see the authentication section of the [Developer Dashboard](https://developers.uniswap.org/dashboard). | ||
|
|
||
| If you're sending the optional `X-Agent-Info` attribution header and it isn't being picked up, check the response for an `x-agent-info-status: malformed` header — see [Agent Attribution](/docs/trading/swapping-api/agent-attribution#confirming-it-was-received) for the full set of rules that make the header malformed. A malformed `X-Agent-Info` never causes an error response; the request proceeds without attribution. |
There was a problem hiding this comment.
This also references the x-agent-info-status: malformed response header. If that header turns out not to exist in production (see the flag on agent-attribution.mdx), this troubleshooting tip should be reverted along with the new page.
Review summaryDo not merge as-is — the PR author's own description flags this as not ready, and the diff confirms why. Main issue (high confidence, self-flagged by the author): The new Secondary issue: The author also notes some of the parse rules in "What makes a header malformed" (byte cap, ASCII range, UTF-16 length limit, disallowed characters) may be missing or stated in the wrong unit relative to the actual Everything else checked out fine:
Once the |
Linear: ECO-639
Summary
Adds a Swapping API page documenting
X-Agent-Info, an optional request header that lets an integration declare its Trading API calls were made by an AI agent rather than a person, so agent-driven volume can be measured separately.The new page
content/trading/swapping-api/agent-attribution.mdxcovers the three-field schema (decision_origin,integration_name,version), the rules that mark a header malformed, and a cURL plus TypeScript example.meta.jsonadds it to the nav after Integration Guide, and short cross-links point to it fromintegration-guide.mdx,common-errors.mdx, anduniswap-ai/overview.mdx.Docs are the adoption mechanism here rather than an afterthought: the integrations this header exists to measure are largely built by agents, and an agent wiring up the Trading API reads the docs.
Type of change
How has this been verified?
Checked the documented rules against the shipped parser,
packages/lib/golang/middlewares/server/xagentinfo.goonUniswap/backendmain. Both worked examples parse clean against it.meta.jsonis valid JSON and every added link resolves to a real heading.Anything else reviewers should know?
Not ready to merge. The page's "Confirming it was received" section documents an
x-agent-info-statusresponse header that does not exist: it lived inUniswap/backend#11003, which was closed unmerged and superseded by#11941(gateway parser only).git grep x-agent-info-statuson backend main returns nothing. Several parse rules are also missing or stated in the wrong unit. Details are in the review comments on this PR.