Getting started with x402Fetch

Make x402 payments with a TypeScript/JavaScript client on XRPL.

Step 1: Install dependencies

npm install x402-xrpl xrpl dotenv

Step 2: Set your environment variables

Create a .env file with your configuration:

RESOURCE_URL=http://localhost:8080/hello
XRPL_NETWORK=xrpl:1
XRPL_BUYER_SEED=sEd... # your wallet seed
XRPL_TESTNET_WS_URL=wss://s.altnet.rippletest.net:51233
XRPL_SKIP_FAUCET=false
XRPL_TESTNET_FAUCET_URL=https://faucet.altnet.rippletest.net/accounts

Step 3: Create a paid fetch client

Create a client.js file (if you're using ESM imports, set "type": "module" in your package.json or use .mjs).

import dotenv from "dotenv";
import { Wallet } from "xrpl";

import {
  decodePaymentRequiredHeader,
  decodePaymentResponseHeader,
  x402Fetch,
} from "x402-xrpl";

dotenv.config({ override: false });

const resourceUrl = process.env.RESOURCE_URL ?? "http://127.0.0.1:8080/hello";
const network = process.env.XRPL_NETWORK ?? "xrpl:1";
const seed = process.env.XRPL_BUYER_SEED;

const faucetUrl =
  process.env.XRPL_TESTNET_FAUCET_URL ?? "https://faucet.altnet.rippletest.net/accounts";
const skipFaucet = ["1", "true", "yes"].includes(
  String(process.env.XRPL_SKIP_FAUCET ?? "false").toLowerCase(),
);

function wsUrlForNetwork(net) {
  if (net === "xrpl:0") {
    return process.env.XRPL_MAINNET_WS_URL ?? "wss://s1.ripple.com:51233";
  }
  if (net === "xrpl:1") {
    return process.env.XRPL_TESTNET_WS_URL ?? "wss://s.altnet.rippletest.net:51233";
  }
  if (net === "xrpl:2") {
    return process.env.XRPL_DEVNET_WS_URL ?? "wss://s.devnet.rippletest.net:51233";
  }
  throw new Error(`unsupported_network:${net}`);
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fundWalletIfNeeded(address) {
  if (skipFaucet) return;
  if (network !== "xrpl:1") return;

  // eslint-disable-next-line no-console
  console.log(`Funding buyer wallet via faucet: ${address}`);

  const resp = await fetch(faucetUrl, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ destination: address }),
  });

  // eslint-disable-next-line no-console
  console.log(`Faucet response: ${resp.status}`);

  // Ledger confirmation is asynchronous; give it a moment.
  await sleep(10_000);
}

async function main() {
  if (!seed) {
    throw new Error("XRPL_BUYER_SEED is required");
  }

  const buyer = Wallet.fromSeed(seed);

  // eslint-disable-next-line no-console
  console.log(`Buyer: ${buyer.classicAddress}`);
  // eslint-disable-next-line no-console
  console.log(`Resource URL: ${resourceUrl}`);

  await fundWalletIfNeeded(buyer.classicAddress);

  const fetchPaid = x402Fetch({
    wallet: buyer,
    networkFilter: network,
    schemeFilter: "exact",
    wsUrlForNetwork,
  });

  const resp = await fetchPaid(resourceUrl, {
    method: "GET",
    headers: { accept: "application/json" },
  });

  // eslint-disable-next-line no-console
  console.log(`\nHTTP ${resp.status}`);
  // eslint-disable-next-line no-console
  console.log(await resp.text());

  const paymentResponse = resp.headers.get("PAYMENT-RESPONSE");
  if (paymentResponse) {
    const decoded = decodePaymentResponseHeader(paymentResponse);
    // eslint-disable-next-line no-console
    console.log("\nDecoded PAYMENT-RESPONSE:");
    // eslint-disable-next-line no-console
    console.log(JSON.stringify(decoded, null, 2));
    return;
  }

  const paymentRequired = resp.headers.get("PAYMENT-REQUIRED");
  if (paymentRequired) {
    const decoded = decodePaymentRequiredHeader(paymentRequired);
    // eslint-disable-next-line no-console
    console.log("\nDecoded PAYMENT-REQUIRED:");
    // eslint-disable-next-line no-console
    console.log(JSON.stringify(decoded, null, 2));
  }
}

main().catch((err) => {
  // eslint-disable-next-line no-console
  console.error(err);
  process.exitCode = 1;
});

Step 4: Run the client

node client.js

Your client will automatically handle the HTTP 402 flow by building a presigned XRPL Payment tx blob and retrying the request with a PAYMENT-SIGNATURE header.