> ## Documentation Index
> Fetch the complete documentation index at: https://ifdocdemo.lumivescence.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Protect an endpoint

> Accept MPP and x402 with InFlow Payments. Set prices, currencies, rails, and subscriptions.

Accept MPP and x402 on your endpoints with the Node.js seller SDKs. This is Step 1 of the InFlow integration. A protected endpoint returns `402 Payment Required` until the agent pays, then returns the resource.

For the shortest path, read the [Payments quickstart](/sellers/get-started) first. For Python, Go, and Rust, see [Protect an endpoint in Python, Go, or Rust](/sellers/protect-an-endpoint-other-languages).

## Before you start

| Item | Detail |
| - | - |
| Node | Version 22 LTS or later. The packages declare `engines.node: >=22.0.0`. |
| Account | A Seller account and an API key. See [Create a Seller account](/sellers/create-a-seller-account). |
| Environment | `sandbox` or `production`. The value must match the dashboard that issued the key. |

## Accept MPP and x402

Integrate both protocols in the same service, so that agents of either protocol can pay. Share the pricing and fulfillment logic between them. A request must never pass through two independent charging paths. [Payments with MPP and x402](/concepts/payments) compares the protocols.

The InFlow balance is the default way for agents to pay. It is the `balance` rail in MPP and the `balance` scheme in x402. Agents can also pay onchain or with a linked card.

## Install the packages

| Protocol | Framework | Install |
| - | - | - |
| x402 | Express | `pnpm add @inflowpayai/x402-seller @x402/express @x402/core @x402/extensions express` |
| x402 | Fastify | `pnpm add @inflowpayai/x402-seller @x402/fastify @x402/core @x402/extensions fastify` |
| x402 | Next 16 App Router | `pnpm add @inflowpayai/x402-seller @x402/next @x402/core @x402/extensions next` |
| MPP | Any | `pnpm add @inflowpayai/mpp-seller mppx@^0.8.17` |

For MPP, `mppx` is a peer dependency. Each framework has a runnable example in [SDK packages](/reference/sdk-packages#examples).

## Protect an endpoint with x402

This server from the `x402-seller-express` example protects two endpoints and leaves one endpoint free.

```ts server.ts theme={null}
import 'dotenv/config';
import { paymentMiddlewareFromConfig } from '@x402/express';
import express from 'express';
import {
  createInflowFacilitator,
  createInflowSellerClient,
  inflowAccepts,
  inflowSchemeRegistrations,
} from '@inflowpayai/x402-seller';

const environment = process.env['INFLOW_ENVIRONMENT'] as 'sandbox' | 'production';
const apiKey = process.env['INFLOW_API_KEY'];
if (!environment || !apiKey) {
  console.error('Set INFLOW_ENVIRONMENT and INFLOW_API_KEY in your environment.');
  process.exit(1);
}

const inflow = createInflowFacilitator({ environment, apiKey });
const seller = await createInflowSellerClient({ environment, apiKey });

const app = express();
app.use(express.json());

app.use(
  paymentMiddlewareFromConfig(
    {
      'GET /api/widgets': {
        accepts: await inflowAccepts(seller, {
          price: '$0.01',
          schemes: ['exact'],
        }),
      },
      'POST /api/upload': {
        accepts: await inflowAccepts(seller, {
          price: '0.10 USDC',
          schemes: ['balance', 'exact'],
        }),
      },
    },
    [inflow],
    await inflowSchemeRegistrations(seller),
  ),
);

app.get('/api/widgets', (_req, res) => {
  res.json({ widgets: [1, 2, 3] });
});

app.post('/api/upload', (_req, res) => {
  res.json({ status: 'received' });
});

app.get('/free', (_req, res) => {
  res.json({ ok: true, note: 'no payment required' });
});

app.listen(Number(process.env['PORT'] ?? 3000));
```

### What each part does

| Function | What it does |
| - | - |
| `createInflowFacilitator` | Makes the facilitator client. It checks and settles payments through InFlow. It is synchronous. |
| `createInflowSellerClient` | Makes the seller client. It reads your seller configuration and finds your signers. It is asynchronous and caches for 60 minutes. |
| `inflowAccepts` | Turns a price into payment options. It resolves the atomic amount and the asset address for each option. |
| `inflowSchemeRegistrations` | Registers a scheme server for each scheme and network pair that you can advertise. |

Pass `inflowSchemeRegistrations` as the third argument. Without it, the middleware stops at boot with `RouteConfigurationError`.

### Write a price

`inflowAccepts` reads three price formats. Each format accepts up to 8 decimal places.

| Format | Example | Currency |
| - | - | - |
| Dollar amount | `'$0.01'` | USD |
| Amount with a currency suffix | `'0.10 USDC'` | The suffix |
| Bare number | `'0.01'` | The `currency` field of the price |

For `balance` and `exact` offers, `USD` matches any stablecoin in your seller configuration. For an `instrument` offer, `USD` is fiat USD.

Stablecoin payments have no minimum amount. An `instrument` price must be in USD, at least USD 0.50, and in whole cents.

### Limit the schemes

Pass `schemes` to limit the payment options on an endpoint, as `POST /api/upload` does above.

| Scheme | What it does | In the default |
| - | - | - |
| `balance` | Moves the InFlow balance of the agent. | Yes |
| `exact` | Settles onchain. | Yes |
| `upto` | Settles a metered amount onchain, up to a maximum. | No. Add it to `schemes`. |
| `instrument` | Charges a linked card of the buyer in USD. | No. Add it to `schemes`. |

### Use more than one facilitator

The middleware accepts a list of facilitators. The first facilitator that claims a scheme and network pair gets the payments for that pair.

```ts theme={null}
paymentMiddlewareFromConfig({/* routes */}, [
  inflow, // claims (balance, inflow), (exact, eip155:8453), …
  otherFacilitator, // claims what inflow does not claim
  partnerFacilitator,
]);
```

### Caching

The foundation middleware sets the cache headers. An unpaid challenge gets `no-store`. A settled response gets `private`.

## Protect an endpoint with MPP

MPP uses [`mppx`](https://github.com/wevm/mppx). Add the InFlow method to `Mppx.create`. Then `charge()` returns a `402` challenge for an unpaid request.

```ts theme={null}
import { Mppx, inflow, paymentHttpTransport } from '@inflowpayai/mpp-seller';

const mppx = Mppx.create({
  transport: paymentHttpTransport(),
  methods: [
    inflow({
      apiKey: process.env.INFLOW_API_KEY!,
      environment: 'sandbox',
    }),
  ],
  secretKey: process.env.MPP_SECRET_KEY,
});

export async function handler(req: Request) {
  const r = await mppx.charge({ amount: '0.01', currency: 'USDC' })(req);
  if (r.status === 402) return r.challenge;
  return r.withReceipt(Response.json({ data: '…' }));
}
```

This handler uses the Web Fetch API. For Express, Hono, Next.js, or Elysia, use `mppx/express`, `mppx/hono`, `mppx/nextjs`, or `mppx/elysia`. The [Payments quickstart](/sellers/get-started) shows the Express adapter.

Pass `paymentHttpTransport()` as the `transport`. Without it, a server error also sends a payment challenge.

### Set the secret key

`Mppx.create` needs a `secretKey`. It also reads the `MPP_SECRET_KEY` variable. Generate the secret.

```bash theme={null}
openssl rand -base64 32
```

The secret must contain at least 32 bytes. Keep it stable across deploys, because the server uses it to check the challenges that it issued.

### Choose the currency

The charge currency sets the rail. The SDK reads the rail for each currency from `GET /v1/mpp/config`, one time at startup.

| Charge currency | Rail | Result |
| - | - | - |
| Crypto, for example `USDC` | `balance` | One challenge. No extra parameters. |
| Fiat, for example `USD` | `instrument` | One challenge. The buyer selects a linked card or uses the primary card. Do not put a buyer instrument ID in the challenge. |
| Not supported, for example `JPY` | none | `MppUnsupportedCurrencyError` |

Stablecoin payments have no minimum amount. Card payments in USD have a minimum of USD 0.50 and must be in whole cents. This applies to the `instrument` rail, `card`, and `stripe`.

### Accept more than one currency

`charge()` advertises one currency. For several currencies, use `inflowChargesNodeListener` or `inflowCharges`. The endpoint sends one challenge for each price.

```ts theme={null}
import { Mppx, inflow, inflowChargesNodeListener, paymentHttpTransport } from '@inflowpayai/mpp-seller';

// Core instance (mppx/server) — keeps compose(). A single instance can also serve single-currency routes.
const mppx = Mppx.create({
  transport: paymentHttpTransport(),
  methods: [inflow({ apiKey: process.env.INFLOW_API_KEY!, environment: 'sandbox' })],
  secretKey: process.env.MPP_SECRET_KEY,
});

// One challenge per price. USD → instrument rail, USDC → balance rail (the method derives each rail from the currency).
const checkout = inflowChargesNodeListener(mppx, [
  { amount: '1.0', currency: 'USD' },
  { amount: '0.0095', currency: 'USDC' },
]);
```

Mount `checkout(req, res)` on a Node `http` server or on an Express endpoint. For a Web Fetch handler, use `inflowCharges(mppx, prices)`.

The framework adapters expose `charge` only and remove `compose`. Thus a multi-currency endpoint needs the core `Mppx` instance from `@inflowpayai/mpp-seller`.

The buyer pays one challenge. Each amount is independent. It is not an exchange rate.

### Control which offers appear

Two hooks select the challenges for a request. `canOffer` works on one method. `selectOffers` works on the server and runs after every `canOffer`.

```ts theme={null}
const mppx = Mppx.create({
  transport: paymentHttpTransport(),
  methods: [
    inflow({
      apiKey: process.env.INFLOW_API_KEY!,
      environment: 'sandbox',
      canOffer: ({ input }) => input.headers.get('x-market') !== 'blocked',
    }),
  ],
  selectOffers: (offers, { request }) =>
    request.headers.get('x-usdc-only') === 'true'
      ? offers.filter((offer) => offer.request.currency === 'USDC')
      : offers,
  secretKey: process.env.MPP_SECRET_KEY,
});
```

<Warning>
  These hooks are not authorization checks. A buyer can still redeem a credential after its offer stops. Enforce access policy separately.
</Warning>

When `canOffer` rejects every offer, the handler fails with `No payment offers are available for this request`. `selectOffers` must return at least one offer.

### Offer a subscription

A subscription protects a resource with recurring payment terms. The buyer approves the terms, and InFlow charges the first period. InFlow then manages later charges, past-due states, and cancellation. Subscriptions use the InFlow `balance` rail. The buyer and the seller do not need a blockchain wallet.

| Field | Meaning |
| - | - |
| `amount` | The positive decimal amount for each period. |
| `currency` | The currency code. It must be a currency that the balance rail supports. |
| `periodUnit` | `hour`, `day`, `week`, `month`, `quarter`, or `year`. |
| `periodCount` | The positive number of period units between charges. |
| `subscriptionExpires` | An RFC 3339 timestamp. After this time, the subscription cannot renew. |
| `externalId` | An optional reference for reconciliation. It is not an authorization key or a lookup key. |

```ts theme={null}
import express from 'express';
import { Mppx } from 'mppx/server';
import { inflow, inflowSubscriptionsNodeListener, paymentHttpTransport } from '@inflowpayai/mpp-seller';

const subscriptions = Mppx.create({
  transport: paymentHttpTransport(),
  methods: [
    inflow.subscription({
      apiKey: process.env.INFLOW_API_KEY!,
      environment: 'sandbox',
    }),
  ],
  secretKey: process.env.MPP_SECRET_KEY,
});

const subscribe = inflowSubscriptionsNodeListener(subscriptions, [
  {
    amount: '9.99',
    currency: 'USDC',
    periodUnit: 'month',
    periodCount: 1,
    subscriptionExpires: '2027-12-31T23:59:59Z',
    externalId: 'pro-monthly',
  },
]);

const app = express();
app.get('/api/subscribe', async (request, response) => {
  const result = await subscribe(request, response);
  if (result.status === 402) return;
  response.json({ access: 'granted' });
});
```

For a Web Fetch handler, use `inflowSubscriptions(mppx, plans)`.

The SDK also accepts `minute` for tests, with a minimum `periodCount` of `5`. Do not offer minute plans to customers.

### Charge in TIP-20 tokens on Tempo

`@inflowpayai/mpp-seller` also exports a `tempo` method for Tempo TIP-20 charges. Pass it to `Mppx.create` as `tempo({ apiKey, currency, recipient })`. Fee-payer sponsorship is off by default. Set `methodDetails.feePayer: true` to issue a sponsored challenge.

The package also exports `card` for USD payments with Visa network tokens, and `stripe` for USD payments with Stripe Shared Payment Tokens. The `mpp-card-seller-express` example uses `card`.

## Check trusted-agent signatures

`@inflowpayai/tap-seller` checks trusted-agent request signatures on your own server. See [SDK packages](/reference/sdk-packages).

## Next step

<Card title="Test a payment" icon="flask" href="/sellers/test-a-payment">
  Pay your own endpoint in the sandbox.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.