npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@spoolis/receipt-verifier

v0.1.9

Published

Verify signed Spoolis Outcome Receipts offline. Spoolis is Outcome infrastructure for machine commerce.

Readme

Verify a Spoolis outcome receipt

import { verifyOutcomeReceipt } from '@spoolis/receipt-verifier'

const result = await verifyOutcomeReceipt(receipt, { trustSet })
console.log(result.valid)
console.log(receipt.amounts.earned)

Install

npm install @spoolis/receipt-verifier

No Spoolis account is required to verify a receipt. The package has no runtime dependencies and uses WebCrypto in Node 20 or later, browsers, workers, and agent sandboxes.

Require an outcome

requireOutcome verifies the full receipt before applying the consumer's policy. This example acts only on a valid Outcome with a passing result, at least 100 accepted units, and fully satisfied evidence requirements:

import { requireOutcome } from '@spoolis/receipt-verifier'

const required = await requireOutcome(receipt, {
  minimum_status: 'pass',
  min_accepted_units: 100,
  evidence_requirements: 'satisfied',
}, { trustSet })

if (!required.ok) throw new Error(required.reasons.join(', '))

A partial batch is result: 'partial', not pass, so minimum_status: 'pass' refuses an 8-of-10 job even though value was earned. To act on partial batches, gate on the units and the money instead: set minimum_status: 'partial' (or omit it) and require min_accepted_units or min_earned. The receipt's amounts.earned already excludes rejected and uncertain units.

Three more policy keys cover common consumer rules:

  • required_conditions: ['deploy_gate'] requires each listed condition id to appear in condition_results with result: 'pass'. A failing condition adds required_condition_failed:<id>; a condition the receipt never evaluated adds required_condition_missing:<id>.
  • max_uncertain_units: 0 refuses a unitized receipt whose units.uncertain exceeds the cap (too_many_uncertain_units). Zero is the no-uncertain gate. A receipt without units adds units_absent.
  • min_accepted_ratio: 0.8 requires units.accepted / units.total to reach the ratio (a decimal between 0 and 1), else accepted_ratio_below_minimum. A receipt without units adds units_absent.

The policy deliberately stops there: no boolean expressions, no cross-receipt logic, and no predicates over evidence content. Consumers with richer rules read the receipt fields directly and decide in their own code.

The optional third argument accepts the same verification options as verifyOutcomeReceipt, including trustSet, environment, statusSource, and now. The issuer policy matches the verified receipt.signing_key_id. Spoolis supplies the signed fact; the consumer decides the rule and whether to act. Forward supersession cannot be determined from an offline receipt, so consumers that need current correction or revocation state should provide the hosted status response through statusSource.

Verification remains offline by default. Set require_current: true only when the consumer must fail closed without current status, and pass a statusSource fetched from GET /api/v1/receipts/{receipt_id}/status after mapping its state to the verifier status shape.

Reading partial, unitized results

A partial delivery is not "nothing happened." An 8-of-10 job has result: 'partial', eight accepted units, and real earned value. Helpers read a receipt you have ALREADY verified:

import { acceptedUnits, rejectedUnits, uncertainUnits, earnedAmount } from '@spoolis/receipt-verifier'

const accepted = acceptedUnits(receipt)   // published unit rows with verdict pass, or null
const retry = rejectedUnits(receipt)      // rows to retry, or null
const unknown = uncertainUnits(receipt)   // rows excluded from earned value, or null
const earned = earnedAmount(receipt)      // decimal string, or null

The per-unit helpers return null when the receipt does not publish per-unit rows; null means "not published," never "zero units." Counts are always available on receipt.units directly.

A common integration trap: gating unitized work with requireOutcome(receipt, { minimum_status: 'pass' }) refuses every partial batch even though value was earned and accepted units exist. A workflow that should act on partial results inspects the units and the money instead: set minimum_status: 'partial' (or omit it), then gate with min_accepted_units, min_accepted_ratio, or min_earned, and route rejected units to retry. Aggregate status, the accepted unit set, earned value, and your downstream policy are four different reads; do not collapse them into one boolean.

Acting on an Outcome

These helpers shape data from a receipt you have already verified. They do not execute an action or call a payment rail.

import { acceptedUnitsExport, holdList, retryList, settlementInstruction } from '@spoolis/receipt-verifier'

const continueWith = acceptedUnitsExport(receipt)
const retry = retryList(receipt)
const hold = holdList(receipt)
const settlement = settlementInstruction(receipt, statusSource)

Continue from accepted units, retry rejected units, and hold uncertain units for escalation. settlementInstruction passes through the receipt's earned decimal string and refuses demo receipts or a provided status that is not active. The consumer owns action and payment policy.

Additive fields within @1

  • payment_binding (optional, added 2026-09-03): { "kind": "x402_payment_identifier", "id": "<16-128 chars, [A-Za-z0-9_-]>" }. Binds the receipt to the buyer-minted x402 payment-identifier it was purchased under; the seller copies the id it actually charged against into the signed payload. A binding-aware consumer verifies the signature first, then matches payment_binding.id against the id it paid with; consumers that do not read the field see no change. Golden vectors: payment-binding.json, payment-binding-tampered.json.

Schema compatibility policy

The receipt schema (spoolis/outcome-receipt@1) follows these rules so consumers can wire several systems to an Outcome without fearing shape changes:

  • Additive-only within @1: existing field names, types, and semantics do not change; new OPTIONAL fields may be added.
  • Verifiers ignore unknown fields; never fail on a field you do not recognize.
  • The stable core is: schema, id, environment, agreement, verification, evidence_root, condition_results row shape, result, units counts and earning_rule, amounts, actors, issued_at, nonce, algorithm, signing_key_id, signature, and supersedes.
  • Every schema addition ships in the same change as new golden vectors in the public corpus, and this package's README documents the addition.
  • A breaking change means a new schema identifier (@2), a documented migration note, and a support window in which this verifier accepts both.

Trust material

Spoolis publishes receipt trust material at https://spoolis.com/.well-known/spoolis-keys.json. Pin the receipt entries you accept and pass that array as trustSet for offline verification. fetchTrustSet(url, environment) is an online convenience that returns only the selected environment's receipt trust entries. The environment defaults to production; pass demo explicitly for sandbox receipts. Fetching at verification time is not required or preferred for a pinned deployment.

Offline verification and online status

Signature validity is offline and permanent when a verifier has the receipt and a pinned trust set. It does not depend on Spoolis uptime. Online status is optional:

import { fetchReceiptStatus, verifyOutcomeReceipt } from '@spoolis/receipt-verifier'

const offline = await verifyOutcomeReceipt(receipt, { trustSet })
if (!offline.valid) throw new Error(offline.reasons.join(', '))

const statusSource = await fetchReceiptStatus('https://spoolis.com', receipt.id)
const result = await verifyOutcomeReceipt(receipt, { trustSet, statusSource })

GET /api/receipts/{id}/status currently returns active for a stored receipt. The response reserves corrected, revoked, advisory entries, and superseding_receipt_id for future workflows. Without a status source, verification stays offline-only and includes the status_source_unavailable advisory. A receipt can remain cryptographically valid even if a future online status marks it superseded.

A later resolution never mutates the original receipt. It produces a new signed Outcome that supersedes it. The supersedes block identifies the original receipt and may use dispute_resolution when agreed dispute handling produced the successor.

Reasons

| Reason | Meaning | | --- | --- | | unknown_schema | The receipt schema version is not supported. | | environment_mismatch | The receipt environment does not match the requested environment. | | invalid_evidence_root | The evidence root is not a lowercase SHA-256 digest. | | invalid_run_digest | The verification run digest is malformed. | | invalid_agreement_hash | The agreement hash is malformed. | | receipt_id_mismatch | The content-derived receipt ID does not match the payload. | | earned_amount_mismatch | Per-unit arithmetic does not equal the earned amount. | | aggregation_inconsistent | A recognized aggregation policy does not produce the stated result. | | corrected | The online status source marks this receipt corrected. | | revoked | The online status source marks this receipt revoked. | | untrusted_signing_key | No recognized, non-revoked trust entry matches the signing key. | | signing_key_id_mismatch | The trust entry public key does not hash to the signing key ID. | | invalid_signature | The Ed25519 signature does not verify. | | webcrypto_unavailable | The runtime has no WebCrypto (globalThis.crypto.subtle). Use Node 20 or later, or supply a WebCrypto polyfill. | | malformed_receipt | A required structure or value is malformed. |

Advisories

| Advisory | Meaning | | --- | --- | | unknown_aggregation_policy | The verifier does not know how to recompute this policy. | | past_execute_by | The optional execution deadline has passed. This does not expire the receipt. | | status_source_unavailable | No corrected or revoked status source was supplied. | | key_retiring | The receipt uses a trusted key marked as retiring or retired. | | key_outside_validity_window | The receipt's issue time falls before the trusted key's valid_from or after its valid_to. This does not invalidate the receipt. |

Security model

A valid result proves that the receipt payload was signed by a key in the supplied trust set. It proves that the content-derived ID matches the signed content. It checks digest formats, recognized aggregation rules, and unit arithmetic. It also enforces the selected production or demo environment. Verification does not prove that referenced evidence is true or available. It does not resolve actor identities. It does not grant payment authority. It does not prove authorization, capture, settlement, or finality on any payment rail. Consumers must enforce their own authorization and consume-once rules before taking economic action. Pin trust material through a trusted channel and review key rotation policy. Use an online status source when corrected or revoked state matters.

Golden vectors

The conformance corpus is maintained at tests/fixtures/receipt-vectors/ in the Spoolis repository. The package test runs every declared vector against the standalone implementation.