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=rejectvsauto_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)
- Open agent-onboarding-prompt.md.
- Fill placeholders (
{{FACILITATOR_URL}},{{PRODUCT_SLUG}},{{TXPASS_ISSUER}},{{CORS_ORIGINS}}) or leave for the agent to request. - Paste the whole brief into Cursor / Replit / Claude / Codex.
- 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:
GET {facilitator}/v1/products— each product's routes and new-identity policy (new_identity).GET {facilitator}/v1/protocol— frozen protocol IDs / products.GET {facilitator}/v1/session/jwks— verify keys.- Raw docs (this file, adopter-quickstart, facilitator-api, agent brief) from GitHub / site.
Machine-readable sources agents should fetch
| Resource | Status | Purpose |
|---|---|---|
GET /v1/protocol | now | Frozen protocol IDs, products |
GET /v1/products (new_identity), GET /v1/protocol (newIdentity) | now | Policy discovery |
GET /v1/session/jwks | now | Verify keys |
| Docs URLs (raw GitHub / site) | now | Human + agent prose |
GET /v1/products | now | Product list: entitled routes with labels and read/write class, policy, paid status |
GET /v1/products/:slug | now | Product document: routes, new-identity policy, staging host and ticked routes |
GET /v1/adopt/:slug/agent-brief.md | now | Filled prompt. Facilitator URL, slug, staging origin and routes are already substituted |
GET /.well-known/txpass.json | now | Thin pointer at the two URLs above. Not a product |
llms.txt / AGENTS.md on txpass.app | with site | Crawlable agent entry. Not part of the product document |
1. Roles
| Role | Responsibility |
|---|---|
| Buyer wallet | Holds identity I; completes AuthFetch / BRC-103; never gives a private key to the publisher. |
| Publisher API | Serves product routes; dual-rail auth (existing API key or Txpass Bearer JWT); maps plan → local tier/quotas. |
| Hosted facilitator | Indexes entitlements, mints short-lived session JWTs, serves JWKS; default for v1 (vs self-host). |
| On-chain subscription receipt | txpass / 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
| Field | Required? | Example | Notes |
|---|---|---|---|
product_slug | Yes | imprintai-api | Stable kebab-case; used as JWT aud / product claim. Do not rename casually. |
display_name | Yes | ImprintAI Provenance API | Human label in docs / future UI. |
publisher_origin / website | Yes | https://imprintai.link | Publisher home / docs site (not necessarily the API Origin). |
contact | No | ops@example.com | Optional ops/email for onboarding follow-up. |
B. Integration posture
| Field | Required? | Example | Notes |
|---|---|---|---|
dual_rail | Yes (locked) | optional_beside_api_key | Txpass sits beside existing API keys. v1 locked — not a cutover. |
entitled_routes | Yes | POST /api/imprint, GET /api/status/:jobId | Method + path patterns that accept Txpass (and usually keys). |
public_keep_open | Yes | GET /api/health, GET /api/verify/:txid | Must not require Txpass (or paid gates). |
skips_human_apply | Yes (locked) | true | On 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? | Example | Notes |
|---|---|---|---|
slug | Yes | free, pro-month, pro-year, enterprise-month | Free is auto-enroll only (not on-chain). |
duration_days | Yes | 3650 (free), 30, 365 | Free often long-lived for dogfood. |
price_sats | No (optional legacy) | null | Unused for receipt verify (no satoshi floor). Free is always null. Publishers bill off-chain. |
quota.per_day / quota.per_month | Yes | 50 / 1000 | Publisher-owned; facilitator stores for session claims / policy. |
maps_to_local_tier | Yes | free, premium, enterprise | Name of the publisher’s existing tier; mapped locally, never from client. |
D. New-identity policy
| Field | Required? | Example | Notes |
|---|---|---|---|
on_new_identity | Yes | auto_enroll | reject | Staging dogfood often auto_enroll; production may reject. |
auto_enroll_plan | If auto_enroll | free | Usually free. |
| Free quotas | If auto_enroll | 50/day, 1000/month | Publisher-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
| Field | Required? | Example | Notes |
|---|---|---|---|
allowed_origins | Yes (if buyer UI in browser) | https://imprint-staging.example, http://localhost:3000 | Exact 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)
| Field | Required? | Example | Notes |
|---|---|---|---|
TXPASS_ISSUER | Yes | https://txpass.app | Set explicitly; must equal the publisher's expected JWT iss exactly, with no trailing slash. |
| Audience / product | Yes | imprintai-api | Equals product_slug. |
| JWKS URL | Derived | {facilitator}/v1/session/jwks | Publisher verifies with JWKS / @txpass/verify. |
G. Facilitator pointing
| Field | Required? | Example | Notes |
|---|---|---|---|
| Facilitator mode | Yes | hosted | v1 default: hosted facilitator URL. Self-host is optional later. |
| Facilitator base URL | Yes | https://txpass.app | Canonical 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.
| Field | Required | Example | Notes |
|---|---|---|---|
api_origins | Yes (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>/routeswith"api_origins": ["https://api.example.com"]in the usual route-copy body (same owner auth andexpected_versionas saving labels).[]clears it. A product requested through/publisherstarts 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 theTXPASS_PRODUCT_API_ORIGINSenv var, e.g.{"imprintai-api":["https://imprintai.link"]}. - Operator SQL (Postgres): the
products.api_originscolumn (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:
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.UPDATE products SET api_origins = ARRAY['https://imprintai.link'], updated_at = extract(epoch from now())::bigint WHERE slug = 'imprintai-api';
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>:
| Field | Example | Notes |
|---|---|---|
docs_url | "https://docs.example.com/api" | Public API docs. https only, no credentials. null or "" clears it. |
examples | see below | Up 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>/routeswith"docs_url"and/or"examples"in the usual route-copy body (same owner auth andexpected_version). Omitted → unchanged;[]/nullclears (built-ins fall back to their seed). The dashboard page has no form for these yet. - Built-in products: a code seed.
imprintai-apishipscreate(POST /api/imprint, minimal body above, key fields,workflow_idwarning) andstatus(GET /api/status/:jobId), taken from Imprint's request schema. A non-empty DB value wins. - Operator SQL:
products.docs_url(TEXT) andproducts.examples(JSONB, default'[]'), added automatically when the facilitator starts. SQL skips validation, so prefer the API.
J. Link wallet identities to accounts (usage limits)
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 (withdlg, andpolfor policy sessions). Absent when the user calls directly.plan— the plan slug the session was minted for (alsoproduct,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:
- 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
subagainst their account. - 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
/publisherwith (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). CheckGET /v1/receipts/issuer: your product shows"ready": trueonce 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 event | Call |
|---|---|
| Subscription starts | issueReceipt for the paid period |
| Renewal paid | issueReceipt 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 end | Nothing. The receipt simply ends at valid_until |
| Immediate cancel, refund, chargeback | revokeReceipt (by receipt_txid or the event_id that issued it) |
| Payment failed | Your choice: leave the receipt to run out, or revoke it now |
| Customer links a wallet for the first time | syncReceipt 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):
- Env: Set
TXPASS_FACILITATOR_URL=https://txpass.appandTXPASS_ISSUER=https://txpass.appexplicitly in production (no trailing slash). The latter must match the publisher's expected JWTissexactly. - Dual-rail middleware: accept existing API key or
Authorization: Bearer <txpass-session-jwt>. - Verify via JWKS (
GET {facilitator}/v1/session/jwks) or@txpass/verify(verifySessionJwt). - Plan allowlist including
freeifauto_enrollis enabled. - Plan-dependent
receipt_txidvalidation (do not mix):- free →
^autoenroll:[0-9a-f]{32}$only - paid →
^[0-9a-f]{64}$only
- free →
- Map plan → local tier/quotas locally — never trust client-supplied quotas.
- No human apply on the Txpass path (
skips_human_apply: true). - Do not mint long-lived API keys from Txpass sessions (session/credential table only; hash at rest).
- Never log raw JWT /
Authorizationheaders. - Buyer UI: AuthFetch to facilitator
POST /v1/session, then Bearer to publisher API. - Structured verify failure codes in staging (e.g. issuer/audience/plan/exp) for faster debug.
- Regression: wrong / expired / forged JWT rejected; existing API-key path unchanged.
4. Buyer journey (short)
- Buyer uses their wallet (BYOW — Yours / Metanet / etc.); publisher never receives a private key.
- AuthFetch to hosted facilitator →
POST /v1/sessionwith{ "product": "<slug>" }. - If
auto_enroll: first-time identity gets planfree(syntheticautoenroll:…receipt); else live paid entitlement (or 402). - Buyer calls publisher API with
Authorization: Bearer <access_token>. - 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/jwkson facilitator OK - ☐ Browser AuthFetch session mint (CORS Origin exact match)
- ☐ Free auto-enroll works when enabled (
plan: free,receipt_txidmatchesautoenroll:+ 32 hex) - ☐ Entitled write succeeds with Bearer JWT (no human apply)
- ☐ Paid path still expects 64-hex
receipt_txidonly (no mixing with autoenroll) - ☐ API-key path still works unchanged
- ☐ Wrong / expired / forged JWT rejected
- ☐ Restart persistence: entitlements survive facilitator restart when Postgres /
DATABASE_URLis 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