Skip to content

feat: discover all protocols + l402.space bridge note#49

Merged
reneaaron merged 12 commits into
masterfrom
feat/discover-all-protocols
Jul 14, 2026
Merged

feat: discover all protocols + l402.space bridge note#49
reneaaron merged 12 commits into
masterfrom
feat/discover-all-protocols

Conversation

@reneaaron

@reneaaron reneaaron commented Jun 23, 2026

Copy link
Copy Markdown
Member

What

Two changes so an agent (or human) with only a lightning wallet can discover and pay for services on any 402 protocol:

  1. discover returns all protocols. Drop the server-side payment_asset=BTC filter so discover returns L402, x402, and MPP services, not just lightning ones.
  2. fetch transparently bridges non-lightning services. lightning-tools already pays L402, MPP, and lightning-payable x402 directly; for endpoints it can't settle over lightning (e.g. USDC-only x402) it hands back an unpaid 402. When that happens, fetch retries once through the l402.space bridge, which re-wraps the upstream as an L402 lightning challenge it can pay. Native L402 is still paid directly and never touches the bridge.

No new flags, no manual URL-encoding, no protocol knowledge required — fetch <service-url> just works regardless of protocol.

Why

Per #26, x402 / MPP services were silently filtered out of discovery, and there was no way to pay them from a lightning wallet. The bridge fallback is an internal implementation detail, so callers never construct a special URL or reason about rails.

Test

  • yarn test — 109 pass, 2 skipped (3 new tests in fetch-bridge.test.ts cover: bridge retry on a 402, direct pay on success with no bridge call, and no double-bridging an l402.space URL).
  • Live discover returns x402 services (previously empty).
  • Verified the bridge live: https://l402.space/<encoded-x402-url> returns 402 + WWW-Authenticate: L402 + a lightning invoice.

Companion skill update: getAlby/payments-skill#28.

Refs #26

Summary by CodeRabbit

  • New Features

    • Expanded API discovery to include services payable through supported Lightning-compatible payment rails.
    • Automatically routes compatible non-Lightning services through a Lightning payment bridge.
    • Preserves direct access for services that natively support Lightning payments.
    • Exposes each service’s payment network information.
  • Bug Fixes

    • Excludes services that cannot be settled through supported payment rails.
    • Retains the original search result count while returning only payable services.

Drop the server-side payment_asset=BTC filter so discover returns L402,
x402 and MPP services. Non-lightning services (e.g. x402/USDC) can be
paid in sats by fetching them through the l402.space bridge.

Also surface payment_network in results so callers can tell which rail a
service settles on and decide whether the bridge is needed.

Refs #26
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@reneaaron, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b04751aa-a9d2-4be9-b521-be4d19f20ff0

📥 Commits

Reviewing files that changed from the base of the PR and between 829de6f and f64290e.

📒 Files selected for processing (1)
  • src/tools/lightning/discover.ts
📝 Walkthrough

Walkthrough

The discover flow now searches 402index.io broadly, filters results to Lightning-native or bridge-settleable services, wraps bridgeable URLs through l402.space, preserves the index total, and exposes payment network data.

Changes

Payable service discovery

Layer / File(s) Summary
Rail resolution and bridged output
src/tools/lightning/discover.ts, src/commands/discover.ts
Discovery normalizes payment rails, over-fetches index results, filters payable services, wraps bridgeable URLs, accepts nullable payment_network, and preserves the original index total.
Bridge and payability validation
src/test/discover-bridge.test.ts
Tests verify bridge URL wrapping, unsupported-rail filtering, native Lightning handling, mixed results, total preservation, and payment_network passthrough.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Discover as discover()
  participant Index as 402index.io
  participant Bridge as l402.space
  Discover->>Index: Fetch over-fetched services
  Index-->>Discover: Return services and total
  Discover->>Bridge: Construct wrapped URLs for bridgeable services
  Bridge-->>Discover: Return l402.space URL form
  Discover-->>Discover: Filter payable services and preserve total
Loading

Possibly related PRs

  • getAlby/cli#13: Introduced the discover command and its initial lightning discovery implementation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: broader discovery plus l402.space bridge handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/discover-all-protocols

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

When lightning-tools hands back an unpaid 402 (e.g. a USDC-only x402
endpoint it can't settle over lightning), retry once through the
l402.space bridge, which re-wraps the upstream as an L402 lightning
challenge we can pay. Native L402 and lightning-payable x402/MPP are
still paid directly and never touch the bridge - no flag, no manual URL
encoding for the caller.

Refs #26
It was added so callers could decide whether to use the bridge, but
fetch now bridges non-lightning services transparently, so the rail is
an implementation detail. protocol already covers what a service is.
Comment thread src/tools/lightning/fetch.ts Outdated
@rolznz

rolznz commented Jul 2, 2026

Copy link
Copy Markdown
Member

@reneaaron as a first step I think we don't need any changes in the CLI - instead only on the skill side (don't less mention lightning, and mention l402.space can enable paying to x402/MPP endpoints)

Address PR feedback (rolznz): the transparent bridge retry in fetch was
hidden from callers, which breaks macaroon/x402 token reuse for follow-up
calls (js-lightning-tools#328) since the token would belong to the bridge
URL, not the original.

Instead, wrap non-lightning discover results (x402 on Base/Stellar/EVM)
in an explicit l402.space bridge URL up front. The URL a caller fetches is
now visible and consistent across follow-up requests, and fetch pays it as
a plain L402 endpoint with no per-request redirection. L402/MPP and x402
on the Lightning network keep their native URL. Also surfaces
payment_network in the output so callers see the rail.

- discover.ts: wrap non-lightning URLs; revert fetch.ts to master
- replace fetch-bridge.test.ts with discover-bridge.test.ts
@reneaaron

Copy link
Copy Markdown
Member Author

Reworked per the feedback — moved the bridging out of fetch into discover.

  • fetch.ts reverted to master — no more hidden per-request retry. That was the piece that could break follow-up/macaroon reuse (js-lightning-tools#328): a token minted by the bridge would've been replayed against the original URL.
  • discover now wraps non-lightning results in an explicit l402.space/<encoded> URL (x402 on Base/Stellar/EVM). L402/MPP and x402-on-Lightning keep their native URL. The URL a caller fetches is now visible and stable across follow-up calls, and fetch just pays it as a plain L402 endpoint. Also surfaces payment_network so callers see the rail. This also lines up with a future switch to l402.space for discovery recommendations.
  • Swapped fetch-bridge.test.tsdiscover-bridge.test.ts.

On your first-step point: I kept the discover filter-drop since that's what surfaces x402/MPP at all — without it the skill note about l402.space has nothing to act on. Happy to split the skill-only change out first if you'd rather land these in sequence.

MPP (like x402) is payment-network agnostic: the index lists MPP services
on Stripe/USD and with no declared rail, none of which settle over
lightning. The previous predicate treated MPP as natively lightning and
left those unwrapped, so they'd fail to pay from a lightning wallet.

Decide by payment_network instead: native only when the rail is Lightning
(handling multi-rail values like "Lightning, Base"), with L402 kept native
by definition even when the index reports no network. Validated against
2000 live 402index.io services: all x402/MPP on Base/eip155/stellar/
Solana/Polygon/Stripe/None bridge; L402 (any network) and x402-on-Lightning
stay native; zero L402-on-non-lightning anomalies.
Comment thread src/tools/lightning/discover.ts
@rolznz

rolznz commented Jul 6, 2026

Copy link
Copy Markdown
Member

On your first-step point: I kept the discover filter-drop since that's what surfaces x402/MPP at all — without it the skill note about l402.space has nothing to act on. Happy to split the skill-only change out first if you'd rather land these in sequence.

That is fine 🤖 👍

Bridging a Stripe/USD or Stellar endpoint is pointless - the bridge can't
pay those upstreams, so the wrapped URL would just 402. Whitelist the
rails l402.space reports as funded (/api/info fundedNetworks: base, solana,
tempo, lightning) instead of bridging everything non-lightning.

Three-way decision per service:
- native (pay directly): L402, or any rail == lightning
- bridged: rail in {base, solana, tempo} (eip155:8453 aliased to base;
  "Base, Solana" multi-rail supported)
- unwrapped: stellar, polygon, stripe, testnets (Base Sepolia != base),
  unknown/None - not payable from a lightning wallet, left as-is

Validated the bucketing against 2000 live 402index.io services.
reneaaron added a commit to getAlby/payments-skill that referenced this pull request Jul 6, 2026
Match getAlby/cli#49 (Option B): discover now returns services on a
bridge-funded rail with their l402.space URL already set, so agents just
fetch the result's url - no manual encoding. Scope the bridge's reach to
the rails l402.space actually funds (USDC on Base/Solana, stablecoin on
Tempo, plus native Lightning/L402) instead of implying any endpoint works;
call out that Stellar/Polygon/Stripe aren't payable from lightning.

Also merges master (resolves the stale conflict on these sections) and
bumps 1.4.0 -> 1.4.1.
reneaaron added 3 commits July 6, 2026 17:01
Filter out services on rails the wallet can't reach (not native lightning
and not a bridge-funded rail), so every discover result is payable in sats
by fetching its url as-is - agents never need to reason about rails or the
bridge. Native lightning keeps its own url; bridge-funded rails
(Base/Solana/Tempo) come back already wrapped in l402.space.

The index can't filter by rail server-side (payment_network is ignored;
payment_asset can't tell USDC-on-Base from USDC-on-Stellar), so we filter
client-side and over-fetch 2x (capped at the index's 200/page) to still
return up to the requested limit. Verified live: a 20-result request comes
back with 20 payable services, no Stellar/Polygon/Stripe leaking through.
Per review feedback (rolznz): MPP payment challenges are incompatible with
L402 and can't be folded into one, so MPP upstreams need the gateway's
dedicated inbound endpoint. The path prefix selects the rail we pay the
gateway over, and we always pay via lightning - so MPP goes through
/mpp-lightning/ (hands back a lightning invoice), while x402 stays on the
default path (gateway folds a lightning L402 challenge). /mpp-tempo/ would
demand a Tempo stablecoin we can't pay from a lightning wallet.

Verified live against a real MPP/Tempo upstream: /mpp-lightning/ returns a
402 with a lnbc invoice; discover now emits mpp-lightning-prefixed URLs for
MPP services and default-prefixed URLs for x402.
Consumer testing flagged that total == services.length made agents read
'total: 10' as 'only 10 services exist'. Report the index's match count for
the query instead, so the caller knows the corpus is larger than the
returned (payability-filtered, sliced) page.
Lightning-native x402 exists (e.g. x402.albylabs.com/demo/quote, which
offers a lnbc invoice directly in its x402 challenge) and must never be
bridged. The index currently reports that service's rail as "Lightning",
which we already keep unwrapped - but it passes raw CAIP-2 ids through for
other rails (eip155:8453 for Base), and a lightning-native challenge's own
network id is bip122:000000000019d6689c085ae165831e93 (Bitcoin mainnet).
Alias that id to lightning so such a service stays direct-paid rather than
routed through the bridge with its markup. Also covers MPP-on-Lightning
with a test (already handled by the rail check, previously untested).
Comment thread src/test/discover-bridge.test.ts Outdated
Per review: the gateway's /mpp-* endpoints select the *inbound* rail (how
you pay the gateway), not the upstream protocol - and the default route
folds a lightning L402 challenge for MPP upstreams too (verified live: a
wrapped MPP/Tempo upstream returns L402 + lnbc with rail "mpp-tempo").
So non-lightning MPP shares the plain bridge path with x402, and MPP on
the Lightning rail is still paid directly, never bridged.
@reneaaron reneaaron marked this pull request as ready for review July 14, 2026 09:14
@reneaaron

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/tools/lightning/discover.ts (2)

44-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider early return for empty paymentNetwork to avoid [""] array.

When paymentNetwork is empty or null, (paymentNetwork ?? "").split(",") produces [""]. While harmless in the current logic (since "" does not match "lightning" or the funded rails), returning an empty array is safer and avoids potential gotchas if the network mapping logic is expanded in the future.

♻️ Proposed refactor
 function paymentRails(paymentNetwork: string | null): string[] {
-  return (paymentNetwork ?? "").split(",").map((rail) => {
-    const normalized = rail.trim().toLowerCase();
-    return NETWORK_ALIASES[normalized] ?? normalized;
-  });
+  if (!paymentNetwork) return [];
+  return paymentNetwork.split(",").map((rail) => {
+    const normalized = rail.trim().toLowerCase();
+    return NETWORK_ALIASES[normalized] ?? normalized;
+  });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/lightning/discover.ts` around lines 44 - 49, Update paymentRails to
return an empty string array immediately when paymentNetwork is null or empty,
before splitting and normalizing values. Preserve the existing alias mapping and
normalization behavior for non-empty paymentNetwork values.

84-84: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider fetching a higher baseline margin to better fulfill requestedLimit.

With requestedLimit * 2, if you request 10 services and more than half of the first 20 index results are on unsupported rails, the function will return fewer than 10 results even if more payable services exist further down the page.

Since the API limit is 200 and the JSON payload is very small, fetching a higher minimum (e.g., 50) adds negligible overhead but significantly increases the likelihood of satisfying small requests.

♻️ Proposed refactor
-  const fetchLimit = Math.min(200, requestedLimit * 2);
+  const fetchLimit = Math.min(200, Math.max(50, requestedLimit * 2));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/lightning/discover.ts` at line 84, Increase the baseline fetch
limit in the discovery flow around fetchLimit so small requestedLimit values
retrieve at least 50 results, while continuing to cap the value at the API
maximum of 200 and scaling larger requests appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/tools/lightning/discover.ts`:
- Around line 44-49: Update paymentRails to return an empty string array
immediately when paymentNetwork is null or empty, before splitting and
normalizing values. Preserve the existing alias mapping and normalization
behavior for non-empty paymentNetwork values.
- Line 84: Increase the baseline fetch limit in the discovery flow around
fetchLimit so small requestedLimit values retrieve at least 50 results, while
continuing to cap the value at the API maximum of 200 and scaling larger
requests appropriately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4488bf1c-4f7b-49d6-9c9e-a34903dca74f

📥 Commits

Reviewing files that changed from the base of the PR and between 4dee856 and 829de6f.

📒 Files selected for processing (3)
  • src/commands/discover.ts
  • src/test/discover-bridge.test.ts
  • src/tools/lightning/discover.ts

With limit*2 alone, discover -l 3 fetched a 6-row page that could be (and
in practice was) entirely unpayable rails, returning zero services despite
1000+ matches. Floor the fetched page at 50 (still capped at the index's
200) so the payability filter has enough raw material. Also make
paymentRails return [] for a null/empty network instead of [""] - no
behavior change (callers only do membership checks), just an honest
contract.
@reneaaron reneaaron merged commit 97055c4 into master Jul 14, 2026
2 checks passed
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.

2 participants