Skip to content

Documentation

For providers

Issue portions against an x402 endpoint, size your bond, choose a sale mechanism and redeem portions in your server.

Prerequisites

  • An x402 endpoint on Base. Your API must already answer unpaid requests with 402 Payment Required and accept x402 payments. PORTION adds a payment scheme to that flow.
  • A wallet on Base holding enough $PRTN for the bond and a little ETH for gas.
  • A clear unit and a clear promise. Decide what one unit is (one million tokens, one hour of audio, one image), which model you serve, and the uptime and latency you commit to. Watchers check exactly what you declare.

Size the bond

text
bond = value of portions outstanding × bond ratio

The value is the quantity at the highest price the series can sell at. With the default 20 % ratio:

SeriesPriceValueBond (20 %)
5,000M tok of Llama-70B0.704 USDC per M tok3,520 USDC704 USDC of $PRTN
20,000 h of Whisper-L, Dutch from 0.36 to 0.270.36 USDC per h (start)7,200 USDC1,440 USDC of $PRTN

The bond is denominated in $PRTN, so its USDC value moves with the $PRTN price. If its value falls below the requirement, you cannot issue new series until you top it up. The requirement falls as units are burned; call withdrawExcessBond to release the difference.

Issue

Call issue on the PortionFactory with the series parameters. The bond is transferred in the same transaction.

Fixed price

Every unit sells at the same price. Choose it as a discount to your spot price: 10–30 % is the expected range. Predictable for you and for buyers.

Dutch auction

The price starts at the start price and falls linearly to the floor over the auction duration. Each buyer pays the price at the time of purchase. Use it when you do not know the clearing discount: the market finds it. The bond is sized on the start price.

You are paid at each sale: the PortionMarket sends you the USDC minus the 0.75 % fee in the buyer's transaction. Unsold units can be cancelled at any time, which reduces the bond requirement.

Integrate the Redeemer

A buyer pays with a portion by sending a signed redemption in the x402 X-PAYMENT header, under the portion scheme. Your server verifies it before serving, then submits it to the Redeemer, which burns the units actually consumed.

ts
import { createPublicClient, createWalletClient, http, type Hex } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import type { Request as Req, Response as Res } from "express";
import { parseRedemption, redeemerAbi, REDEEMER_ADDRESS } from "./portion"; // ABI and address published at launch
 
const account = privateKeyToAccount(process.env.PROVIDER_KEY as Hex);
const reader = createPublicClient({ chain: base, transport: http() });
const writer = createWalletClient({ account, chain: base, transport: http() });
 
const SERIES_ID = 42n;
 
type Redemption = {
  holder: `0x${string}`;
  seriesId: bigint;
  maxUnits: bigint; // ceiling the buyer authorised for this request
  requestId: Hex; // unique per request, prevents replay
  deadline: bigint;
  nonce: bigint;
};
 
/**
 * Verify the portion before serving, burn after.
 * `serve` runs your handler and returns the units it used.
 */
export async function portionPayment(req: Req, res: Res, serve: () => Promise<number>) {
  const header = req.header("x-payment");
  if (!header) return res.status(402).json(paymentRequirements());
 
  const { scheme, payload } = JSON.parse(Buffer.from(header, "base64").toString());
  if (scheme !== "portion") return res.status(402).json(paymentRequirements());
 
  const r: Redemption = parseRedemption(payload.redemption);
  const signature = payload.signature as Hex;
 
  // 1. Check signature, balance, expiry and dispute status on-chain, without writing.
  const ok = await reader.readContract({
    address: REDEEMER_ADDRESS,
    abi: redeemerAbi,
    functionName: "verify",
    args: [{ ...r, units: r.maxUnits }, signature],
  });
  if (!ok || r.seriesId !== SERIES_ID) return res.status(402).json(paymentRequirements());
 
  // 2. Serve the request and measure what it used, in series units.
  const used = BigInt(await serve());
 
  // 3. Burn exactly what was used, never more than the buyer authorised.
  const units = used < r.maxUnits ? used : r.maxUnits;
  const hash = await writer.writeContract({
    address: REDEEMER_ADDRESS,
    abi: redeemerAbi,
    functionName: "redeem",
    args: [{ ...r, units }, signature],
  });
  res.setHeader("X-PAYMENT-RESPONSE", Buffer.from(JSON.stringify({ scheme: "portion", tx: hash, units: units.toString() })).toString("base64"));
}
 
function paymentRequirements() {
  return {
    x402Version: 1,
    accepts: [{ scheme: "portion", network: "base", seriesId: SERIES_ID.toString(), redeemer: REDEEMER_ADDRESS }],
  };
}

For high request volumes, queue redemptions and submit them with redeemBatch. The buyer's signature covers maxUnits and a deadline, so a queued redemption stays valid only for the window the buyer accepted.

Good practice

  • Declare what you can hold. Watchers measure against your declared uptime, latency and model, not against best effort.
  • Keep a margin on the bond. A $PRTN drawdown can put you under the requirement and block new issuance.
  • Issue in tranches. Several short series with close expiries are easier to price and to honour than one long series.
  • Publish health data. A public status endpoint and signed response logs are your evidence if you need to contest an attestation.
  • Never burn more than was used. Buyers and watchers can compare Redeemed events with their own request logs. Over-burning is a reason to stop buying from you, and a model-mismatch attestation will follow.