Publisher guide

Integration brief for coding agents

A filled copy for one product is GET {facilitator}/v1/adopt/{slug}/agent-brief.md (product document: GET {facilitator}/v1/products/{slug}). Use that when the publisher has a slug. The placeholders below are only for a hand paste before those URLs exist for the product.

Paste this entire document into Cursor / Replit / Claude / Codex. Replace placeholders before or after paste. Staging UI may fill them later.

{{FACILITATOR_URL}}   e.g. https://txpass.app
{{PRODUCT_SLUG}}      e.g. imprintai-api
{{TXPASS_ISSUER}}     e.g. https://txpass.app; set explicitly, exact JWT iss, no trailing slash
{{CORS_ORIGINS}}      exact Origins, comma-separated, no trailing slash

Role

You are integrating optional Txpass beside an existing API-key auth system for product {{PRODUCT_SLUG}}. Dual-rail only: do not replace keys, redesign billing, or cut over to Txpass-only.

Warning — second ignition

Enabling Txpass adds a second authenticated path beside API keys (dual-rail). That is intentional: wallet buyers can reach entitled routes without a publisher API key. Production default is on_new_identity=reject unless the publisher explicitly opts into free auto_enroll. Harden with plan allowlists, entitled routes, CORS, and quotas. Developer FAQ: docs/website-faq.md.

Locked rules (v1 — do not violate)

  1. Dual-rail: accept existing API key or Authorization: Bearer <txpass-session-jwt> on entitled routes.
  2. Do not replace API keys; keys stay the primary path for non-wallet buyers.
  3. No human apply on the Txpass path (skips_human_apply: true) — live entitlement is enough.
  4. Do not mint long-lived API keys from Txpass sessions (session/credential table only; hash at rest).
  5. Never log raw JWT / Authorization headers.
  6. BYOW: buyer uses their wallet; publisher never receives a private key / WIF.
  7. Map plan → local tier/quotas locally — never trust client-supplied quotas.
  8. Plan-dependent receipt_txid (do not mix):
    • free → ^autoenroll:[0-9a-f]{32}$ only
    • paid → ^[0-9a-f]{64}$ only

Fetch first (facilitator discovery)

curl -sS {{FACILITATOR_URL}}/v1/protocol
curl -sS {{FACILITATOR_URL}}/v1/products
curl -sS {{FACILITATOR_URL}}/v1/session/jwks

Use /v1/protocol for frozen IDs, the delegation aud and policy hints (newIdentity), /v1/products for each product's routes and new-identity policy, JWKS for verify. (/health is a liveness probe only: { "ok": true, "version": … }.)

Enablement checklist (publisher side)

  1. Env: set TXPASS_FACILITATOR_URL={{FACILITATOR_URL}} and TXPASS_ISSUER={{TXPASS_ISSUER}} explicitly. TXPASS_ISSUER must equal the publisher's expected JWT iss exactly, with no trailing slash; production is https://txpass.app.
  2. Dual-rail middleware: API key or Bearer Txpass session JWT.
  3. Verify via JWKS (GET {{FACILITATOR_URL}}/v1/session/jwks) or @txpass/verify (verifySessionJwt).
  4. Plan allowlist includes free if facilitator auto_enroll is on.
  5. Plan-dependent receipt_txid validation (free vs paid — see Locked rules).
  6. Map plan → local tier/quotas locally.
  7. No human apply on Txpass path.
  8. Do not mint long-lived keys from Txpass.
  9. Never log raw JWT / Authorization.
  10. Buyer UI: AuthFetch → facilitator POST /v1/session with { "product": "{{PRODUCT_SLUG}}" }, then Bearer to publisher API.
  11. Structured verify failure codes in staging (issuer / audience / plan / exp).
  12. Regression: wrong/expired/forged JWT rejected; API-key path unchanged.

Env vars (publisher)

TXPASS_FACILITATOR_URL={{FACILITATOR_URL}}
TXPASS_ISSUER={{TXPASS_ISSUER}}
# Audience / product claim:
#   {{PRODUCT_SLUG}}
# Facilitator (if you operate it): TXPASS_CORS_ORIGINS={{CORS_ORIGINS}}

Verify sketch

import { verifySessionJwt } from '@txpass/verify'

const jwks = await fetch(`${process.env.TXPASS_FACILITATOR_URL}/v1/session/jwks`).then(r => r.json())
const result = await verifySessionJwt(bearer, jwks, {
  audience: '{{PRODUCT_SLUG}}',
  issuer: process.env.TXPASS_ISSUER,
  planAllowlist: ['free', 'pro-month', 'pro-year', 'enterprise-month'], // edit to yours
})
if (!result.ok) return res.status(401).json({ error: 'invalid_txpass_session' })
const tier = mapPlanToTier(result.claims.plan) // local map only

Acceptance tests

  • ☐ Facilitator GET /health, /v1/protocol, /v1/session/jwks OK
  • ☐ Browser AuthFetch session mint (CORS Origin exact match: {{CORS_ORIGINS}})
  • ☐ Free auto-enroll (if enabled): plan: free, receipt_txid = autoenroll: + 32 hex
  • ☐ Entitled write succeeds with Bearer JWT (no human apply)
  • ☐ Paid path expects 64-hex receipt_txid only
  • ☐ API-key path unchanged
  • ☐ Wrong / expired / forged JWT rejected
  • ☐ Entitlements survive facilitator restart when Postgres / DATABASE_URL is set

If blocked — ask the human

Do not invent these. Ask for:

  • Product slug ({{PRODUCT_SLUG}})
  • CORS origins ({{CORS_ORIGINS}})
  • Issuer string ({{TXPASS_ISSUER}} — decode a real JWT iss)
  • Facilitator URL ({{FACILITATOR_URL}})
  • Which routes are entitled vs public-keep-open
  • Plan → local tier map

Docs (fetch if needed)

  • docs/publisher-onboarding.md — intake + full enablement
  • docs/adopter-quickstart.md — thin path
  • docs/facilitator-api.md — HTTP contract
  • docs/publisher-guide.md — deeper dual-rail notes
  • docs/website-faq.md — dual-rail / second-ignition FAQ for launch

Humans should not retype protocol details; fetch live endpoints and these docs.