Skip to main content
Glama

Fetch L402-protected URL

l402_fetch

Fetch URLs protected by the L402 protocol, automatically handling Bitcoin Lightning payments from your session budget.

Instructions

Fetch a URL that may require a Bitcoin Lightning payment (L402 protocol). Side effect: deducts sats from the session budget when a payment is required — check l402_balance first if budget is limited. Flow: sends request → if 402 received, pays the Lightning invoice (1 attempt) → retries once with payment proof → returns response body as text. Fails with error if: budget is exhausted, URL is unreachable, or the Lightning payment fails. Do NOT use for regular (non-L402) URLs — use a standard fetch tool instead. Do NOT use if l402_balance shows 0 sats remaining.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesThe URL to fetch (http or https)
methodNoHTTP method — GET, POST, PUT, DELETE, PATCH. Default: GET
bodyNoRequest body as string (for POST/PUT requests)
headersNoAdditional HTTP request headers as key-value pairs

Implementation Reference

  • The MCP tool handler function for 'l402_fetch'. Calls L402Client.fetch() with the provided url, method, body, and headers, then returns the response text with a '[Paid X sats]' prefix if a payment was made.
      async ({ url, method, body, headers }) => {
        const httpMethod = (method ?? "GET").toUpperCase();
        try {
          const res = await requireClient().fetch(url, {
            method: httpMethod,
            body,
            headers: (headers ?? {}) as Record<string, string>,
          });
    
          const text = await res.text();
          const report = requireClient().spendingReport();
          const spent = report?.transactions.at(-1)?.sats ?? 0;
    
          return {
            content: [
              {
                type: "text" as const,
                text: spent > 0
                  ? `[Paid ${spent} sats] HTTP ${res.status}\n\n${text}`
                  : `HTTP ${res.status}\n\n${text}`,
              },
            ],
          };
        } catch (err) {
          return {
            content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
            isError: true,
          };
        }
      }
    );
  • Input schema for the l402_fetch tool, defining url (required), method, body, and headers (all optional) using Zod validators.
    inputSchema: {
      url:     z.string().describe("The URL to fetch (http or https)"),
      method:  z.string().optional().describe("HTTP method — GET, POST, PUT, DELETE, PATCH. Default: GET"),
      body:    z.string().optional().describe("Request body as string (for POST/PUT requests)"),
      headers: z.record(z.string(), z.string()).optional().describe("Additional HTTP request headers as key-value pairs"),
    },
    annotations: {
      readOnlyHint: false,
      idempotentHint: false,
      openWorldHint: true,
    },
  • mcp/server.ts:107-161 (registration)
    Registration of the 'l402_fetch' tool via server.registerTool(), including its title, description, inputSchema, annotations, and the handler callback.
    // Tool: l402_fetch
    server.registerTool(
      "l402_fetch",
      {
        title: "Fetch L402-protected URL",
        description:
          "Fetch a URL that may require a Bitcoin Lightning payment (L402 protocol). " +
          "Side effect: deducts sats from the session budget when a payment is required — check l402_balance first if budget is limited. " +
          "Flow: sends request → if 402 received, pays the Lightning invoice (1 attempt) → retries once with payment proof → returns response body as text. " +
          "Fails with error if: budget is exhausted, URL is unreachable, or the Lightning payment fails. " +
          "Do NOT use for regular (non-L402) URLs — use a standard fetch tool instead. " +
          "Do NOT use if l402_balance shows 0 sats remaining.",
        inputSchema: {
          url:     z.string().describe("The URL to fetch (http or https)"),
          method:  z.string().optional().describe("HTTP method — GET, POST, PUT, DELETE, PATCH. Default: GET"),
          body:    z.string().optional().describe("Request body as string (for POST/PUT requests)"),
          headers: z.record(z.string(), z.string()).optional().describe("Additional HTTP request headers as key-value pairs"),
        },
        annotations: {
          readOnlyHint: false,
          idempotentHint: false,
          openWorldHint: true,
        },
      },
      async ({ url, method, body, headers }) => {
        const httpMethod = (method ?? "GET").toUpperCase();
        try {
          const res = await requireClient().fetch(url, {
            method: httpMethod,
            body,
            headers: (headers ?? {}) as Record<string, string>,
          });
    
          const text = await res.text();
          const report = requireClient().spendingReport();
          const spent = report?.transactions.at(-1)?.sats ?? 0;
    
          return {
            content: [
              {
                type: "text" as const,
                text: spent > 0
                  ? `[Paid ${spent} sats] HTTP ${res.status}\n\n${text}`
                  : `HTTP ${res.status}\n\n${text}`,
              },
            ],
          };
        } catch (err) {
          return {
            content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
            isError: true,
          };
        }
      }
    );
  • The L402Client.fetch() method — the core runtime logic handling the L402 flow: cached tokens, initial request, 402 parsing, payment via wallet, token caching, and retry with credentials.
      async fetch(url: string, init: RequestInit = {}): Promise<Response> {
        // Try cached token first
        const cached = this.tokenStore.get(url);
        if (cached) {
          const res = await this._fetchWithToken(url, init, cached.macaroon, cached.preimage);
          if (res.status !== 402) return res;
          // Token rejected — clear cache and fall through to fresh payment
          this.tokenStore.set(url, { macaroon: "", preimage: "" });
        }
    
        // Initial unauthenticated request
        const res402 = await fetch(url, init);
    
        if (res402.status !== 402) return res402;
    
        // Parse 402 response — supports both L402 and x402 formats
        const { macaroon, invoice, priceSats } = await this._parse402(res402);
    
        // Check budget before paying
        if (this.budget && priceSats !== undefined) {
          this.budget.check(url, priceSats);
        }
    
        // Pay invoice
        let preimage: string;
        try {
          const result = await this.wallet.payInvoice(invoice);
          preimage = result.preimage;
        } catch (err) {
          throw new L402PaymentError(`Payment failed: ${String(err)}`, invoice);
        }
    
        // Record spend
        if (this.budget && priceSats !== undefined && priceSats > 0) {
          this.budget.record(url, priceSats);
        }
    
        // Cache token for future calls
        this.tokenStore.set(url, { macaroon, preimage });
    
        // Retry with credentials
        for (let attempt = 0; attempt < this.maxRetries; attempt++) {
          const retryRes = await this._fetchWithToken(url, init, macaroon, preimage);
          if (retryRes.status !== 402) return retryRes;
        }
    
        throw new L402PaymentError("Server rejected payment after retries", invoice);
      }
    
      private async _fetchWithToken(
        url: string,
        init: RequestInit,
        macaroon: string,
        preimage: string,
      ): Promise<Response> {
        return fetch(url, {
          ...init,
          headers: {
            ...(init.headers ?? {}),
            "Authorization": `L402 ${macaroon}:${preimage}`,
          },
        });
      }
    
      private async _parse402(res: Response): Promise<{ macaroon: string; invoice: string; priceSats?: number }> {
        let body: Record<string, unknown> = {};
        try { body = await res.clone().json() as Record<string, unknown>; } catch { /* non-JSON 402 */ }
    
        // L402 format: { macaroon, invoice, priceSats }
        const macaroon  = body.macaroon as string | undefined;
        const invoice   = (body.invoice ?? body.paymentRequest ?? body.payment_request) as string | undefined;
        const priceSats = (body.priceSats ?? body.price_sats) as number | undefined;
    
        // x402 (Coinbase) format: X-Payment-Required header with JSON
        if (!macaroon || !invoice) {
          const xHeader = res.headers.get("X-Payment-Required") ?? res.headers.get("x-payment-required");
          if (xHeader) {
            try {
              const xData = JSON.parse(xHeader) as Record<string, unknown>;
              const xInvoice  = xData.invoice as string | undefined;
              const xMacaroon = xData.macaroon ?? xData.token ?? xData.payment_token;
              if (xInvoice && xMacaroon) {
                return { macaroon: String(xMacaroon), invoice: xInvoice };
              }
            } catch { /* malformed header */ }
          }
        }
    
        if (!macaroon) throw new L402ParseError("402 response missing macaroon field", body);
        if (!invoice)  throw new L402ParseError("402 response missing invoice field", body);
    
        return { macaroon, invoice, priceSats };
      }
    }
  • Python Pydantic input schema (_L402FetchInput) for the l402_fetch LangChain tool, with url, method, and body fields.
    class _L402FetchInput(BaseModel):
        url: str = Field(description="The URL to fetch. Must be an L402-protected API endpoint.")
        method: str = Field(default="GET", description="HTTP method: GET, POST, PUT, DELETE")
        body: Optional[str] = Field(default=None, description="Request body as JSON string (for POST/PUT)")
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses side effect (deducting sats), describes the flow (request→payment→retry), and lists failure modes. Adds significant context beyond annotations, which only indicate non-read-only, non-idempotent, open-world.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Concise, front-loaded with purpose, uses clear structure with flow explanation and explicit 'Do NOT' warnings. Every sentence serves a purpose without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, the description explains return value (response body as text) and lists failure conditions. Covers all necessary information for an agent to use the tool effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the description adds little over the schema's own parameter descriptions. The description mentions body as string and headers as key-value pairs, but these are already in the schema. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool fetches URLs that may require Bitcoin Lightning payment (L402 protocol), distinguishing it from standard fetch tools and sibling tools like l402_balance.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly advises not to use for regular URLs, suggests checking l402_balance first, and warns against use when budget is zero. Provides clear when-to-use and when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ShinyDapps/l402-kit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server