Publisher guide

Publisher onboarding: intake and enablement


Second ignition (dual-rail): Txpass does not replace your API keys. Enabling it adds a second authenticated path — wallet → facilitator session JWT → your API verifies Bearer via JWKS. You control how open that path is (on_new_identity=reject vs auto_enroll, plans, routes, CORS, quotas). Production default: reject; free auto-enroll is explicit opt-in. See website-faq.md.

Agent-first onboarding (preferred)

Humans should not be the integration middleware. Coding agents are the primary integrator; humans supply product decisions (slug, origins, issuer) when asked.

Path A — Copy-paste brief (ship now)

  1. Open agent-onboarding-prompt.md.
  2. Fill placeholders ({{FACILITATOR_URL}}, {{PRODUCT_SLUG}}, {{TXPASS_ISSUER}}, {{CORS_ORIGINS}}) or leave for the agent to request.
  3. Paste the whole brief into Cursor / Replit / Claude / Codex.
  4. Agent implements dual-rail + verify against this checklist (§3).

Path B — Agent fetches live sources (ship now)

Agent discovers policy and protocol without human retyping:

  1. GET {facilitator}/v1/products — each product's routes and new-identity policy (new_identity).
  2. GET {facilitator}/v1/protocol — frozen protocol IDs / products.
  3. GET {facilitator}/v1/session/jwks — verify keys.
  4. Raw docs (this file, adopter-quickstart, facilitator-api, agent brief) from GitHub / site.

Machine-readable sources agents should fetch

ResourceStatusPurpose
GET /v1/protocolnowFrozen protocol IDs, products
GET /v1/products (new_identity), GET /v1/protocol (newIdentity)nowPolicy discovery
GET /v1/session/jwksnowVerify keys
Docs URLs (raw GitHub / site)nowHuman + agent prose
GET /v1/productsnowProduct list: entitled routes with labels and read/write class, policy, paid status
GET /v1/products/:slugnowProduct document: routes, new-identity policy, staging host and ticked routes
GET /v1/adopt/:slug/agent-brief.mdnowFilled prompt. Facilitator URL, slug, staging origin and routes are already substituted
GET /.well-known/txpass.jsonnowThin pointer at the two URLs above. Not a product
llms.txt / AGENTS.md on txpass.appwith siteCrawlable agent entry. Not part of the product document

1. Roles

RoleResponsibility
Buyer walletHolds identity I; completes AuthFetch / BRC-103; never gives a private key to the publisher.
Publisher APIServes product routes; dual-rail auth (existing API key or Txpass Bearer JWT); maps plan → local tier/quotas.
Hosted facilitatorIndexes entitlements, mints short-lived session JWTs, serves JWKS; default for v1 (vs self-host).
On-chain subscription receipttxpass / v1 / sub record of a paid plan the publisher already billed; Txpass builds and funds it at the publisher's signed request (K). Free tier uses facilitator-issued synthetic ids only (v1).

2. Intake form — what the publisher provides to Txpass

This section is the future staging UI field list. Freeze these fields before any UI work. UI is blocked on freezing §2.

A. Product identity

FieldRequired?ExampleNotes
product_slugYesimprintai-apiStable kebab-case; used as JWT aud / product claim. Do not rename casually.
display_nameYesImprintAI Provenance APIHuman label in docs / future UI.
publisher_origin / websiteYeshttps://imprintai.linkPublisher home / docs site (not necessarily the API Origin).
contactNoops@example.comOptional ops/email for onboarding follow-up.

B. Integration posture

FieldRequired?ExampleNotes
dual_railYes (locked)optional_beside_api_keyTxpass sits beside existing API keys. v1 locked — not a cutover.
entitled_routesYesPOST /api/imprint, GET /api/status/:jobIdMethod + path patterns that accept Txpass (and usually keys).
public_keep_openYesGET /api/health, GET /api/verify/:txidMust not require Txpass (or paid gates).
skips_human_applyYes (locked)trueOn the Txpass path, live entitlement is enough; no Clerk/human apply.

C. Plans

Default template (publishers may rename paid slugs later; they must keep the plan → local tier map on their side):

Field (per plan)Required?ExampleNotes
slugYesfree, pro-month, pro-year, enterprise-monthFree is auto-enroll only (not on-chain).
duration_daysYes3650 (free), 30, 365Free often long-lived for dogfood.
price_satsNo (optional legacy)nullUnused for receipt verify (no satoshi floor). Free is always null. Publishers bill off-chain.
quota.per_day / quota.per_monthYes50 / 1000Publisher-owned; facilitator stores for session claims / policy.
maps_to_local_tierYesfree, premium, enterpriseName of the publisher’s existing tier; mapped locally, never from client.

D. New-identity policy

FieldRequired?ExampleNotes
on_new_identityYesauto_enroll | rejectStaging dogfood often auto_enroll; production may reject.
auto_enroll_planIf auto_enrollfreeUsually free.
Free quotasIf auto_enroll50/day, 1000/monthPublisher-owned; override via config/env.

Explicit (v1): Free entitlements are facilitator-issued. Synthetic receipt_txid = autoenroll: + 32 hex (^autoenroll:[0-9a-f]{32}$). No on-chain free receipts in v1.

E. Browser / CORS

FieldRequired?ExampleNotes
allowed_originsYes (if buyer UI in browser)https://imprint-staging.example, http://localhost:3000Exact Origins for AuthFetch from buyer UI (scheme+host+port, no trailing slash).

Staging uses env TXPASS_CORS_ORIGINS (comma-separated). Production may move to a DB allowlist when multi-tenant.

F. Session / JWT alignment (critical)

FieldRequired?ExampleNotes
TXPASS_ISSUERYeshttps://txpass.appSet explicitly; must equal the publisher's expected JWT iss exactly, with no trailing slash.
Audience / productYesimprintai-apiEquals product_slug.
JWKS URLDerived{facilitator}/v1/session/jwksPublisher verifies with JWKS / @txpass/verify.

G. Facilitator pointing

FieldRequired?ExampleNotes
Facilitator modeYeshostedv1 default: hosted facilitator URL. Self-host is optional later.
Facilitator base URLYeshttps://txpass.appCanonical production URL for health/protocol/jwks/session.

YAML example (Imprint-shaped — copy and edit)

product_slug: imprintai-api
display_name: ImprintAI Provenance API
publisher_origin: https://imprintai.link
contact: null

integration: optional_beside_api_key   # dual_rail — v1 locked
txpass_skips_human_apply: true         # locked

entitled_routes:
  - POST /api/imprint
  - GET /api/status/:jobId
  - POST /api/log
  - POST /api/metadata
  - POST /api/label
  - POST /api/c2pa/label
  - POST /api/label/finalize
  - GET /api/metadata/workflow/:workflowId
  - GET /api/metadata/:id

public_keep_open:
  - GET /api/verify/:txid
  - GET /api/verify/chain/:txid
  - GET /api/metadata/chain/:txid
  - GET /api/resolve/:txid
  - POST /api/decode
  - GET /api/c2pa/options
  - GET /api/health
  - GET /api/blockchain/tx/:txid
  - GET /api/blockchain/confirm/:txid
  - GET /api/wallet/status

plans:
  - slug: free
    duration_days: 3650
    price_sats: null
    quota: { per_day: 50, per_month: 1000 }
    maps_to_local_tier: free
  - slug: pro-month
    duration_days: 30
    price_sats: null
    quota: { per_day: 200, per_month: 5000 }
    maps_to_local_tier: premium
  - slug: pro-year
    duration_days: 365
    price_sats: null
    quota: { per_day: 200, per_month: 5000 }
    maps_to_local_tier: premium
  - slug: enterprise-month
    duration_days: 30
    price_sats: null
    quota: { per_day: 1000, per_month: 30000 }
    maps_to_local_tier: enterprise

on_new_identity: auto_enroll
auto_enroll_plan: free

allowed_origins:
  - https://imprint-staging.example
  - http://localhost:3000

facilitator:
  mode: hosted
  base_url: https://txpass.app
  # Set TXPASS_ISSUER explicitly to https://txpass.app (exact JWT iss, no trailing slash).

H. API origins (where agents send session tokens)

Agents (@txpass/agent 0.1.4 and later) only send a session token to the API origins registered for the product. They read them from GET /v1/products/<slug> → api_origins, so users don't have to run txpass-agent config --api-base.

FieldRequiredExampleNotes
api_originsYes (for agents)["https://imprintai.link"]Bare origins only: scheme://host[:port], no path, no trailing slash. https required (http://localhost only for dev). Up to 10.

How it is set:

  • Publisher dashboard API (owner auth): POST /v1/publisher/products/<slug>/routes with "api_origins": ["https://api.example.com"] in the usual route-copy body (same owner auth and expected_version as saving labels). [] clears it. A product requested through /publisher starts with the origin of its API base URL.
  • Built-in products without a saved copy: a code seed (imprintai-api → https://imprintai.link). An operator can replace it with the TXPASS_PRODUCT_API_ORIGINS env var, e.g. {"imprintai-api":["https://imprintai.link"]}.
  • Operator SQL (Postgres): the products.api_origins column (TEXT[], default '{}', added automatically when the facilitator starts). A non-empty value wins over the seed. It only applies when the product already has a row:
    UPDATE products SET api_origins = ARRAY['https://imprintai.link'], updated_at = extract(epoch from now())::bigint
     WHERE slug = 'imprintai-api';
    Use only normalised origins (lowercase host, no path, no trailing slash); SQL skips the API's validation. Public responses are cached for up to 15 s (store) and 60 s (HTTP), and agents cache the list for 1 h.

I. API docs and example requests (what agents send)

Agents can't guess request bodies. txpass-agent describe <slug>, the MCP txpass_describe tool and the agent brief's Calling the API section all read two optional fields from GET /v1/products/<slug>:

FieldExampleNotes
docs_url"https://docs.example.com/api"Public API docs. https only, no credentials. null or "" clears it.
examplessee belowUp to 10. Each: id (a-z, 0-9, -, ≤ 40; used by txpass-agent request --example <id>), method, path (a path pattern such as /api/status/:jobId, no host or query), description (required, ≤ 500), optional body (JSON object, ≤ 4 KiB, not for GET/HEAD), notes (≤ 10 strings) and fields (≤ 30 of { name, type, required?, description? }). 32 KiB in total.
"examples": [
  { "id": "create", "method": "POST", "path": "/api/imprint", "description": "Create an imprint.",
    "body": { "model": "txpass-test", "type": "created", "async": true },
    "fields": [{ "name": "model", "type": "string", "required": true }],
    "notes": ["Don't send workflow_id in tests."] },
  { "id": "status", "method": "GET", "path": "/api/status/:jobId", "description": "Poll a job." }
]

Make bodies minimal and safe to send. Agents run them as-is, and each call counts toward the user's limits.

How it is set:

  • Publisher dashboard API (owner auth): POST /v1/publisher/products/<slug>/routes with "docs_url" and/or "examples" in the usual route-copy body (same owner auth and expected_version). Omitted → unchanged; [] / null clears (built-ins fall back to their seed). The dashboard page has no form for these yet.
  • Built-in products: a code seed. imprintai-api ships create (POST /api/imprint, minimal body above, key fields, workflow_id warning) and status (GET /api/status/:jobId), taken from Imprint's request schema. A non-empty DB value wins.
  • Operator SQL: products.docs_url (TEXT) and products.examples (JSONB, default '[]'), added automatically when the facilitator starts. SQL skips validation, so prefer the API.

Txpass doesn't meter calls or create a separate quota; linking and enforcing limits stay with you. A verified session token tells you who is calling:

  • sub — the user's wallet identity key (the entitlement owner).
  • act.sub — the agent key, on delegated / policy sessions only (with dlg, and pol for policy sessions). Absent when the user calls directly.
  • plan — the plan slug the session was minted for (also product, scope).

To make session calls count toward the user's existing limits, map sub to an account in your system, then apply that account's existing quota and tier:

  1. One-time link flow (recommended): a signed-in user proves control of the wallet (e.g. connects it and mints a session, or signs a challenge) and you store sub against their account.
  2. Or match an account record you already hold for that identity key.

If sub isn't linked, decide what happens: reject, or route calls to a separate principal/bucket with its own limits (a demo setup may use a separate principal). Key limits on the account, not the agent key, so every agent a user connects shares the same quota.

K. Publisher-issued receipts (paid plans)

You bill your customers the way you already do (card, invoice, your existing plans). Txpass is not part of that billing and never sees prices. After you bill, you tell Txpass the subscription window and Txpass records it on chain for the customer's wallet identity: it builds the txpass / v1 / sub receipt, funds it from the Txpass receipt issuer key, broadcasts it and registers it. Agents of that customer can then mint sessions on the paid plan, and any verifier can check the receipt without Txpass.

What you need

  • Your publisher key: an owner identity of your product, the wallet key you sign in to /publisher with (the product must be live). Keep the private key on your server; Txpass only ever holds the public key and issues no API keys. Nothing else is registered per product: no seller key, no pay-to address.
  • Your plan slugs: your own plan names (e.g. pro-month). Each receipt covers at least 30 days and at most one year (or the plan's length when Txpass knows it). Check GET /v1/receipts/issuer: your product shows "ready": true once Txpass has turned issuing on.
  • The customer's wallet identity key, linked to their account in your system (see J).

How you call it. Each call is an AuthFetch (BRC-103) request from your publisher key whose body is a request document signed by the same key, with a timestamp, a random nonce and your own event_id (e.g. your billing webhook event id). @txpass/client does all of this:

import { PrivateKey, ProtoWallet } from '@bsv/sdk'
import { createPublisherClient } from '@txpass/client'

const txpass = createPublisherClient({
  facilitatorUrl: 'https://txpass.app',
  wallet: new ProtoWallet(PrivateKey.fromWif(process.env.TXPASS_PUBLISHER_KEY_WIF!)),
})

await txpass.issueReceipt({
  product: 'imprintai-api', plan: 'pro-month', identity: customerIdentityKey,
  valid_from: periodStart, valid_until: periodEnd, event_id: billingEventId,
})

Which call for which billing event

Billing eventCall
Subscription startsissueReceipt for the paid period
Renewal paidissueReceipt for the next period; valid_from may equal the end of the current receipt (chaining), so access never lapses
Plan change (upgrade or downgrade)changePlan: revoke the current receipt, then issue the new plan for the rest of the period. Receipts are at least 30 days; only this plan-change receipt may be shorter, and then it may not end after the one it replaces
Cancel at period endNothing. The receipt simply ends at valid_until
Immediate cancel, refund, chargebackrevokeReceipt (by receipt_txid or the event_id that issued it)
Payment failedYour choice: leave the receipt to run out, or revoke it now
Customer links a wallet for the first timesyncReceipt once for the current period (a no-op if it is already recorded)

Retries are safe. The same event_id with the same content never issues twice (you get the stored result back); the same event_id with different content is rejected. Every request carries a fresh nonce and a timestamp within 5 minutes, so a captured request cannot be replayed.

Copy-paste webhook example: Stripe webhook example. Full reference: Facilitator API.

3. What the publisher enables on their side (checklist)

Generic (not Imprint-specific):

  1. Env: Set TXPASS_FACILITATOR_URL=https://txpass.app and TXPASS_ISSUER=https://txpass.app explicitly in production (no trailing slash). The latter must match the publisher's expected JWT iss exactly.
  2. Dual-rail middleware: accept existing API key or Authorization: Bearer <txpass-session-jwt>.
  3. Verify via JWKS (GET {facilitator}/v1/session/jwks) or @txpass/verify (verifySessionJwt).
  4. Plan allowlist including free if auto_enroll is enabled.
  5. Plan-dependent receipt_txid validation (do not mix):
    • free → ^autoenroll:[0-9a-f]{32}$ only
    • paid → ^[0-9a-f]{64}$ only
  6. Map plan → local tier/quotas locally — never trust client-supplied quotas.
  7. No human apply on the Txpass path (skips_human_apply: true).
  8. Do not mint long-lived API keys from Txpass sessions (session/credential table only; hash at rest).
  9. Never log raw JWT / Authorization headers.
  10. Buyer UI: AuthFetch to facilitator POST /v1/session, then Bearer to publisher API.
  11. Structured verify failure codes in staging (e.g. issuer/audience/plan/exp) for faster debug.
  12. Regression: wrong / expired / forged JWT rejected; existing API-key path unchanged.

4. Buyer journey (short)

  1. Buyer uses their wallet (BYOW — Yours / Metanet / etc.); publisher never receives a private key.
  2. AuthFetch to hosted facilitator → POST /v1/session with { "product": "<slug>" }.
  3. If auto_enroll: first-time identity gets plan free (synthetic autoenroll:… receipt); else live paid entitlement (or 402).
  4. Buyer calls publisher API with Authorization: Bearer <access_token>.
  5. On paid upgrade: on-chain subscribe → receipt ingest → new live entitlement preferred over free.

5. Acceptance smoke (publisher)

Mirror of successful Imprint free + Yours dogfood:

  • ☐ GET /health, GET /v1/protocol, GET /v1/session/jwks on facilitator OK
  • ☐ Browser AuthFetch session mint (CORS Origin exact match)
  • ☐ Free auto-enroll works when enabled (plan: free, receipt_txid matches autoenroll: + 32 hex)
  • ☐ Entitled write succeeds with Bearer JWT (no human apply)
  • ☐ Paid path still expects 64-hex receipt_txid only (no mixing with autoenroll)
  • ☐ API-key path still works unchanged
  • ☐ Wrong / expired / forged JWT rejected
  • ☐ Restart persistence: entitlements survive facilitator restart when Postgres / DATABASE_URL is configured

7. Out of scope

  • Proxying upstream APIs / holding publisher upstream keys
  • Key vault / custodial wallets
  • Replacing Stripe entirely for all customers
  • On-chain free receipts (v1 = facilitator-issued only)
  • Cloud Agent scaffolding notes