Skip to content

facilitator: implement GET /supported - #143

Open
Elizabethxxx wants to merge 1 commit into
Miracle656:mainfrom
Elizabethxxx:feat/facilitator-supported-endpoint-124
Open

facilitator: implement GET /supported#143
Elizabethxxx wants to merge 1 commit into
Miracle656:mainfrom
Elizabethxxx:feat/facilitator-supported-endpoint-124

Conversation

@Elizabethxxx

Copy link
Copy Markdown
Contributor

Summary

Lens is currently only an x402 resource server (a seller) — src/middleware/x402.ts delegates verify and settle to https://facilitator.stellar.org. This PR implements the first piece of the other side of that interface: GET /supported, the capability-discovery route every x402 client hits first to decide whether a facilitator can serve them.

This is metadata only — it moves no money and holds no keys.

What changed

  • Added src/routes/facilitator.ts registering GET /supported.
  • The response is typed as SupportedResponse from @x402/core (kinds, extensions, signers) rather than hand-rolled — src/routes/facilitator.ts imports the type directly and the branch type-checks clean against it.
  • Both networks are advertised: kinds contains one entry each for stellar:pubnet and stellar:testnet, matching the CAIP-2 ids already used in src/middleware/x402.ts's NETWORK resolution (driven by STELLAR_NETWORK / dual-network config, not a second source of truth).
  • Each kind's extra reports areFeesSponsored, mirroring what ExactStellarScheme.getExtra() from @x402/stellar returns, configurable via FACILITATOR_FEES_SPONSORED (default true).
  • signers is populated from an optional FACILITATOR_SIGNER_ADDRESSES env var (comma-separated), or an empty object when unset. I deliberately did not instantiate @x402/stellar's facilitator-side ExactStellarScheme here — its constructor requires real signing keys, and capability discovery must not require live keys to answer. Once verify/settle land in a follow-up, this route should read signers from that same registered scheme instance instead of a separate env var.
  • The route is registered with config: { public: true } (bypasses API-key auth, same convention as /status and /metrics) and lives outside the GATED_ROUTES prefix list in middleware/x402.ts, so it is never x402-gated — a facilitator cannot charge for its own capability discovery.
  • Documented the new env vars in .env.example.

Comparison against facilitator.stellar.org/supported

I could not reach facilitator.stellar.org from my sandbox (outbound DNS is blocked in this environment), so I was not able to diff a live response byte-for-byte. Instead I matched the shape by reading the shipped types: SupportedResponse and SupportedKind in node_modules/@x402/core/dist/cjs/mechanisms-*.d.ts, and ExactStellarScheme.getExtra()/getSigners() in node_modules/@x402/stellar/dist/cjs/exact/facilitator/index.d.ts, which is what facilitator.stellar.org itself is built on. The response this route returns is structurally identical to what x402Facilitator.getSupported() would produce for a facilitator that has ExactStellarScheme registered on both stellar:pubnet and stellar:testnet. I'd appreciate a maintainer diff against the live endpoint if one is available, and I'm happy to adjust if anything differs (e.g. exact x402Version value, key ordering, or additional extra fields the live facilitator includes).

Tests

src/tests/facilitator.test.ts covers:

  • 200 with no payment header (never x402-gated)
  • response shape (kinds/extensions/signers present, correct types)
  • both networks present with scheme "exact"
  • areFeesSponsored present and toggleable via env
  • signers empty by default, populated correctly when FACILITATOR_SIGNER_ADDRESSES is set
  • no auth required

Test plan

  • npx tsc --noEmit passes
  • npx vitest run src/tests/facilitator.test.ts src/tests/middleware/x402.test.ts passes (19/19)
  • Maintainer diff against a live facilitator.stellar.org/supported response

closes #124

Lens is currently only an x402 resource server (a seller). This adds
the first piece of the facilitator side of the interface: GET /supported,
the metadata route every x402 client hits first to decide whether Lens
can serve them.

The response matches SupportedResponse from @x402/core (kinds,
extensions, signers) rather than a hand-rolled shape. It advertises the
exact scheme for both stellar:pubnet and stellar:testnet, driven by the
existing dual-network convention used in middleware/x402.ts, and reports
areFeesSponsored in each kind's extra to mirror what
ExactStellarScheme.getExtra() reports from @x402/stellar.

Signer addresses are optional and read from FACILITATOR_SIGNER_ADDRESSES
since this route is pure capability discovery and must not require live
signing keys to answer. The route is registered outside the x402
gating plugin's matched prefixes and marked config.public, so it is
neither payment-gated nor API-key gated — a facilitator cannot charge
for its own capability discovery.

closes Miracle656#124
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@Elizabethxxx Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning in the doc comments here is the best of any PR in this batch — particularly the point that capability discovery must answer without live signing keys, and must not itself be gated behind payment. A client has to be able to call /supported before it has any payment method configured. That's exactly right and easy to get wrong.

Two things.

1. Direct collision with #142

@Anambraboi-1's #142 also implements GET /supported, in src/api/facilitator.ts, and both files export a function named registerFacilitatorRoutes. Same route path, same exported symbol, two different files, both registered from src/index.ts. Whichever merges second either fails to compile or silently shadows the other depending on import order — this won't resolve itself in a rebase.

Not your fault; two people picked up overlapping issues. But it needs deciding before either lands, so please sync with @Anambraboi-1.

2. This advertises networks that may not be configured — and that's the substantive difference

const kinds: SupportedKind[] = (['mainnet', 'testnet'] as const).map(network => ({
  x402Version: 2, scheme: 'exact', network: STELLAR_NETWORK_IDS[network], extra,
}))

Both networks are advertised unconditionally. #142 registers a scheme only when that network has a FACILITATOR_SECRET_KEY, and derives /supported from what is actually registered.

That difference matters: with no mainnet key configured, this route tells a client stellar:pubnet is supported, the client selects mainnet, and /verify then fails. The client did nothing wrong — we advertised a capability we don't have. For an RFP judged on spec compliance, a /supported that overstates is worse than one that returns fewer kinds.

Suggested resolution — take the best half of each:

  • Keep #142's derivation from registered schemes, so the answer is always truthful.
  • Keep your framing that the route must answer without live keys and must stay ungated — and your tests, which #142 is thinner on.

Concretely, that probably means this PR narrows to its tests plus the doc comments, applied against #142's implementation. Which is a slightly unsatisfying outcome for the work you did, so I want to be clear the analysis here is good — it's the duplication that forces a choice, not the quality.

Minor

FACILITATOR_SIGNER_ADDRESSES as a separate env var means the advertised signers can drift from the keys actually loaded. If it derives from the registered schemes instead, the two can't disagree.

Same @ts-ignore note as #142: prefer @ts-expect-error so it removes itself once the types resolve, and it's worth checking whether moduleResolution in tsconfig.json is the real fix — your comment says the @x402 packages ship ESM-only types, which is usually a resolution-mode problem rather than a genuinely missing type.

@Miracle656

Copy link
Copy Markdown
Owner

Following up on my review — I've made the call on the /supported collision so this isn't left waiting on a conversation between two PRs.

Decision: /supported lands in #142, not here. The deciding factor is the one substantive difference between the two implementations: facilitator.getSupported() derives its answer from the schemes actually registered, so it cannot advertise stellar:pubnet when no mainnet key is configured. This version lists both networks unconditionally, which tells a client we support a network we'd then fail to verify on.

That's not a knock on the work. Your reasoning is the better-argued of the two — particularly that discovery must answer without live signing keys and must not itself be gated behind payment. A client has to call /supported before it has any payment method at all, and that's easy to get wrong. #142 has the right derivation but none of that written down.

What I'd like this PR to become: your tests and doc comments, applied against #142's implementation. Concretely:

That leaves a smaller diff than you wrote, which I know is an unsatisfying outcome. To be clear about the reason: it's duplication forcing a choice, not a quality judgement. Two people picked up overlapping issues and that's on how they were scoped, not on you.

If you'd rather not do the rework, say so and I'll port the tests and comments across myself with attribution to you — either is fine, I'd just rather ask than assume.

One note if you do rework: FACILITATOR_SIGNER_ADDRESSES as a separate env var lets the advertised signers drift from the keys actually loaded. Deriving them from the registered schemes means the two can't disagree — worth carrying over as a suggestion to #142.

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.

x402 facilitator: implement GET /supported

2 participants