Portable verifier
The hosted Txpass index is a convenience cache. The receipt on chain is the source of truth.
What ships
- Open protocol doc —
docs/protocol.md+ SCOPE / PROTOCOL_IDS. @txpass/verify— parse subscribe/revoke OP_RETURN payload fields, commitment helpers, entitlement-at-time evaluation, and full receipt-transaction verification:verifyReceiptTx: txid, a single txpass OP_RETURN that exactly matches the payload, seller/product/plan, identity binding, either an input funded by an accepted Txpass receipt issuer key (issuersoption; the normal, publisher-issued form) or a legacy pay-to output, and the window (plan duration + grace; legacy pay-to receipts also need ≥ 30 days and no future start)findIssuerFundingInput: which input proves issuer funding (needs the parent tx: from the BEEF, or fetch it bysourceTXID)verifyReceiptInclusion: merkle path checked against a ChainTracker, with a minimum-confirmations settingtransactionFromBeefString: BEEF (BRC-62/95/96) input
- Auth middleware (in-repo, not on npm) — Express template under
packages/auth-middleware: after mutual auth resolves identityI, call verify entitlement. It is not published as an npm package. - Facilitator — ingest / entitlements endpoints that dogfood the same packages. Chain access (tx, merkle path, headers, spent lookups) sits behind a
ReceiptChainProviderinterface (WhatsOnChain by default), and the published config is atGET /v1/pricing.
Acceptance
A third party with only the protocol doc + @txpass/verify can admit a known receipt without calling txpass.app.
Example (any operator)
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)
// Pin the issuer public keys in your own config (from GET /v1/receipts/issuer once, or your own issuer if you run a fork).
const issuers = ['03…']
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 })
// v.issuedBy is set for a Txpass-issued receipt
const inc = await verifyReceiptInclusion(parsed.tx, new WhatsOnChain('main'), { minConfirmations: 1 })
Paid receipt duration
Paid plans are monthly or longer (duration_days 30..3660). A receipt's window may not exceed the plan's duration_days plus grace. Txpass issues receipts of at least 30 days, except after a plan change, when the replacing receipt covers the rest of the replaced one's period. The payload carries no plan-change marker, so the verifier accepts issuer-attested receipts shorter than 30 days (it trusts the issuer for that rule); legacy pay-to receipts keep the 30-day minimum in the verifier. Txpass-issued receipts may also start in the future when they chain a renewal onto the current receipt. Agent sessions stay short (15–60 minutes) and are minted from a live entitlement. Free auto-enroll is separate and unchanged.
Proving your own receipt
Receipts carry an identity commitment SHA256(I || salt), not your key. To prove a receipt is yours to a verifier that does not call Txpass, give it the receipt_txid, the payload, your identity key I and the salt. Txpass shows the salt only to you: on /account next to each receipt (Copy salt), and in GET /v1/account/entitlements (reveal.saltHex) when you call it signed in with your wallet. It is never published, never given to the publisher and never shown to anyone else.
Who you trust
A Txpass-issued receipt says Txpass recorded this subscription for the publisher identity named in its seller field, at that publisher's signed request. Txpass issues only for an owner of the product (the keys that manage it in /publisher) or a seller key configured for it, and stores only their public keys. So the issuer key is the trust anchor: to verify a Txpass-issued receipt you need no list of seller keys. With @txpass/verify, pass the payload's seller in product.sellers (or pin the publisher keys you know, if you want to be stricter), leave payToLockingScriptsHex empty, and pass the accepted issuer keys as issuers; a receipt without an issuer-funded input then fails as receipt_wrong_payee. Plan durations: use the product's plans from GET /v1/receipts/issuer ("any" means a one-year upper bound). You decide which issuer keys you accept: the facilitator publishes its current and retired keys at GET /v1/receipts/issuer, and you can pin them, add your own, or accept none (then only legacy pay-to receipts verify). Revocations of Txpass-issued receipts are also published on chain as txpass / v1 / revoke records funded by the issuer key.
Current limits
- Seeing that a
revoke_out(the output that can be spent to revoke the receipt) was spent requires a chain lookup. The facilitator checks when the receipt is ingested, then again from a cache at most every 10 minutes when a session or entitlement is loaded. Nothing watches the chain in the background. A revoke outpoint that sits inside the receipt transaction itself cannot be expressed yet. - A chain reorganization after the receipt was activated is not checked again.
- By default, block headers are trusted from WhatsOnChain. An operator can supply their own header source.