Skip to content

feat: ProtocolSettlement sends X-ATXP-App-Name header to auth#167

Merged
badjer merged 3 commits into
mainfrom
protocol-settlement-app-name
Apr 20, 2026
Merged

feat: ProtocolSettlement sends X-ATXP-App-Name header to auth#167
badjer merged 3 commits into
mainfrom
protocol-settlement-app-name

Conversation

@badjer
Copy link
Copy Markdown
Contributor

@badjer badjer commented Apr 20, 2026

Summary

Companion to auth#254. ProtocolSettlement now sends an X-ATXP-App-Name header on every /settle/* and /verify/* request so auth can attribute settlement observability events to the calling service (llm, music, search, etc.).

ProtocolSettlement has two construction sites in this repo:

  1. Direct — callers like LLM construct it themselves.
  2. Inside atxpExpress — the express middleware instantiates it per-request for tool servers using @atxp/express.

This PR plumbs appName through both paths:

  • New ProtocolSettlementOptions with appName?: string as the 5th (options bag) arg on the constructor. Positional-compatible, no breaking change.
  • New appName?: string field on ATXPConfig so atxpExpress({ appName: 'music' }) works.
  • atxpExpress.ts:124 forwards config.appName into new ProtocolSettlement(...).

Resolution order

Documented on both ProtocolSettlementOptions and ATXPConfig:

1. explicit option value, if non-empty after trim
2. process.env.APP_NAME, if non-empty after trim
3. header omitted

An explicit empty string ({ appName: '' }) disables the env fallback — useful for tests and multi-service processes.

Why env fallback?

Follows the SDK's existing Pattern-A precedent — NODE_ENV drives the default for allowHttp / allowInsecureRequests across oAuthResource.ts, atxpFetcher.ts, atxpClient.ts, serverConfig.ts, with explicit options exposed as overrides. This is the established convention for non-secret env-driven defaults. (Pattern B — silent env reads with no API exposure — is reserved for secrets like ALCHEMY_API_KEY and ATXP_OPAQUE_KEY.)

Practical benefit: every service in our ecosystem already sets APP_NAME for turtle's PostHog events. With this SDK bump, auth-side observability attribution works with zero code changes in LLM, tool servers, etc.

Implementation notes

  • appName added to BuildableATXPConfigFields alongside minimumPayment — both are optional fields that have no DEFAULT_CONFIG value, consistent precedent.
  • Resolved once in the constructor (not re-read per request) so mid-process process.env mutation can't change behavior mid-flight.
  • Added to both verify() and settle() for consistency — both are observability surfaces auth would want attributed.

Test plan

  • npx vitest run src/protocol.test.ts in atxp-server — 33/33 passing (7 new under X-ATXP-App-Name header describe):
    • sends header when explicit option set
    • falls back to process.env.APP_NAME
    • explicit option overrides env
    • explicit empty string disables env fallback
    • omits header when neither set
    • trims whitespace-only values to undefined
    • applies to verify() as well as settle()
  • npx vitest run in atxp-express — 45/45 passing
  • Package-wide: atxp-server (15 files), atxp-express (4), atxp-common (12), atxp-client (17) — all passing
  • npx tsc --noEmit — clean in atxp-server and atxp-express
  • npm run lint — clean, no new warnings

Rollout

  1. Merge + release this SDK change.
  2. Auth-side PR (auth#254) merges — auth starts reading the header (no-op until SDK rollout).
  3. Bump @atxp/server / @atxp/express in LLM, tool servers, any ProtocolSettlement consumers.
  4. Build the "Payment revenue by app" Honeycomb dashboard keyed on payment.app_name.

badjer and others added 3 commits April 20, 2026 13:49
…d /verify/*

Lets auth (see circuitandchisel/auth#254) attribute settlement
observability events to the calling service — so Honeycomb/PostHog
dashboards can slice revenue by llm / music / search / etc. Companion
to the auth-side change that reads the header.

ProtocolSettlement is used by two call paths:
  1. Direct construction by callers (LLM today)
  2. Hardcoded inside @atxp/express's middleware (tool servers)

Both need to be able to set the app name. This PR plumbs it through
both:

- New ProtocolSettlementOptions with `appName?: string` as 5th
  constructor arg (options bag, backwards-compatible positional add).
- New `appName?: string` field on ATXPConfig (exposed via ATXPArgs, so
  `atxpExpress({ appName: 'music' })` works).
- atxpExpress forwards config.appName into `new ProtocolSettlement(...)`.

Resolution order (documented on both surfaces):
  1. explicit option value, if set to non-empty string
  2. process.env.APP_NAME, if set to non-empty string
  3. header omitted

An explicit empty string disables the env fallback (useful in tests).

The env fallback follows the Pattern-A precedent already established
in the SDK (NODE_ENV → allowHttp / allowInsecureRequests in
oAuthResource.ts, atxpFetcher.ts, atxpClient.ts, serverConfig.ts).
Callers that already set APP_NAME for turtle's PostHog events get
correct auth observability with zero code changes on SDK bump.

appName added to BuildableATXPConfigFields alongside minimumPayment
since both are optional fields that have no DEFAULT_CONFIG value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xpress test)

Per PR review:
1. Export ProtocolSettlementOptions from @atxp/server's index.ts — the new
   type was previously only deep-importable.
2. Switch header name to X-ATXP-APP-NAME to align with the existing
   X-ATXP-PAYMENT / X-ATXP-TOKEN screaming-case precedent in the SDK.
   Wire-level it's case-insensitive, but downstream log/metric pipelines
   key on the literal casing.
3. Rewrite the "resolved once at construction" comment — accurate for
   long-lived callers (LLM), but @atxp/express instantiates
   ProtocolSettlement per-request so env IS re-read each time. The old
   comment overpromised.
4. Add an atxp-express integration test that actually sends an MCP
   request with a payment credential, stubs global fetch, and asserts
   the outgoing /settle/* fetch carries the X-ATXP-APP-NAME header when
   config.appName is set (and omits it when neither config nor env has
   a value). Closes the loop on the one-line forwarding glue.

No runtime behavior change beyond the header casing.
Cleans up 4 stragglers from the earlier casing switch that weren't
caught in the first sweep:

- types.ts JSDoc on ATXPConfig.appName
- protocol.ts JSDoc on ProtocolSettlementOptions.appName
- protocol.ts inline comment on buildHeaders
- protocol.test.ts test title

Also adds a "Expected format" note to both JSDocs pointing at the
format auth's readAppNameHeader enforces (1–64 chars, [a-zA-Z0-9._-]+).
The SDK doesn't validate — it trusts callers and sends whatever
non-empty trimmed string it has — so a value outside that format
produces a missing span attribute on the auth side rather than a
failed settle. The docstring flags this so operators debugging absent
observability know where to look.

No runtime behavior change.
@badjer badjer merged commit e4656a0 into main Apr 20, 2026
1 check passed
@badjer badjer deleted the protocol-settlement-app-name branch April 20, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant