Skip to content

Participant-scoped payment history

SubEtha can expose payment history through the Facilitator for a configured payer and provider. This is a participant-scoped API: a caller is not authorized merely because it knows a tenant or actor identifier. The record must bind that actor to the requested participant role, and the request must carry an authenticated principal.

The history API is an optional capability of a Facilitator deployment. It is not available merely because the standard /verify or /settle endpoints are running.

When the history API is registered, the Facilitator exposes these routes. The ordinary list/read routes require the complete history environment. The proof routes are attached as a group only when all proof issuer, trust-anchor, manifest, and durable artifact settings are present; partial proof configuration fails closed at startup.

Method Path Purpose
POST /history/auth/challenge Create a wallet-login challenge for payer or provider
POST /history/auth/verify Verify the participant’s wallet signature and issue a short-lived identity token
GET /history/payments List payments visible to the authenticated participant
GET /history/payments/:paymentId Read one visible payment
POST /history/proofs/issue Issue a proof when complete proof configuration is enabled
POST /history/proofs/verify Verify a proof when complete proof configuration is enabled
POST /history/proofs/revoke Revoke a proof when complete proof configuration is enabled

The proof routes are unavailable unless the complete proof configuration (issuer, trust anchor, manifest, and durable artifact settings) is enabled; there is no verify-only or revoke-only deployment mode. With complete configuration, the official demo and standalone Facilitator daemon wire issue, offline-capable verification, and revoke through the same HTTP routes. No-proof-config deployments retain ordinary history list/read capability without proof publication. Deployments with no proof configuration should be described as no-proof-config deployments, not as deployments without History.

List/read requests carry the short-lived identity token as an Authorization: Bearer token. GET /history/payments returns { payments: [...] }; pagination is not part of this verified API surface, so consumers must not infer cursor, limit, or ordering semantics.

The three proof routes also require an Authorization: Bearer token and resolve the same authenticated participant identity. The route passes that identity and the requested payment/proof identifier to the History API; authorization for issuance, verification, and revocation is then enforced by the configured API policy. The HTTP verify route is an online API call, even though the underlying verifier can also be used locally with retained artifacts.

This is the short version of the flow; the next section defines the verification states and boundaries precisely.

  1. Issue a signed proof. After a persisted payment reaches accepted or finalized, the configured issuer creates a proof from that record and signs it. These states are not interchangeable: accepted means the request path accepted the payment, while finalized means the later proof-gated settlement completed.
  2. Carry the issuer’s public key safely. The issuer publishes a signed key manifest. The verifier checks that manifest against an out-of-band bootstrap trust anchor before using the listed public key to check the proof signature.
  3. Keep the artifacts together. An operator-managed filesystem export can contain the proof, manifest, and signed status artifacts (for example, proof.json, manifest.json, and a status chain). The current implementation does not provide a hosted registry or universal availability guarantee.
  4. Verify locally. Once those artifacts and the trust anchor have been obtained, the verifier checks signatures, the proof’s internal payment-binding fields, validity times, and—when supplied—the signed status chain without calling the Facilitator or issuer API. Proof-only verification does not receive the canonical persisted record or salt, so it does not recompute the record commitment. Issuance itself may still require the configured Facilitator or another trusted issuer.
  5. Represent revocation separately. Revocation appends a signed status to the chain; it does not modify or delete the original proof bytes. The proof remains available as an issuance-time claim while the later status changes its current-status result.

This flow does not promise automatic long-term retention, replication, or protection against later copying of a bearer proof. Operators are responsible for protecting, backing up, rotating, and deleting filesystem exports according to their deployment requirements.

An issued proof can be verified without calling the Facilitator or issuer API when the verifier has the signed proof and the issuer’s published key manifest. The manifest is validated against an out-of-band bootstrap trust anchor and supplies the issuer key registry used to verify the proof signature.

This is the outage-resilience boundary: if a payer or provider has already received and retained the signed proof, the manifest, and the bootstrap trust anchor, that party can later present and verify the issuer’s payment attestation without the Facilitator being online. The attestation can establish the payment lifecycle state recorded by the issuer—accepted for request-path acceptance or finalized for the later proof-gated settlement—but it is not an unconditional replacement for chain or operator evidence. Current-status claims additionally require the applicable signed status artifact. Proofs and artifacts must be distributed and retained by the participants or their operators in advance; the current implementation does not provide automatic participant delivery, a hosted registry, universal availability, or automatic retention.

The verification states are deliberately separate:

  • VALID_AT_ISSUANCE — the proof structure, signature, and issuance-time key validity verify; the proof carries a signed record-commitment value, but proof-only offline verification does not receive the canonical persisted record or salt and therefore cannot recompute that commitment.
  • VALID_NOW — the proof is valid, the verified active status artifact is effective at the verification time, and the signing key is not revoked at that time.
  • REVOKED — the proof is structurally valid and either a verified signed revocation artifact applies, or an otherwise valid active status is evaluated after the manifest’s issuer key revoked_at has taken effect. Later key metadata alone does not change the proof-only result.
  • STATUS_UNKNOWN — a current-status artifact was supplied, but its provenance, signature, chain, or effective time cannot be verified. For an unexpired valid proof with no status artifact, the verifier returns VALID_AT_ISSUANCE and makes no current-status claim; an expired proof returns EXPIRED.
  • EXPIRED — the proof’s validity window has ended.
  • INVALID — the proof structure, signature, or manifest trust cannot be verified.

Evaluation is ordered by validity first: malformed or untrusted proof data is INVALID; an expired proof is EXPIRED; an unexpired proof without a status artifact is VALID_AT_ISSUANCE, while a supplied but unverifiable status artifact is STATUS_UNKNOWN; VALID_NOW or REVOKED requires a verified effective status or key-revocation condition.

Status artifacts are signed append-only publications bound to the proof, payment, issuer, and key. They change the status assertion, not the held proof itself. The verification path is offline after the artifacts have been obtained; proof issuance may still involve the configured Facilitator or another trusted issuer.

New lifecycle proofs are bound to a canonical persisted payment record through a per-record commitment. Caller-supplied commitments or evidence are not authoritative for this path. An issued proof remains a bearer artifact: participant authorization protects the online history API, but does not prevent later copying of an offline proof.

The configured participant wallet signs the exact newline-delimited challenge message returned by /history/auth/challenge using an EIP-191 personal-sign (viem.verifyMessage) signature. The message contains:

  • the history authentication purpose and domain;
  • tenantId, actorId, and the participant subject (payer or provider);
  • a random nonce; and
  • the challenge expiry.

The verified implementation uses these limits:

  • challenge lifetime: 300 seconds;
  • issued identity-token lifetime: at most 3600 seconds;
  • per-client-IP-plus-subject challenge issuance limit: 10 per minute;
  • per-client-IP-plus-subject verification limit: 20 per minute;
  • pending challenge capacity: 10,000.

The rate-limit key uses the client IP derived from Express req.ip together with the requested subject; deployments using a trusted proxy should configure that proxy behavior deliberately because it affects the client-IP component of the key.

A challenge is single-use. A signature for another wallet, another subject, an expired challenge, or a replayed nonce is rejected. The issued token carries the authenticated participant identity; spoofed x-history-* headers are not an identity source in wallet-token mode. A deployment may explicitly wire the legacy resolver, which reads x-history-tenant and x-history-actor; that compatibility path is not wallet authentication and should be limited to a trusted network.

A configured history record binds the payer and provider independently, for example:

payer: tenant-a / payer-1
provider: tenant-b / provider-1

The payer can read the record only with the payer subject and matching payer binding. The provider can read it only with the provider subject and matching provider binding. An unrelated actor, a subject mismatch, or a record without a required participant binding fails closed in the configured History API, including the standalone path when its complete history environment is present.

The projections are role-specific:

  • Payer: settlement linkage and operational secrets such as mint, treasury, and linkage fields are redacted. The payer view does not expose the real recipient destination through this history surface.
  • Provider: the provider projection may include provider-side settlement evidence, subject to the deployment’s record and authorization configuration.

Authorization and participant binding are separate checks. A tenant grant alone does not establish that the actor participated in a particular payment.

The Facilitator lifecycle adapter records an intent before an irreversible burn, then records accepted and finalized transitions as the settlement progresses. A failed history-intent write rolls back the in-memory reservation so a payment can be retried; it does not silently accept a payment without its required history intent.

The official demo attaches durable JSONL payment history to settlement when PAYMENT_HISTORY_PATH is set. The standalone facilitator constructs and attaches that persistence only when the complete History environment is present, including PAYMENT_HISTORY_PATH, the identity secret, and both payer/provider tenant and actor bindings. Neither path exposes the participant-aware HTTP API from the path variable alone; the complete HTTP History configuration is required for that:

Terminal window
export PAYMENT_HISTORY_PATH=./payment-history.jsonl
export HISTORY_IDENTITY_SECRET='[REDACTED]'
export HISTORY_PAYER_TENANT=tenant-a
export HISTORY_PAYER_ACTOR=payer-1
export HISTORY_PROVIDER_TENANT=tenant-b
export HISTORY_PROVIDER_ACTOR=provider-1

Wallet-bound HTTP login additionally requires HISTORY_PAYER_ADDRESS and HISTORY_PROVIDER_ADDRESS. Keep the identity secret and wallet/private-key material in the environment or a secret manager; never put real values in source, shell history, or public documentation.

The verified persistence implementation is append-oriented and does not define an automatic record-retention period. Operators should choose file protection, backup, rotation, and deletion policies appropriate to the payment data they retain.

History is an audit/read surface, not a replacement for the payment protocol:

  • accepted means the payment was accepted on the request path;
  • finalized means the later proof-gated settlement completed;
  • a history record does not make /settle synchronous with finalization.

See accepted vs finalized and the Facilitator quickstart for the settlement flow.