ReinkeyDocsConsole

Meter reference

reinkey(options)

Creates a client bound to one facilitator and one payee. It reads the network, asset and channel contract from the facilitator's GET /supported, so the call is async.

OptionType
facilitatorstringBase URL of the facilitator that verifies and settles vouchers.
payTostringYour Stellar address. Channels must name it as payee or payment is rejected with WRONG_PAYEE.
publicUrlstring?Public origin of your server, used as the resource in payment terms. Derived from the request when omitted; set it when you sit behind a proxy.
assetstring?Contract id of the payment asset. Learned from the facilitator when omitted.
channelContractstring?Channel contract id. Learned from the facilitator when omitted.
minDepositbigint?Minimum deposit suggested in the 402. Default: 1000 × the price of one call.
timeoutMsnumber?Timeout for calls to the facilitator. Default 10000.
fetchFetchLike?Custom fetch, for tests or a private network layer. Default globalThis.fetch.

Returns:

Member
meter(options)Middleware that makes one endpoint paid. See below.
stream(req, res, options)Runs a per-second or per-token SSE stream after meter() took the first slice. See Streaming units.
acceptVoucher(voucher, ctx)Low level: submits a bare voucher to the facilitator. Throws FacilitatorError on rejection.
deps(options?)Dependencies for the low-level meter(options, deps) export.
network, asset, channelContract, payTo, facilitatorWhat the client resolved.
receiptSignerPublic key that signs receipts, as announced by the facilitator; null if it announces none.
exactEnabledWhether the facilitator advertises the exact scheme. The hosted facilitator does not.

rk.meter(options)

Express-compatible middleware ((req, res, next)), so it also works in Nest and Connect.

OptionType
pricebigintPrice of one unit, in the asset's base unit.
unit"request" | "token" | "second"What one unit is.
descriptionstring?Shown to buyers in the 402 body.
sliceTokensnumber?For token: tokens paid per voucher.
sliceSecondsnumber?For second: seconds paid per voucher.
bazaar{ info?, schema? }?Discovery metadata for the x402 Bazaar extension: an example input (info.input) and output format (info.output), plus an optional JSON schema. Derived from the HTTP method and unit when omitted.

On success the middleware sets req.payment to the receipt, sets the PAYMENT-RESPONSE header and calls next().

The 402 response

JSON
{
  "x402Version": 2,
  "error": "PAYMENT_REQUIRED",
  "source": "gateway",
  "message": "This resource is paid. Send a payment in the PAYMENT-SIGNATURE header.",
  "resource": { "url": "https://api.example.com/book", "description": "Order book" },
  "accepts": [
    {
      "scheme": "channel",
      "network": "stellar:testnet",
      "asset": "C…USDC",
      "payTo": "G…SELLER",
      "amount": "5000",
      "unit": "request",
      "maxTimeoutSeconds": 60,
      "extra": {
        "channelContract": "C…CHANNEL",
        "minDeposit": "5000000",
        "facilitator": "https://…",
        "areFeesSponsored": true
      }
    }
  ]
}

The same object is base64-encoded in the PAYMENT-REQUIRED header. For token and second units, extra also carries sliceTokens / sliceSeconds and sliceAmount (the price of one slice).

A payment that is present but rejected also returns 402, with error set to the reason code and the same accepts, so the buyer can correct and retry.

Discovery (Bazaar)

Every 402 carries an extensions.bazaar object describing how to call the resource:

JSON
"extensions": {
  "bazaar": {
    "info": {
      "input": { "type": "http", "method": "GET", "queryParams": {} },
      "output": { "type": "json", "mimeType": "application/json" }
    },
    "schema": {}
  }
}

The middleware also forwards this to the facilitator with each verification. After the first verified payment, the resource appears in the facilitator's catalog, GET /discovery/resources, where agents (and the reinkey_list_resources MCP tool) can find it. Nothing to register, nothing to keep in sync: a paid endpoint lists itself.

Headers

HeaderDirection
PAYMENT-REQUIREDresponse, on 402base64 JSON of the body above
PAYMENT-SIGNATURErequestbase64 JSON payment payload (x402 v2)
PAYMENT-RESPONSEresponse, on successbase64 JSON receipt
X-PAYMENT / X-PAYMENT-RESPONSEbothaccepted and mirrored for x402 v1 clients

Expose the response headers in CORS if browsers call your API.

Streaming units

With unit: "token" or "second", rk.meter() charges the first slice; rk.stream() then runs the slice loop for you. It opens a session on the facilitator, writes the SSE headers and the session event, and every time a slice is used up it tells the buyer payment-required and waits for the voucher on the facilitator (POST /streams/:id/wait). @reinkey/sdk's streamPaid() is the matching buyer side and needs no configuration: it finds the facilitator in the 402's extra.facilitator.

TypeScript
app.get(
  "/ticker",
  rk.meter({ price: 1000n, unit: "second", sliceSeconds: 1 }),
  async (req, res) => {
    const s = await rk.stream(req, res, { price: 1000n, unit: "second", sliceSeconds: 1 });
    while (await s.next()) {          // false when the buyer stops paying or disconnects
      s.send("tick", await quote());
      await sleep(1000);
    }
    await s.end();                    // settles the session, emits `done`
  },
);

s.next() returns false and sets s.ended when the stream stops: CHANNEL_EXHAUSTED (the deposit can't cover the next slice, decided before waiting), TIMEOUT (no voucher within voucherTimeoutMs, default 10 s), ACCOUNT_FROZEN, or done when the client disconnects. Every stream produces stream.started / stream.ended events in the facilitator's ledger, so it shows up in the console like any other payment.

Reinkey runs on Stellar testnet. The contracts are unaudited. Don't send mainnet funds.