Brian Booms · booms rewards · protocol

BSP — Booms Protocol, Draft 0.1

Status: DRAFT — not a standard. Standard status requires L4: two

implementations that never shared a vendor stack completing

issue → redeem → settle → reverse against each other. Until then this is a

working draft with a working reference implementation.

Published 2026-09-12. Revised 2026-09-12 — a clean-room second

implementation was built from this text alone (37/37 checks passed) and found

18 ambiguities; every one is pinned below. Supersedes UALP Draft v0.1

(2026-09-12), which remains frozen for history at /.well-known/ualp.json.

One-sentence definition

BSP is the shared contract that lets any issuer create a portable,

machine-readable dollar reward and lets any wallet, merchant, or agent apply,

attribute, and settle it without sharing a vendor.

BSP is an open, wallet-neutral protocol for issuing, discovering, quoting,

authorizing, redeeming, attributing, settling, and reversing portable rewards.

It is not a loyalty program, not a required wallet, and not a cryptocurrency.

Booms Rewards is one network that implements BSP. Anyone else can implement

BSP without permission.

What BSP standardizes

Shared meaning for a reward across merchants, wallets, customers, affiliates,

and agents:

Business rules stay with the issuer (earn rates, eligible SKUs, stacking,

blackout dates). BSP standardizes the object and the handshake, not every

program's marketing rules.

What BSP is not

Design principles

  1. Protocol ≠ company. Spec is public. Competing implementations expected.
  2. Wallet neutrality. The reward is not owned by a Booms wallet. Email,

pass, bank app, retailer account, browser, agent memory, QR, or crypto

wallet can hold a reference to it.

  1. Fiat-denominated by default. Customer-facing value is a currency amount

(USD 8.40), not a speculative token. Crypto may be a settlement rail, not

the unit.

  1. Issuer sovereignty. An issuer can restrict where a reward redeems.

Portability is allowed, not forced.

  1. Agent-native. A machine must be able to

discover → compare effective price → purchase → attribute → redeem → settle

with no human dashboard.

  1. Reversible commerce. Returns, fraud, partial capture, and expiration

are first-class, not afterthoughts.

  1. No lock-in in the spec. Required fields stay small. Extensions are

namespaced (x-<vendor>-<field>).

Identities

Identities are URIs. An issuer SHOULD use its HTTPS origin

(https://merchant.example); did:web: is MAY. A blockchain address or

account is never required. Holders are identified by whatever the wallet

presents: an HTTPS account URI, an email + claim code, or a wallet address —

the protocol does not mandate one.

Core objects

Reward — the portable unit

{
  "id": "urn:bsp:reward:2f9c7a41",
  "type": "bsp.Reward",
  "spec": "bsp/0.1",
  "amount": { "value": "8.40", "currency": "USD" },
  "state": "issued",
  "issuer": "https://merchant.example",
  "holder": "https://customer.example/acct/42",
  "redeemable": true,
  "constraints": {
    "merchants": ["https://merchant.example"],
    "min_basket": "25.00",
    "expires_at": "2026-12-31T23:59:59Z"
  },
  "attribution": {
    "referrer": "https://agent.example",
    "campaign": "fall-26"
  },
  "issued_at": "2026-09-12T07:21:00Z",
  "proof": { "type": "JWS", "jws": "eyJhbGciOiJFZERTQSIs..." }
}

amount.value is a decimal string (never float). state is one of

issued | authorized | redeemed | partially_redeemed | reversed | expired.

constraints.merchants, when absent, means the reward is portable to any

merchant that accepts BSP; when present, it lists exactly where it redeems

(issuer sovereignty). proof is defined in § Signature profile.

Quote

{
  "type": "bsp.Quote",
  "spec": "bsp/0.1",
  "id": "urn:bsp:quote:9d11",
  "reward": "urn:bsp:reward:2f9c7a41",
  "merchant": "https://merchant.example",
  "basket_fingerprint": "sha256:4f2c…",
  "applicable_amount": { "value": "8.40", "currency": "USD" },
  "amount_due_after": { "value": "21.60", "currency": "USD" },
  "expires_at": "2026-09-12T07:26:00Z",
  "proof": { "type": "JWS", "jws": "…" }
}

basket_fingerprint is sha256: + hex of the JCS-canonicalized basket

object (§ Quote binding). expires_at defaults to 300 seconds after issue;

a merchant MAY advertise quote_ttl_seconds in its /.well-known/bsp.json.

Authorization

{
  "type": "bsp.Authorization",
  "spec": "bsp/0.1",
  "id": "urn:bsp:auth:77ab",
  "quote": "urn:bsp:quote:9d11",
  "holder": "https://customer.example/acct/42",
  "consent": "urn:bsp:consent:51cd",
  "authorized_at": "2026-09-12T07:23:00Z",
  "expires_at": "2026-09-12T07:26:00Z",
  "proof": { "type": "JWS", "jws": "…" }
}

expires_at is copied from the quote. The authorization holds the quoted

amount against the reward until expiry so concurrent checkouts serialize on

the hold.

Consent — the holder's mandate

{
  "type": "bsp.Consent",
  "spec": "bsp/0.1",
  "id": "urn:bsp:consent:51cd",
  "holder": "https://customer.example/acct/42",
  "reward": "urn:bsp:reward:2f9c7a41",
  "merchants": ["https://merchant.example"],
  "scope": ["redeem"],
  "single_use": false,
  "issued_at": "2026-09-12T07:22:00Z",
  "expires_at": "2026-09-12T08:22:00Z",
  "proof": { "type": "JWS", "jws": "…" }
}

Consent is signed by the holder (not the merchant, not the agent).

merchants lists the merchant scopes the consent covers; scope lists the

actions (redeem, and in future authorize). single_use: true means the

consent is consumed by its first authorization. Verification is all of:

holder matches, reward matches, merchant in scope, action in scope, not

expired, signature valid, issued_at within ±5 minutes. **No valid consent,

no agent spend — no exceptions.**

Redemption

{
  "type": "bsp.Redemption",
  "spec": "bsp/0.1",
  "id": "urn:bsp:redemption:31ef",
  "authorization": "urn:bsp:auth:77ab",
  "quote": "urn:bsp:quote:9d11",
  "order_ref": "order-8814",
  "captured_amount": { "value": "8.40", "currency": "USD" },
  "redeemed_at": "2026-09-12T07:24:00Z",
  "proof": { "type": "JWS", "jws": "…" }
}

Signed by the capturing merchant. A partial redemption (captured_amount

less than the authorized amount) issues a child Reward for the remainder.

Settlement

Signed by the settling party. Records obligations per a

CommissionAllocation; it does not itself move money (§ Settlement).

Reversal

{
  "type": "bsp.Reversal",
  "spec": "bsp/0.1",
  "id": "urn:bsp:reversal:08bc",
  "redemption": "urn:bsp:redemption:31ef",
  "reason": "refund",
  "amount": { "value": "8.40", "currency": "USD" },
  "reversed_at": "2026-09-12T09:10:00Z",
  "proof": { "type": "JWS", "jws": "…" }
}

reason is one of cancel | refund | fraud | expired. Signed by the

reversing merchant. A full reversal of a full redemption restores the

original reward to issued; any other shape issues a fresh child reward for

the refunded amount and marks the redemption partially_reversed. A second

reversal of the same redemption returns 409 already_reversed.

CommissionAllocation — economics in the protocol, rates not frozen

{
  "type": "bsp.CommissionAllocation",
  "spec": "bsp/0.1",
  "gross": { "value": "10.00", "currency": "USD" },
  "allocations": [
    { "party": "holder_or_creator", "share": "0.75" },
    { "party": "network", "share": "0.25" }
  ]
}

BSP defines HOW value is split, not a mandatory 75/25. Shares are decimal

strings summing to exactly "1.00" (else 422 allocation_invalid). An

issuer MAY use 90/10 or 100/0 on rewards it fully funds. A network (e.g.

Booms Rewards) MAY require a fee to participate in its discovery/settlement

graph. That fee is network policy, never a protocol constant.

Event (webhooks)

{
  "type": "bsp.Event",
  "spec": "bsp/0.1",
  "id": "urn:bsp:event:44d2",
  "kind": "reward.redeemed",
  "object": { "type": "bsp.Redemption", "…": "…", "proof": { "…": "…" } },
  "occurred_at": "2026-09-12T07:24:00Z",
  "proof": { "type": "JWS", "jws": "…" }
}

kind is one of `reward.issued | reward.redeemed | reward.reversed |

settlement.report. The embedded object` carries its own proof; the event

envelope is signed by the emitter.

Lifecycle and endpoints

StepMeaningEndpoint
IssueIssuer creates a signed Reward; delivers a reference (email, pass, QR, API)POST /bsp/v1/earn
DiscoverWallet/agent asks a merchant: "does this holder have applicable rewards?"POST /bsp/v1/discover
QuoteMerchant returns applicable amount for this basket, after constraintsPOST /bsp/v1/quote
AuthorizeHolder (or agent with consent) approves applying a specific quotePOST /bsp/v1/authorize
RedeemMerchant captures the reward against an order; state → redeemed / partialPOST /bsp/v1/redeem
SettleObligations recorded: merchant subsidy, holder credit, affiliate/agent cut, network feePOST /bsp/v1/settle
ReverseCancel, refund, fraud, or expiration rolls state and settlement backPOST /bsp/v1/reverse

Read endpoints for audit and reconciliation (merchant-authenticated):

GET /bsp/v1/rewards/:id, GET /bsp/v1/redemptions/:id,

GET /bsp/v1/settlements/:id, GET /bsp/v1/reversals/:id.

Plus GET /bsp/v1/balance?holder= and the capability document

GET /.well-known/bsp.json, which advertises supported operations,

currencies, jwks_uri, quote_ttl_seconds, and webhook callback URLs.

"Accepts BSP" is a capability claim, not a brand partnership.

All mutating endpoints are HTTPS POST, return JSON, and use the error

catalog (§ Errors). Amounts are always {value, currency} objects.

Request envelopes (normative)

The objects above are what the protocol *returns and signs*. These are the

request bodies each endpoint *takes*:

basket shape:

{
  "items": [
    { "sku": "dl-0042", "name": "Deep Focus Vol. 1",
      "unit_price": { "value": "29.00", "currency": "USD" }, "quantity": 1 }
  ],
  "totals": {
    "subtotal": { "value": "29.00", "currency": "USD" },
    "tax": { "value": "0.00", "currency": "USD" },
    "shipping": { "value": "0.00", "currency": "USD" },
    "total": { "value": "29.00", "currency": "USD" }
  },
  "merchant": "https://merchant.example",
  "window": { "from": "2026-09-12T07:20:00Z", "to": "2026-09-12T07:35:00Z" }
}

requested_at (RFC 3339 UTC) is REQUIRED on authorize and redeem and

governed by the ±5-minute skew rule; it is optional elsewhere.

Signature profile (interoperability-critical)

Two implementations interoperate only if signatures verify identically.

This profile is REQUIRED:

at 0.1.

of the object without its proof member. Sign proof-less bytes;

attach proof: {"type": "JWS", "jws": "<compact>"} afterwards.

JWKS; the issuer's /.well-known/bsp.json advertises jwks_uri. kid

MUST be HTTPS in production; test environments MAY use HTTP with a

warning.

Redemption, Settlement, Reversal, and Event object. The money

moves on signed records — including the capture and the undo.

transition (issued → authorized → redeemed) would invalidate the proof.

The transitioning party MUST re-sign the object with the new state;

verifiers verify the proof against the current object. (A ledger MAY

retain prior signed versions as history; the wire always carries the

current signed state.)

rule governs consent.issued_at and the requested_at on

authorize/redeem. Quote and authorization expires_at are enforced

strictly — no skew.

Idempotency, exactly-once redemption, quote binding

settle, and reverse. Missing → 400 missing_idempotency_key. The key

namespace is per-endpoint. Replaying a key returns the original response

byte-identical with header X-Idempotent-Replayed: true, regardless of

any difference in the replayed body. Retries never double-post: the same

earn key returns the same reward; the same settle key never double-records

obligations.

per reward id. A second redeem with a *different* key against an already

redeemed reward returns 409 already_redeemed. Double-spend is a ledger

property, not a hope.

JCS-canonicalized basket object above (items, totals, merchant,

window with {from, to} RFC 3339). Redeem presents the quote id; the

merchant MUST recompute the fingerprint for the presented basket and

reject mismatch with 422 quote_basket_mismatch. Agents cannot shop a

quote and redeem against a different cart.

exists, unexpired, and matches → consent valid → exactly-once (409) →

quote exists and unexpired → basket fingerprint match (422) → amount

checks. When several failures apply, the earlier check's error wins.

authorization holds the quoted amount against the reward until its

expires_at, so concurrent checkouts serialize on the hold. A second

authorize against a held reward returns 409 already_authorized.

Balance and discover semantics

not spendable; a partially_redeemed parent carries no remainder — the

child reward does.

constraints.merchants means portable anywhere BSP is accepted.

min_basket (failure → 422 basket_below_minimum).

Errors

{"error": "<machine_code>", "message": "…"}. Agents branch on codes:

CodeHTTPMeaning
invalid_request400malformed body / missing field
missing_idempotency_key400required header absent
invalid_signature401JWS fails verification
consent_required403agent redeem without consent
consent_invalid403consent signature/scope/holder/reward mismatch
consent_expired403consent past expires_at
scope_denied403merchant outside consent scope
unknown_reward / unknown_quote / unknown_authorization / unknown_redemption404id not found
already_redeemed409exactly-once violated
already_reversed409reversal replayed with new key
already_authorized409reward already held
duplicate_order_ref409order_ref seen with different envelope
reward_expired410reward past constraints.expires_at
quote_expired422quote past expires_at
authorization_expired422authorization past expires_at
quote_basket_mismatch422fingerprint recompute differs
allocation_invalid422shares don't sum to 1.00
basket_below_minimum422under min_basket
amount_exceeds_applicable422capture above quoted amount
merchant_halted503kill switch engaged

Settlement

Settlement records obligations; it does not itself move money.

Netting is per period per counterparty pair; adapters execute the net:

card, ACH, wallet balance, or stablecoin (e.g. USDC). The Reward stays

denominated in currency either way. Pilot profile (0.1): obligations are

recorded and attested; no prefunded reserves required. Reserves are a stated

future, not a silent assumption — "skip reserves and you don't have a

network; you have a promise" applies at network scale, and the spec does not

pretend otherwise.

Webhooks

Issuers and networks MAY subscribe: reward.issued, reward.redeemed,

reward.reversed, settlement.report. Deliveries use the bsp.Event

envelope above, are retried with backoff, and receivers dedupe on event id.

Conformance levels

vendor stack complete issue → redeem → settle → reverse.

L4 is the line between "a product with an API" and "a standard."

Worked example — happy path

  1. Issuer calls POST /bsp/v1/earn with `{amount: {value:"8.40",

currency:"USD"}, holder: "https://customer.example/acct/42"}` and an

Idempotency-Key; gets the signed Reward; emails the holder a QR

referencing urn:bsp:reward:2f9c7a41.

  1. Agent calls POST /bsp/v1/discover with holder + merchant; merchant

returns the reward as applicable.

  1. Agent calls POST /bsp/v1/quote with {reward, merchant, basket};

merchant returns the Quote with basket_fingerprint and

applicable_amount 8.40; the agent compares effective price

(30.00 − 8.40 = 21.60) against other merchants.

  1. Holder's agent presents the signed Consent; POST /bsp/v1/authorize

with {quote, consent, requested_at} returns the Authorization, holding

8.40 until the quote's expires_at.

  1. Agent completes purchase; POST /bsp/v1/redeem with `{authorization,

quote, order_ref, basket, requested_at}` captures 8.40. Reward →

redeemed (re-signed by the merchant).

  1. POST /bsp/v1/settle with {redemption, allocation} records

obligations per the CommissionAllocation.

Worked example — refund

  1. Order refunded. Merchant calls POST /bsp/v1/reverse with

{redemption: "urn:bsp:redemption:…", reason: "refund"} and an

Idempotency-Key.

  1. Full reversal of a full redemption: the original reward returns to

issued (re-signed). Partial amounts: a child reward is issued for the

refunded amount and the redemption is marked partially_reversed. A

linked reversing settlement entry adjusts obligations. No dangling

credit; key replay returns the original reversal byte-identical.

Honest status (2026-09-12)

text alone, same party — not L4): 37-check lifecycle + adversarial suite,

37/37, including double-spend, basket mismatch, idempotency, consent, and

75/25 + 90/10 allocations. Its 18 findings are folded into this revision.

settlement reserves (do not exist), cross-merchant settlement (specified,

not operating).

← Back to the protocol overview