facilitator: implement GET /supported - #143
Conversation
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
|
@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! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
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.
|
Following up on my review — I've made the call on the Decision: 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 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: |
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
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:
Test plan
closes #124