Quorlet
Official CA

Docs

How a Quorlet paywall behaves over HTTP.

The 402 response

A request to /api/x402/[id] without payment proof returns HTTP 402. The JSON body is also sent base64-encoded in the PAYMENT-REQUIRED header. Each request creates a fresh, random extra.reference public key that is stored as a pending payment.

Networks use CAIP-2 ids: mainnet solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, devnet solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1. asset is SOL or the USDC mint (mainnet EPjFWdd5…yTDt1v, devnet 4zMMC9sr…DncDU). amount is in base units: lamports (9 decimals) or USDC micro-units (6 decimals).

HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: <base64 of the JSON body>
Content-Type: application/json

{
  "x402Version": 2,
  "error": "Payment required",
  "accepts": [{
    "scheme": "exact",
    "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
    "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "1000000",
    "payTo": "<recipient wallet>",
    "resource": "https://<host>/api/x402/<paywall-id>",
    "description": "<paywall title>",
    "maxTimeoutSeconds": 600,
    "extra": { "reference": "<pubkey>", "decimals": 6, "symbol": "USDC", "memo": "quorlet:<paywall-id>" }
  }]
}

Headers

  • PAYMENT-REQUIRED (response, on 402): base64 of the 402 JSON body.
  • X-PAYMENT-SIGNATURE (request): the Solana transaction signature. The ?sig= query parameter is also accepted.
  • PAYMENT-RESPONSE (response, on 200): base64 of { success, signature, network, payer }.

Verification rules

When a signature is supplied, the server fetches the transaction from Solana RPC at confirmed commitment and unlocks only if all of these hold:

  1. The transaction exists on the paywall's network and did not fail.
  2. Its account keys include a reference that was issued for this paywall and is still pending.
  3. The recipient received at least the price: for SOL, the lamport balance delta; for USDC, the post minus pre token balance for the recipient and mint.
  4. The signature was not used for another payment. Repeating the request with a signature that already unlocked the same paywall returns the content again.

On failure the response is HTTP 402 with an error field describing the reason and an empty accepts array. Request the endpoint again without a signature to get a new reference.

A confirmed transaction can take a moment to appear on RPC. If the error says the transaction was not found, retry after a couple of seconds.

curl

# 1. Ask for the resource. Expect HTTP 402 with payment requirements.
curl -i https://<host>/api/x402/<paywall-id>

# 2. Pay on Solana (see the TypeScript example), then repeat with the signature.
curl -i https://<host>/api/x402/<paywall-id> \
  -H "X-PAYMENT-SIGNATURE: <transaction signature>"

TypeScript buyer (@solana/web3.js)

The example pays a SOL paywall. For USDC, send a transferChecked to the recipient's associated token account (creating it if needed) and add the reference to that instruction the same way.

import {
  Connection, Keypair, PublicKey, SystemProgram,
  Transaction, sendAndConfirmTransaction,
} from "@solana/web3.js";

const endpoint = "https://<host>/api/x402/<paywall-id>";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const payer = Keypair.fromSecretKey(/* your key */);

// 1. Get the requirements
const res = await fetch(endpoint);
if (res.status !== 402) throw new Error("Not a paywall");
const { accepts } = await res.json();
const req = accepts[0]; // scheme "exact", asset "SOL"

// 2. Pay, including the reference as a read-only, non-signer key
const ix = SystemProgram.transfer({
  fromPubkey: payer.publicKey,
  toPubkey: new PublicKey(req.payTo),
  lamports: BigInt(req.amount),
});
ix.keys.push({
  pubkey: new PublicKey(req.extra.reference),
  isSigner: false,
  isWritable: false,
});
const signature = await sendAndConfirmTransaction(
  connection, new Transaction().add(ix), [payer]
);

// 3. Redeem
const unlocked = await fetch(endpoint, {
  headers: { "X-PAYMENT-SIGNATURE": signature },
});
console.log(await unlocked.json()); // { title, contentType, content }