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/sdkfor 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.
- Fetch the facilitator JWKS from
GET {FACILITATOR}/v1/session/jwks(cache it; refetch onunknown_kid). - Verify the JWT signature against that JWKS.
- Check
issequalsTXPASS_ISSUER,audequals your product slug, andexpis in the future. - Check
scopecovers this method and path (scopeAllows). - Read
sub: the user's identity keyI. When the session is delegated,act.subis the agent's key. - Optional instant cut-off:
POST {FACILITATOR}/v1/session/introspectwith{ "token": "<jwt>" }and requireactive: 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.
- Fetch the product's sellers and plans from
GET {FACILITATOR}/v1/pricing(products[<slug>]) and the Txpass receipt issuer keys fromGET {FACILITATOR}/v1/receipts/issuer(accepted_public_keys; pin them in your config). - Read the receipt's
txpass / v1 / subOP_RETURN (on-chain fields): product, plan, window, identity. - Match the
identitycommitment to the caller'sI(with the revealed salt). - Confirm the receipt is Txpass-issued: one input spends an output of an accepted issuer key (
verifyReceiptTxwithissuers). There are no prices on the receipt. - Check the window is live (
valid_from≤ now <valid_until) and no longer than the plan. - Check the inclusion proof (BUMP, alone or inside a BEEF) against block headers.
- Check
revoke_outisnoneor 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.appin 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/protocolGET {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
| Kind | Behaviour |
|---|---|
| Paid write | API key or live Txpass session; consume quota |
| Follow-ups (status/finalize) | Ownership without re-quota; keep reachable after sub expiry if accepted while live |
| Public | Unchanged (verify/docs/health) |
4. Buyer flow (you document this once)
- Buyer uses their wallet (Yours / Metanet / etc.) — you never take a private key.
- AuthFetch → facilitator
POST /v1/sessionwith{ "product": "<slug>" }. If the publisher enabledauto_enroll, first-time identities get planfree; otherwise a live paid entitlement (or 402) is required. - 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_ISSUERmatches facilitatoriss - ☐ Docs mention optional Txpass beside API keys
- ☐ If using auto_enroll: map plan
free→ your free tier quotas; production may setTXPASS_ON_NEW_IDENTITY=reject
See also
- agent-onboarding-prompt.md — Paste this into your coding agent
- facilitator-api.md — HTTP contract
- specs/delegation-v1.md — agent delegation (
I → A) spec - publisher-guide.md — deeper / Imprint-shaped notes
- portable-verifier.md — forkable verify package
- publisher-onboarding.md — intake + enablement + agent-first Path A/B/C