x402-stacks

Use when implementing HTTP 402 payment-required endpoints on Stacks blockchain, setting up micropayment clients with STX or sBTC tokens, handling x402 protocol wallet setup, building browser/frontend dApps with @stacks/connect wallet signing, or integrating x402 payments in React apps

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "x402-stacks" with this command: npx skills add x402stacks/x402stacks-skill/x402stacks-x402stacks-skill-x402-stacks

x402-stacks SDK

Overview

x402-stacks enables automatic HTTP-level payments for APIs using STX or sBTC tokens on Stacks blockchain. Two client paths: server/CLI (axios interceptor with private key) and frontend/browser (wallet extension signing via @stacks/connect). Server protects endpoints with Express middleware.

Core principle: HTTP 402 Payment Required becomes a working protocol.

Installation

# Server/CLI
npm i x402-stacks

# Frontend (Browser) — additional deps
npm i x402-stacks @stacks/connect @stacks/transactions axios

Wallet Setup

Choose your path based on environment:

  • Server/CLI → Option 1 or 2 (private key in env)
  • Frontend (Browser) → Option 3 (wallet extension, no private keys)

Option 1: Load Existing Wallet (Server/CLI)

When: You have a private key already

import { privateKeyToAccount } from 'x402-stacks';

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY!,
  'testnet'  // or 'mainnet'
);

Option 2: Generate New Wallet (Server/CLI)

When: You don't have a wallet yet

import { generateKeypair, privateKeyToAccount } from 'x402-stacks';

// Generate once, save the private key
const keypair = generateKeypair('testnet');
console.log('Save this:', keypair.privateKey);
console.log('Fund at:', `https://explorer.stacks.co/sandbox/faucet?chain=testnet`);

// Use it
const account = privateKeyToAccount(keypair.privateKey, 'testnet');

Important: After generating, save private key to .env file and fund via faucet before making payments.

Option 3: Connect Browser Wallet (Frontend)

When: Building a browser dApp — keys never leave the wallet extension

// Always dynamic import (SSR/bundler safe)
const { connect, disconnect, isConnected } = await import('@stacks/connect');

// Connect wallet (opens popup)
await connect();

// Check connection
if (isConnected()) {
  // Read stored wallet data
  const walletData = getLocalStorage();
  // walletData contains addresses, public keys, etc.
}

// Disconnect
await disconnect();

Never store or request private keys in the browser. The wallet extension manages signing.

Quick Start: Server/CLI Client (Pays Automatically)

import axios from 'axios';
import { wrapAxiosWithPayment, privateKeyToAccount } from 'x402-stacks';

const account = privateKeyToAccount(process.env.PRIVATE_KEY!, 'testnet');

const api = wrapAxiosWithPayment(
  axios.create({ baseURL: 'http://localhost:3000', timeout: 60000 }),
  account
);

// Payment happens automatically on 402 response
const response = await api.get('/api/premium-data');

Quick Start: Frontend Client (Browser)

Three steps: parse the 402, sign via wallet, retry with payment header.

Step 1: Parse 402 Response

import type { AxiosError } from 'axios';
import { X402_HEADERS, type PaymentRequiredV2 } from 'x402-stacks';

function decodePaymentRequired(header: string): PaymentRequiredV2 | null {
  try {
    return JSON.parse(atob(header));    // btoa/atob — browser-safe, no Buffer
  } catch {
    return null;
  }
}

function parse402Response(error: AxiosError): PaymentRequiredV2 {
  // Try header first (base64 encoded)
  const headerValue = error.response?.headers?.[X402_HEADERS.PAYMENT_REQUIRED];
  const fromHeader = decodePaymentRequired(headerValue);
  if (fromHeader && fromHeader.accepts?.length > 0) return fromHeader;

  // Fall back to response body
  const data = error.response?.data as Record<string, unknown> | undefined;
  if (data && Array.isArray(data.accepts) && data.accepts.length > 0) {
    return data as unknown as PaymentRequiredV2;
  }

  throw new Error('No valid payment requirements in 402 response');
}

Step 2: Sign Payment via Wallet (No Broadcast)

import type {
  PaymentRequiredV2, PaymentRequirementsV2, PaymentPayloadV2,
} from 'x402-stacks';

async function signX402Payment(
  paymentRequired: PaymentRequiredV2,
  accepted: PaymentRequirementsV2,
  network: 'mainnet' | 'testnet',
): Promise<string> {
  const { request } = await import('@stacks/connect');
  const { makeUnsignedSTXTokenTransfer } = await import('@stacks/transactions');

  // 1. Get public key from wallet
  const addrResponse = await request('stx_getAddresses') as {
    addresses?: Array<{ symbol?: string; publicKey?: string }>;
  };
  const stxEntry = addrResponse.addresses?.find((a) => a.symbol === 'STX');
  if (!stxEntry?.publicKey) throw new Error('Could not get public key from wallet');

  // 2. Build unsigned transaction
  const unsignedTx = await makeUnsignedSTXTokenTransfer({
    publicKey: stxEntry.publicKey,
    recipient: accepted.payTo,
    amount: BigInt(accepted.amount),
    network,
  });
  const txHex = unsignedTx.serialize();

  // 3. Sign WITHOUT broadcasting — facilitator will broadcast
  const signResult = await request('stx_signTransaction', {
    transaction: txHex,
    broadcast: false,
  }) as { transaction: string };
  if (!signResult.transaction) throw new Error('No signed transaction from wallet');

  // 4. Build payload and base64 encode (browser-safe)
  const payload: PaymentPayloadV2 = {
    x402Version: 2,
    resource: paymentRequired.resource,
    accepted,
    payload: { transaction: signResult.transaction },
  };
  return btoa(JSON.stringify(payload));
}

Step 3: Two-Request Flow

import axios from 'axios';
import { X402_HEADERS } from 'x402-stacks';

async function fetchWithPayment(url: string, network: 'mainnet' | 'testnet') {
  try {
    return await axios.get(url);
  } catch (error) {
    if (!axios.isAxiosError(error) || error.response?.status !== 402) throw error;

    const paymentRequired = parse402Response(error);
    const accepted = paymentRequired.accepts[0];  // first accepted scheme

    const paymentHeader = await signX402Payment(paymentRequired, accepted, network);

    return axios.get(url, {
      headers: { [X402_HEADERS.PAYMENT_SIGNATURE]: paymentHeader },
      timeout: 60000,
    });
  }
}

React Integration

For React apps, wrap the frontend primitives above in a Context + typed request helper. Four files to create:

FilePurposeReusable?
StacksWalletContext.tsxWallet state, connect/disconnectCopy as-is
x402.tsparse402Response + signX402PaymentCopy as-is (code in Steps 1-2 above)
x402Request.tsGeneric typed two-step requestSwap URL/body for your API
WalletButton.tsxConnect/disconnect UIAdapt styling to your app

Wallet Context (StacksWalletContext.tsx)

import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';

export type Network = 'mainnet' | 'testnet';

interface StacksWalletContextType {
  isLoading: boolean;
  isWalletConnected: boolean;
  network: Network;
  stxAddress: string | undefined;
  connectWallet: () => Promise<void>;
  disconnectWallet: () => Promise<void>;
}

const StacksWalletContext = createContext<StacksWalletContextType | undefined>(undefined);

function extractStxAddress(data: unknown): string | undefined {
  if (!data || typeof data !== 'object') return undefined;
  const obj = data as Record<string, unknown>;
  const addresses = obj.addresses;
  if (!addresses || typeof addresses !== 'object') return undefined;
  const stx = (addresses as Record<string, unknown>).stx;
  if (!Array.isArray(stx) || stx.length === 0) return undefined;
  return stx[0]?.address as string | undefined;
}

export function StacksWalletProvider({ children }: { children: React.ReactNode }) {
  const [walletData, setWalletData] = useState<unknown>(undefined);
  const [isLoading, setIsLoading] = useState(true);
  const [network] = useState<Network>('mainnet');

  useEffect(() => {
    const check = async () => {
      try {
        const { isConnected, getLocalStorage } = await import('@stacks/connect');
        if (isConnected()) setWalletData(getLocalStorage());
      } catch { /* wallet not installed */ }
      finally { setIsLoading(false); }
    };
    check();
  }, []);

  const connectWallet = useCallback(async () => {
    const { connect, getLocalStorage } = await import('@stacks/connect');
    await connect();
    setWalletData(getLocalStorage());
  }, []);

  const disconnectWallet = useCallback(async () => {
    const { disconnect } = await import('@stacks/connect');
    disconnect();
    setWalletData(undefined);
  }, []);

  return (
    <StacksWalletContext.Provider value={{
      isLoading, isWalletConnected: !!walletData, network,
      stxAddress: extractStxAddress(walletData), connectWallet, disconnectWallet,
    }}>
      {children}
    </StacksWalletContext.Provider>
  );
}

export function useStacksWallet() {
  const ctx = useContext(StacksWalletContext);
  if (!ctx) throw new Error('useStacksWallet must be used within StacksWalletProvider');
  return ctx;
}

Generic x402 Request (x402Request.ts)

Typed wrapper over the two-step pattern. Uses parse402Response and signX402Payment from x402.ts (Steps 1-2 above).

import axios, { AxiosError } from 'axios';
import { parse402Response, signX402Payment, X402_HEADERS } from './x402';
import type { Network } from './StacksWalletContext';

export async function x402Request<T>(
  method: 'get' | 'post',
  url: string,
  body: unknown,
  network: Network,
): Promise<T> {
  let encodedPayload: string;

  try {
    const { data } = await axios({ method, url, data: body });
    return data as T;  // didn't 402 — return directly
  } catch (err) {
    if (err instanceof AxiosError && err.response?.status === 402) {
      const paymentReq = parse402Response(err);
      const accepted = paymentReq.accepts[0];
      if (!accepted) throw new Error('No accepted payment methods');
      encodedPayload = await signX402Payment(paymentReq, accepted, network);
    } else {
      throw err;
    }
  }

  // Retry with payment proof
  const { data } = await axios({
    method, url, data: body,
    headers: { [X402_HEADERS.PAYMENT_SIGNATURE]: encodedPayload },
  });
  return data as T;
}

Usage with any endpoint:

const job = await x402Request('post', '/api/v1/exports', { id: '123' }, 'mainnet');
const article = await x402Request('get', '/api/v1/premium/456', null, 'mainnet');

Provider Setup

import { StacksWalletProvider } from './StacksWalletContext';

function App() {
  return (
    <StacksWalletProvider>
      {/* routes, components, etc. */}
    </StacksWalletProvider>
  );
}

Component Usage

import { useStacksWallet } from './StacksWalletContext';
import { x402Request } from './x402Request';

function BuyButton({ resourceId }: { resourceId: string }) {
  const { isWalletConnected, connectWallet, network } = useStacksWallet();
  const [loading, setLoading] = useState(false);

  const handlePurchase = async () => {
    if (!isWalletConnected) await connectWallet();
    setLoading(true);
    try {
      const result = await x402Request('post', '/api/v1/buy', { id: resourceId }, network);
      console.log('Paid!', result);
    } catch (err) {
      console.error('Payment failed:', err);
    } finally {
      setLoading(false);
    }
  };

  return <button onClick={handlePurchase} disabled={loading}>
    {loading ? 'Processing...' : 'Buy'}
  </button>;
}

Quick Start: Server (Requires Payment)

import express from 'express';
import { paymentMiddleware, STXtoMicroSTX, STACKS_NETWORKS } from 'x402-stacks';

const app = express();

app.get('/api/premium-data',
  paymentMiddleware({
    amount: STXtoMicroSTX(0.00001),
    payTo: process.env.SERVER_ADDRESS!,
    network: STACKS_NETWORKS.TESTNET,  // or STACKS_NETWORKS.MAINNET
    asset: 'STX',
    facilitatorUrl: 'https://facilitator.stacksx402.com',  // Free facilitator
  }),
  (req, res) => {
    res.json({ data: 'Premium content' });
  }
);

Registering on x402scan

Make your service discoverable by registering it on x402scan.

Registration Endpoint

POST https://scan.stacksx402.com/api/v1/resources
Content-Type: application/json

{
  "url": "https://your-service.com/api/your-endpoint"
}

Your Endpoint Must Return

When x402scan validates your URL, it expects HTTP 402 (or 200) with:

{
  "x402Version": 1,
  "name": "My AI Service",
  "image": "https://your-service.com/logo.png",
  "accepts": [{
    "scheme": "exact",
    "network": "stacks",
    "asset": "STX",
    "maxAmountRequired": "1000000",
    "resource": "https://your-service.com/api/your-endpoint",
    "description": "What this service does",
    "mimeType": "application/json",
    "payTo": "SP2...YOUR_ADDRESS",
    "maxTimeoutSeconds": 60,
    "outputSchema": {
      "input": {
        "type": "https",
        "method": "GET",
        "queryParams": {
          "q": { "type": "string", "required": true, "description": "Query param" }
        }
      },
      "output": {
        "type": "object",
        "properties": { "result": { "type": "string" } }
      }
    }
  }]
}

Validation Requirements

Must HaveError if Missing
HTTPS URLinvalid_url
Non-empty nameinvalid_name
At least 1 accepts entryempty_accepts
network: "stacks" in all acceptsinvalid_network
outputSchema in all acceptsmissing_output_schema

Why outputSchema Matters

The outputSchema tells agents HOW to call your service:

  • input.method - HTTP method (GET/POST)
  • input.queryParams - Query parameters (for GET)
  • input.bodyFields - Body structure (for POST)
  • output - Response format

This is the contract agents use to call your service programmatically.

Quick Registration Example

curl -X POST https://scan.stacksx402.com/api/v1/resources \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-service.com/api/endpoint"}'

After registration: Your service appears in the x402scan directory and gets re-validated every 24h.

Quick Reference

FunctionPathPurposeReturns
privateKeyToAccount(key, network)ServerLoad wallet from key{ address, privateKey, network }
generateKeypair(network)ServerCreate new wallet{ privateKey, publicKey, address }
wrapAxiosWithPayment(axios, account)ServerAuto-pay on 402axios instance
paymentMiddleware(config)ServerProtect endpointExpress middleware
STXtoMicroSTX(amount)BothConvert STX to microSTXbigint
decodePaymentResponse(header)BothGet payment details{ transaction, payer, network }
X402_HEADERSBothHeader name constants{ PAYMENT_REQUIRED, PAYMENT_SIGNATURE, ... }
connect()FrontendOpen wallet popupvoid
request('stx_getAddresses')FrontendGet wallet public key{ addresses }
request('stx_signTransaction', opts)FrontendSign tx (no broadcast){ transaction }
makeUnsignedSTXTokenTransfer(opts)FrontendBuild unsigned STX txTransaction object
useStacksWallet()ReactWallet state hook{ isWalletConnected, stxAddress, connectWallet, ... }
x402Request<T>(method, url, body, network)ReactTyped two-step payment requestPromise<T>

Payment Flow

Server/CLI (automatic)

1. Client → GET /api/data → Server
2. Server → 402 + payment-required header → Client
3. Client signs tx (not broadcast)
4. Client → GET + payment-signature → Server
5. Server → Facilitator settles tx → Blockchain
6. Server → 200 + data + payment-response → Client

Frontend (Browser)

1. Browser → GET /api/data → Server
2. Server → 402 + payment-required header → Browser
3. Browser parses 402, opens wallet popup
4. User approves → wallet signs tx (no broadcast)
5. Browser → GET + payment-signature header → Server
6. Server → Facilitator settles tx → Blockchain
7. Server → 200 + data → Browser

Common Mistakes

MistakeFix
Generating wallet every runSave private key to .env and load it
Using plain "mainnet" instead of CAIP-2Use STACKS_NETWORKS.MAINNET or "stacks:1"
Not funding testnet walletGet tokens from faucet first
Forgetting timeout for settlementSet timeout: 60000 (60 seconds minimum)
Mixing STX and microSTX amountsUse STXtoMicroSTX() converter
Hardcoding private keysAlways use environment variables
Using Buffer for base64 in browserUse btoa() / atob() — Buffer is Node-only
Broadcasting tx from clientSet broadcast: false — facilitator broadcasts
Static imports of @stacks/connectAlways use dynamic await import(...) (SSR/bundler safe)
Not validating accepts arrayCheck accepts?.length > 0 before accessing [0]
Storing private keys in browserUse wallet extension — keys never leave it
Using useStacksWallet outside providerWrap app root with <StacksWalletProvider>
Not checking isWalletConnected before paymentCall connectWallet() first if not connected

Token Support

  • STX (default): Native Stacks token
  • sBTC: Bitcoin on Stacks (add tokenType: 'sBTC' and tokenContract)

Environment Variables

# Server/CLI Client
PRIVATE_KEY=your-private-key-hex
NETWORK=testnet

# Server
SERVER_ADDRESS=ST1...
FACILITATOR_URL=https://facilitator.stacksx402.com

Frontend: No env vars needed. Network is set in code; wallet extension manages keys.

When NOT to Use

  • Traditional payment flows (use Stripe/PayPal)
  • Subscription models (x402 is for pay-per-use)
  • High-frequency micro-transactions (<$0.001 - fees make it impractical)
  • When users don't have crypto wallets

Resources

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Web3

china-sportswear-outdoor-sourcing

Comprehensive sportswear and outdoor equipment sourcing guide for international buyers – provides detailed information about China's athletic apparel, footwear, outdoor gear, and accessories manufacturing clusters, supply chain structure, regional specializations, and industry trends (2026 updated).

Archived SourceRecently Updated
Web3

china-lighting-sourcing

Comprehensive lighting industry sourcing guide for international buyers – provides detailed information about China's LED, smart, outdoor, automotive, and specialty lighting manufacturing clusters, supply chain structure, regional specializations, and industry trends (2026 updated).

Archived SourceRecently Updated
Web3

china-furniture-sourcing

Comprehensive furniture industry sourcing guide for international buyers – provides detailed information about China's residential, office, hotel, outdoor, and custom furniture manufacturing clusters, supply chain structure, regional specializations, and industry trends (2026 updated).

Archived SourceRecently Updated
Web3

china-home-appliances-sourcing

Comprehensive home appliances industry sourcing guide for international buyers – provides detailed information about China's major appliances, kitchen appliances, and small appliances manufacturing clusters, supply chain structure, regional specializations, and industry trends (2026 updated).

Archived SourceRecently Updated