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:
- what the reward is
- who issued it
- who may hold or spend it
- where it can be discovered and quoted
- how it is authorized and applied at checkout
- how value is attributed and settled
- how it is expired, refunded, or reversed
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
- A points currency with a floating price
- A requirement to store value in one company's ledger
- A replacement for card networks
- A guarantee that every reward works at every merchant
- Governance-by-tweet
Design principles
- Protocol ≠ company. Spec is public. Competing implementations expected.
- 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.
- 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.
- Issuer sovereignty. An issuer can restrict where a reward redeems.
Portability is allowed, not forced.
- Agent-native. A machine must be able to
discover → compare effective price → purchase → attribute → redeem → settle
with no human dashboard.
- Reversible commerce. Returns, fraud, partial capture, and expiration
are first-class, not afterthoughts.
- 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
| Step | Meaning | Endpoint |
|---|---|---|
| Issue | Issuer creates a signed Reward; delivers a reference (email, pass, QR, API) | POST /bsp/v1/earn |
| Discover | Wallet/agent asks a merchant: "does this holder have applicable rewards?" | POST /bsp/v1/discover |
| Quote | Merchant returns applicable amount for this basket, after constraints | POST /bsp/v1/quote |
| Authorize | Holder (or agent with consent) approves applying a specific quote | POST /bsp/v1/authorize |
| Redeem | Merchant captures the reward against an order; state → redeemed / partial | POST /bsp/v1/redeem |
| Settle | Obligations recorded: merchant subsidy, holder credit, affiliate/agent cut, network fee | POST /bsp/v1/settle |
| Reverse | Cancel, refund, fraud, or expiration rolls state and settlement back | POST /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*:
POST /bsp/v1/earn→{amount, holder, constraints?, attribution?}→201RewardPOST /bsp/v1/discover→{holder, merchant}→200 {rewards: [Reward]}GET /bsp/v1/balance?holder=→200 {holder, balances: [{currency, value}], reward_ids: [...]}POST /bsp/v1/quote→{reward, merchant, basket}→200QuotePOST /bsp/v1/authorize→{quote, consent, requested_at}→200AuthorizationPOST /bsp/v1/redeem→{authorization, quote, order_ref, basket, captured_amount?, requested_at}→200RedemptionPOST /bsp/v1/settle→{redemption, allocation}→200SettlementPOST /bsp/v1/reverse→{redemption, reason, amount?}→200Reversal
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:
- Serialization: JWS Compact Serialization.
- Algorithms:
EdDSAwith Ed25519 keys SHOULD;ES256MAY. Nothing else
at 0.1.
- Canonicalization: the JWS payload is the JCS (RFC 8785) canonical form
of the object without its proof member. Sign proof-less bytes;
attach proof: {"type": "JWS", "jws": "<compact>"} afterwards.
- Key discovery:
kid(JWS header) is the HTTPS URL of the signer's
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.
- What is signed: every
Reward,Quote,Authorization,Consent,
Redemption, Settlement, Reversal, and Event object. The money
moves on signed records — including the capture and the undo.
- State transitions re-sign:
statelives inside the signed bytes, so a
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.)
- Timestamps: objects carry
*_atin RFC 3339 UTC. The ±5-minute skew
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
Idempotency-Keyheader is REQUIRED on earn, quote, authorize, redeem,
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.
- Exactly-once redemption: the issuer's ledger single-flights redemption
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.
- Quote binding:
basket_fingerprintissha256:+ hex of the
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.
- Redeem check order (normative): idempotency replay → authorization
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 under load: authorize-then-redeem is two-phase; the
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
balancesums rewards in stateissuedonly. Authorized amounts are held,
not spendable; a partially_redeemed parent carries no remainder — the
child reward does.
discoverreturns rewards applicable to the merchant: absent
constraints.merchants means portable anywhere BSP is accepted.
applicable_amount= min(remaining reward amount, basket total), after
min_basket (failure → 422 basket_below_minimum).
Errors
{"error": "<machine_code>", "message": "…"}. Agents branch on codes:
| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | malformed body / missing field |
missing_idempotency_key | 400 | required header absent |
invalid_signature | 401 | JWS fails verification |
consent_required | 403 | agent redeem without consent |
consent_invalid | 403 | consent signature/scope/holder/reward mismatch |
consent_expired | 403 | consent past expires_at |
scope_denied | 403 | merchant outside consent scope |
unknown_reward / unknown_quote / unknown_authorization / unknown_redemption | 404 | id not found |
already_redeemed | 409 | exactly-once violated |
already_reversed | 409 | reversal replayed with new key |
already_authorized | 409 | reward already held |
duplicate_order_ref | 409 | order_ref seen with different envelope |
reward_expired | 410 | reward past constraints.expires_at |
quote_expired | 422 | quote past expires_at |
authorization_expired | 422 | authorization past expires_at |
quote_basket_mismatch | 422 | fingerprint recompute differs |
allocation_invalid | 422 | shares don't sum to 1.00 |
basket_below_minimum | 422 | under min_basket |
amount_exceeds_applicable | 422 | capture above quoted amount |
merchant_halted | 503 | kill 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
- L0 — Readable: parse and display a Reward.
- L1 — Wallet: receive, store, present a Reward reference.
- L2 — Merchant: full checkout handshake + reversals (+ read endpoints).
- L3 — Network: discovery directory, multi-party settlement, attribution.
- L4 — Independent interop: two implementations that never used the same
vendor stack complete issue → redeem → settle → reverse.
L4 is the line between "a product with an API" and "a standard."
Worked example — happy path
- Issuer calls
POST /bsp/v1/earnwith `{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.
- Agent calls
POST /bsp/v1/discoverwith holder + merchant; merchant
returns the reward as applicable.
- Agent calls
POST /bsp/v1/quotewith{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.
- 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.
- Agent completes purchase;
POST /bsp/v1/redeemwith `{authorization,
quote, order_ref, basket, requested_at}` captures 8.40. Reward →
redeemed (re-signed by the merchant).
POST /bsp/v1/settlewith{redemption, allocation}records
obligations per the CommissionAllocation.
Worked example — refund
- Order refunded. Merchant calls
POST /bsp/v1/reversewith
{redemption: "urn:bsp:redemption:…", reason: "refund"} and an
Idempotency-Key.
- 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)
- Draft 0.1 (revised). Not a standard. No L4 implementation pair exists.
- Reference implementation: Booms Rewards — live on the canonical
/bsp/v1/*wire athttps://pay.brianbooms.comsince 2026-09-12 (legacy/api/v1/partner/*tier deployed dark). - Conformance evidence, reference implementation: 159-check suite against the live wire, 159/159 — full lifecycle, exactly-once redemption, basket binding, idempotency on all six mutating endpoints, consent matrix, partial redeem/reversal, read endpoints, error catalog; every signed object cryptographically verified against the served JWKS.
- Live sandbox: open at
https://pay.brianbooms.com/bsp/v1with a public test key (see/.well-known/bsp.json); settlement records obligations and moves no money. - Conformance evidence, clean-room second implementation (built from this
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.
- Wire drift resolved 2026-09-12: the reference implementation now serves
the canonical
/bsp/v1/*paths (159/159 conformance). New implementations MUST use/bsp/v1/*; the legacy/api/v1/partner/*tier is deployed dark.
- Untested: expiry as a live event, authorization under concurrency,
settlement reserves (do not exist), cross-merchant settlement (specified,
not operating).