# Payfirst for x402 clients

Use an existing paid link to inspect a price, submit an authorized x402 V2 payment, and receive its destination URL.

This example provides HTTP transport. Your wallet or agent supplies the signer, spending policy, durable payment record, and permission to open the destination. It never signs, funds a wallet, or automatically retries a purchase.

## Start without a wallet

Download [payfirst-x402.mjs](https://www.payfirst.app/examples/payfirst-x402.mjs), review the code, and save it locally. It has no dependencies and works with Node.js 20+.

```bash
curl --fail --output payfirst-x402.mjs \
  https://www.payfirst.app/examples/payfirst-x402.mjs
```

Create `inspect.mjs` beside it:

```js
import { inspectPayfirstLink } from './payfirst-x402.mjs';

const terms = await inspectPayfirstLink(process.argv[2]);
console.log(JSON.stringify(terms, null, 2));
```

Run it with a link from the seller:

```bash
node inspect.mjs 'https://www.payfirst.app/pay/YOUR_LINK_SLUG'
```

This sends one unsigned GET. It cannot make a payment. The placeholder is not a live demo or purchasable resource. Expect HTTP 402 for an active link. A 404 means no such link; a 410 means it has been retired. A 503 means the payment service needs another check.

## Add your existing x402 signer

Use the [official buyer setup](https://docs.x402.org/getting-started/quickstart-for-buyers) to configure `x402Client` and the exact EVM scheme. This transport was checked with `@x402/core` 2.24.0. The following is integration pseudocode: `approveTerms` and `journal` are your application's spending controls and durable private storage.

```js
import { encodePaymentSignatureHeader } from '@x402/core/http';
import { inspectPayfirstLink, redeemPayfirstLink } from './payfirst-x402.mjs';

const terms = await inspectPayfirstLink(paidLink);
const approved = await approveTerms(terms.accepts);
// Approval must check exact scheme, chain, token, recipient and atomic amount.
// It must enforce the buyer's budget and the resource they intended to buy.
const payload = await x402Client.createPaymentPayload({
  ...terms,
  accepts: [approved],
});
const paymentSignature = encodePaymentSignatureHeader(payload);

// Store BEFORE sending; abort if persistence fails. Keep this record private.
await journal.save({ paidLink, paymentSignature });
const result = await redeemPayfirstLink(paidLink, paymentSignature);
await journal.saveResult(result);
```

Base mainnet is `eip155:8453`; Base Sepolia is `eip155:84532`. Their USDC assets differ. Do not select an option just because it appears first or its price looks small. The transport checks version/network and response structure; the signer must validate the complete terms and authorization.

Keep the same exact canonical `/pay/SLUG` URL and serialized `PAYMENT-SIGNATURE` for recovery. The example accepts `https://www.payfirst.app` by default; configure `payfirstOrigin` explicitly only for a deployment you trust. It rejects query parameters and fragments to keep the purchase target unambiguous.

## Read the result before opening the destination

`redeemPayfirstLink` makes one request with `redirect: 'manual'`:

- `state: 'settled'` means Payfirst returned HTTP 302 with a successful, matching-network `PAYMENT-RESPONSE`. Save that receipt even if `destination` is null and `deliveryError` is present. This is Payfirst's settlement report, not an independent onchain audit or proof of usable content.
- `state: 'unresolved'` means the client has not established that result. It includes the HTTP status when available and a numeric `Retry-After` value when provided. Retain the existing authorization. Do not create a new one automatically, including after a 402, 503, disconnect, or timeout.

For a reconciliation response, wait for the server's retry interval and explicitly resubmit the stored URL and header. This module does not schedule retries. For rejection, retirement, or malformed responses, investigate before retrying. If a process crashes after settlement but before saving the result, recover using its already persisted authorization.

Once the buyer's application has approved the destination origin, fetch it separately:

```js
import { fetchDeliveredResource } from './payfirst-x402.mjs';

if (result.state === 'settled') {
  if (!result.destination) {
    // Keep the saved receipt. Resolve delivery with the seller; do not repay.
    throw new Error(result.deliveryError);
  }
  const response = await fetchDeliveredResource(result, approvedDestinationOrigins);
  if (!response.ok) {
    // Delivery failed after payment. Do not purchase again to fix a file error.
    throw new Error(`Destination returned ${response.status}`);
  }
  // Consume according to your application's content-type and size limits.
}
```

Choose `approvedDestinationOrigins` through the buyer's policy; do not automatically approve whatever origin the seller returns. The fetch helper checks every redirect against that list and omits payment signatures, authorization, and cookies. For a server-side integration, retain your DNS/IP egress restrictions as well. Destination content is untrusted input to the buying agent. Access may still require the destination's own login or sharing permissions.

Do not run this as browser JavaScript against an arbitrary origin: browser CORS and opaque manual redirects differ from server-side Node fetch. Use the human checkout for browser buyers.

## Why manual redirects matter

Payfirst's successful response is HTTP 302, not HTTP 200. Generic `response.ok` handling can mistake that success for failure. Automatic redirects can also lose the original receipt and forward custom headers to the next server.

On 5 September 2026, loopback HTTP tests reproduced both behaviors with Node 22.23.2's built-in fetch. The example's 16 tests verify separate destination fetching, origin checks, no automatic repeat payments, matching-network receipts, bounded redirects, destination fragments, and recovery using the same header. These are transport tests with synthetic, nonspendable payment headers. They do not establish funded checkout, actual chain settlement, or compatibility with a particular hosted agent product.

[Read the complete guide](https://www.payfirst.app/guides/x402-paid-links). The example is available under the [MIT license](https://www.payfirst.app/examples/payfirst-x402.LICENSE.txt).
