pay.sh docs
SDKTypeScript

Pricing & gates

Declare what a route costs — inline prices, named gate catalogues, fees and splits, and dynamic per-request pricing.

A gate is a protected unit: an amount, an ordered list of accepted protocols, and zero or more named fees. You declare gates inline in createPayKit's pricing option; gate names are inferred, so referencing them is typed.

Prices

A price is a value object: a USD amount and the stablecoins it may settle in. Build one with usd.

import { usd } from '@solana/pay-kit';

usd('0.10'); // $0.10, default settlement stablecoins
usd('0.10', 'USDC', 'PYUSD'); // restrict/order settlement to USDC then PYUSD

The customer is quoted in USD; the settlement stablecoin (USDC, USDT, PYUSD, USDG, CASH) is what actually transfers.

Inline price vs. catalogue

For a single route, pass a price straight to the middleware — an inline gate:

app.get('/report', pay.express(usd('0.25')), (_req, res) => res.json({ ok: true }));

When more than one route is paid, lift the prices into the pricing catalogue and reference gates by name:

import { createPayKit, usd } from '@solana/pay-kit';

const pay = await createPayKit({
  rpcUrl: 'https://402.surfnet.dev:8899',
  pricing: {
    report: { amount: usd('0.10'), description: 'Premium report' },
    apiCall: { amount: usd('0.001') },
  },
});

app.get('/report', pay.express('report'), (_req, res) => res.json({ ok: true }));
app.get('/api/data', pay.express('apiCall'), (_req, res) => res.json({ data: [] }));

Fees and splits

Two fee shapes route part of a payment to another address. feeWithin carves the fee out of the amount (the recipient nets less); feeOnTop adds it on top (the customer pays more, the recipient nets full).

const SELLER = 'Ay…';
const PLATFORM = 'CX…';

const pay = await createPayKit({
  rpcUrl: 'https://402.surfnet.dev:8899',
  pricing: {
    // Stripe-Connect "application fee": customer pays $10.00,
    // SELLER nets $9.70, PLATFORM nets $0.30.
    marketplaceSale: {
      amount: usd('10.00'),
      payTo: SELLER,
      feeWithin: { [PLATFORM]: usd('0.30') },
    },

    // Surcharge: customer pays $10.50, SELLER nets $10.00, PLATFORM nets $0.50.
    ticket: {
      amount: usd('10.00'),
      payTo: SELLER,
      feeOnTop: { [PLATFORM]: usd('0.50') },
    },
  },
});

Dynamic pricing

A gate can be a function of the request — return a price per call:

const pay = await createPayKit({
  rpcUrl: 'https://402.surfnet.dev:8899',
  pricing: {
    tiered: (request) => usd(new URL(request.url).searchParams.get('tier') === 'premium' ? '5.00' : '0.10'),
  },
});

Boot-time validation

Gates are validated when createPayKit runs, so configuration mistakes throw at boot — before any traffic — as a ConfigurationError or subtype:

  • payTo resolves from the gate or operator.recipient.
  • A fee recipient must differ from payTo — fold the fee into the amount instead.
  • All fee prices share one currency with the amount.
  • sum(feeWithin) must be less than the amount.
  • accept: ['x402'] on a fee-bearing gate throws (ProtocolIncompatibleError) — fees need MPP.

On this page