Publisher guide

Publisher quickstart

Who this is for: an API publisher who already has API keys (or similar) and wants optional wallet-subscription access via Txpass.

Verification is free. Txpass does not charge publishers a per-call fee, a listing fee, or a platform cut. You bill your users however you already do. If we ever sell publisher-side extras (support, SLA, a directory), verification stays free.

Calls made with a Txpass session count toward the user's existing usage limits once you link their wallet identity to their account in your system. Txpass doesn't meter calls or create a separate quota; linking and enforcing limits stay with you.

Preferred: paste into your coding agent (Path A). Or have the agent fetch facilitator /v1/protocol, /v1/products, /v1/session/jwks and implement from live discovery (Path B). See publisher-onboarding.md — Agent-first onboarding.

Packages

The libraries used below are on npm (early access 0.x; Node.js 20+). Full list and install commands: Packages on npm.

  • @txpass/verify — npm i @txpass/verify
  • @txpass/client — npm i @txpass/client @bsv/sdk (with @bsv/sdk for AuthFetch)
  • @txpass/agent — npm i -g @txpass/agent
  • @txpass/protocol — npm i @txpass/protocol

Verify

Two separate checks. Most APIs only need A.

A. Session check (every request)

The caller sends Authorization: Bearer <jwt>. Verify it locally; no call to Txpass per request.

  1. Fetch the facilitator JWKS from GET {FACILITATOR}/v1/session/jwks (cache it; refetch on unknown_kid).
  2. Verify the JWT signature against that JWKS.
  3. Check iss equals TXPASS_ISSUER, aud equals your product slug, and exp is in the future.
  4. Check scope covers this method and path (scopeAllows).
  5. Read sub: the user's identity key I. When the session is delegated, act.sub is the agent's key.
  6. Optional instant cut-off: POST {FACILITATOR}/v1/session/introspect with { "token": "<jwt>" } and require active: true (see Facilitator API).
import { verifySessionJwt, scopeAllows, isDelegatedSession } from '@txpass/verify'

const jwks = await fetch(`${process.env.TXPASS_FACILITATOR_URL}/v1/session/jwks`).then(r => r.json())
const r = await verifySessionJwt(bearer, jwks, { audience: 'your-product-slug', issuer: process.env.TXPASS_ISSUER })
if (!r.ok || !scopeAllows(r.claims, req.method, req.path)) return res.status(401).end()
const user = r.claims.sub                                          // I
const agent = isDelegatedSession(r.claims) ? r.claims.act!.sub : null // A, when delegated

B. Receipt check (the entitlement behind paid sessions)

A paid session is minted from an on-chain subscription receipt (receipt_txid in the JWT). You keep billing your customers as you do today; after you bill, you ask Txpass to record the subscription and Txpass builds, funds and publishes the receipt (Publisher onboarding, Stripe webhook example). Anyone can check that receipt without Txpass, using @txpass/verify and a chain source.

  1. Fetch the product's sellers and plans from GET {FACILITATOR}/v1/pricing (products[<slug>]) and the Txpass receipt issuer keys from GET {FACILITATOR}/v1/receipts/issuer (accepted_public_keys; pin them in your config).
  2. Read the receipt's txpass / v1 / sub OP_RETURN (on-chain fields): product, plan, window, identity.
  3. Match the identity commitment to the caller's I (with the revealed salt).
  4. Confirm the receipt is Txpass-issued: one input spends an output of an accepted issuer key (verifyReceiptTx with issuers). There are no prices on the receipt.
  5. Check the window is live (valid_from ≤ now < valid_until) and no longer than the plan.
  6. Check the inclusion proof (BUMP, alone or inside a BEEF) against block headers.
  7. Check revoke_out is none or unspent (a chain lookup).
import { transactionFromBeefString, verifyReceiptTx, verifyReceiptInclusion } from '@txpass/verify'
import { WhatsOnChain } from '@bsv/sdk' // or any ChainTracker you trust

const parsed = transactionFromBeefString(beefHex, receiptTxid)
if (!parsed.ok) throw new Error(parsed.message)
const v = verifyReceiptTx({ tx: parsed.tx, receiptTxid, payload, product /* from /v1/pricing */, identity: { reveal, expectedIdentity: I, allowBareIdentity: false }, window: { now: Date.now() / 1000 | 0, maxClockSkewSeconds: 600 }, issuers /* pinned issuer public keys */ })
const inc = await verifyReceiptInclusion(parsed.tx, new WhatsOnChain('main'), { minConfirmations: 1 })
// steps 2–5: verifyReceiptTx (product/plan/window/I + issuer funding) · step 6: verifyReceiptInclusion · step 7: your chain lookup

Full detail: Portable verifier and Protocol v1.

What you get

  • Buyers pay / hold an on-chain (or staging) subscription receipt for your product slug.
  • Buyers AuthFetch to the hosted facilitator, mint a short-lived Bearer JWT.
  • Your API verifies that JWT with JWKS and maps plan → your local tiers/quotas.
  • Existing API keys keep working.

Prerequisites

  • Hosted facilitator base URL: https://txpass.app in production.
  • Shared issuer string (see below).
  • Your product slug and plan allowlist (copy Imprint’s pattern or define your own).

1. Env

TXPASS_FACILITATOR_URL=https://txpass.app
# Set explicitly; must equal the publisher's expected JWT `iss`, with no trailing slash.
TXPASS_ISSUER=https://txpass.app

Issuer rule: Set TXPASS_ISSUER explicitly on both facilitator and publisher to https://txpass.app (full URL, no trailing slash). It must match the publisher's expected JWT iss exactly.

Check:

  • GET {FACILITATOR}/health (liveness only: { "ok": true, "version": … })
  • GET {FACILITATOR}/v1/protocol
  • GET {FACILITATOR}/v1/session/jwks

2. Verify Bearer sessions (publisher)

On protected routes, accept either your existing API key or Authorization: Bearer <txpass-session-jwt>.

The verifier is on npm (early access, Node.js 20+): npm i @txpass/verify.

import { verifySessionJwt } from '@txpass/verify'
// or: jose + createLocalJWKSet against JWKS JSON

const jwks = await fetch(`${process.env.TXPASS_FACILITATOR_URL}/v1/session/jwks`).then(r => r.json())
const result = await verifySessionJwt(bearer, jwks, {
  audience: 'your-product-slug', // e.g. imprintai-api
  issuer: process.env.TXPASS_ISSUER,
  planAllowlist: ['free', 'pro-month', 'pro-year', 'enterprise-month'],
})
if (!result.ok) return res.status(401).json({ error: 'invalid_txpass_session' })

// Map plan → your tier LOCALLY — never trust client quotas
const tier = mapPlanToTier(result.claims.plan)

Store only token_hash_sha256 / jti / identity binding if you persist credentials — never plaintext bearer, never mint a normal long-lived API key from Txpass.

3. Mark routes

KindBehaviour
Paid writeAPI key or live Txpass session; consume quota
Follow-ups (status/finalize)Ownership without re-quota; keep reachable after sub expiry if accepted while live
PublicUnchanged (verify/docs/health)

4. Buyer flow (you document this once)

  1. Buyer uses their wallet (Yours / Metanet / etc.) — you never take a private key.
  2. AuthFetch → facilitator POST /v1/session with { "product": "<slug>" }. If the publisher enabled auto_enroll, first-time identities get plan free; otherwise a live paid entitlement (or 402) is required.
  3. Call your API with Authorization: Bearer <access_token>.

Staging may use a throwaway harness wallet you control for tests; customers never paste WIFs.

Agents on a user's plan (delegation v1)

The user signs a delegation cert for the agent's own key A; the agent sends it in POST /v1/session ({ "product", "delegation" }) over AuthFetch as A. The JWT keeps sub = the user (your existing user/quota mapping works) and adds act.sub = agent and dlg = cert id. @txpass/verify exposes them (isDelegatedSession, scopeAllows, option allowDelegated: false to refuse). Delegated sessions default to 15 min. If a cert carries quota_share (advisory), enforce it per delegation with quotaShareLimits / quotaShareAllows (counter key sub:dlg). Full spec: specs/delegation-v1.md.

Agent policies (one signature, many agents; specs/agent-policy-v1.md) mint the same delegated shape: sub = the user, act.sub = the agent, dlg = that agent's admission id, plus an additive pol claim (the policy id). Your verifier needs no change; quota_share keyed by sub:dlg becomes a per-agent cap.

402 handling for AuthFetch clients

@bsv/sdk 2.7.1 AuthFetch throws Unsupported x-bsv-payment-version response header on any HTTP 402 that is not a BRC-105 micropayment challenge, which is exactly what Txpass returns for "no entitlement". The SDK drops the response body. Do not patch node_modules; use @txpass/client instead (npm i @txpass/client @bsv/sdk):

import { AuthFetch } from '@bsv/sdk';
import { requestSession, safeAuthFetch } from '@txpass/client';

const authFetch = new AuthFetch(wallet);
const r = await requestSession(authFetch, { facilitatorUrl, product: 'imprintai-api' /*, delegation */ });
if (!r.ok && r.error === 'payment_required') {
  // no live entitlement (for delegations: the issuer has none) — prompt to subscribe
}
// or, for any route: const res = await safeAuthFetch(authFetch, url, init); // 402 → synthetic Response

A synthetic response carries x-txpass-client-synthetic: 1 and { "error": "payment_required", "synthetic": true }. If you roll your own client, catch the error whose message matches /Unsupported x-bsv-payment-version/ and treat it as 402 payment_required.

5. Done when

  • ☐ Key path unchanged
  • ☐ Valid session JWT admits a paid route
  • ☐ Wrong/expired/forged JWT rejected
  • ☐ TXPASS_ISSUER matches facilitator iss
  • ☐ Docs mention optional Txpass beside API keys
  • ☐ If using auto_enroll: map plan free → your free tier quotas; production may set TXPASS_ON_NEW_IDENTITY=reject

See also