exodus-monetization
v0.2.0
Published
Client DX for Exodus-style monetization: entitlements claim, SAT (signed action tokens), and guarded fetch. Server remains source of truth.
Downloads
129
Maintainers
Readme
exodus-monetization
Thin developer-experience client for Exodus-style monetization APIs:
GET /api/entitlements— claim (+ optional JWT)POST /api/sat— short-lived signed action tokens (SAT), bound to HTTP method + pathX-SATon premium routes that enforce replay-protected actions
Security model: the browser never “grants” premium. The server decides using your DB, Stripe webhooks, and entitlements. This SDK only sequences calls (mint SAT → call API) so apps don’t reimplement that glue.
Problem → solution
| Problem | What this SDK does |
|--------|---------------------|
| Paywalls that only hide buttons | Nothing — you still must enforce on the server |
| Duplicated fetch /api/sat + fetch boilerplate | guardedFetch / protect |
| Guessing SAT feature keys | Use exported SAT_FEATURE_KEYS / SatFeatureKey |
Install
npm install exodus-monetization
# or
pnpm add exodus-monetizationReact hook (optional peer): react >= 18
npm install react exodus-monetizationCore entry does not import React. Use subpath exodus-monetization/react for useMonetizationClient.
Usage
Core client
import {
createMonetizationClient,
isMonetizationError,
validateFeatureKey,
SAT_FEATURE_KEYS,
} from "exodus-monetization";
const m = createMonetizationClient({
baseUrl: "", // same-origin; or "https://your-app.com"
entitlementsCacheTtlMs: 12_000,
/** After “fresh” TTL: return last snapshot immediately + refresh in background (stale-while-revalidate). */
entitlementsStaleAfterFreshMs: 60_000,
onEvent: (e) => {
/* e.type: sat_mint_failed | sat_mint_retried | sat_action_retry | sat_action_blocked */
console.debug("[exodus]", e);
},
});
const { claim, token } = await m.getEntitlements();Retries (DX only, server decides access):
- Mint SAT : une nouvelle tentative si erreur réseau, 5xx,
sat_store_unavailable, ou corpssat_invalid/sat_expired(transient). guardedFetch: si la réponse métier est 403 avecsat_invalidousat_expired, un remint + une nouvelle requête. Pas de retry sursat_replay_or_expired/sat_feature_forbidden.
validateFeatureKey(x) : lève MonetizationError si la clé n’est pas dans SAT_FEATURE_KEYS (aligné sur ton POST /api/sat).
guardedFetch
Posts to /api/sat when feature is set, then adds X-SAT:
const res = await m.guardedFetch("/api/messages", {
method: "POST",
feature: "chat.send",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content: "Hello" }),
});SAT feature keys must match your server whitelist (see SAT_FEATURE_KEYS).
protect
When you want full control of the second fetch:
await m.protect("demo.secure_read", "GET", "/api/demo/secure-read", async ({ headers }) =>
fetch("/api/demo/secure-read", { credentials: "include", headers }),
);React
"use client";
import { useMonetizationClient } from "exodus-monetization/react";
const m = useMonetizationClient({ baseUrl: "" });Errors
Failed SAT mint throws MonetizationError (code, status, body). Use isMonetizationError(e) in catch.
Monorepo (this repo)
pnpm install
pnpm run build:pkg # avant le premier `next dev` ou après changement SDK
pnpm devL’app importe exodus-monetization (workspace) ou les shims @/sdk/monetization/*.
Publishing (maintainers)
cd packages/monetization
pnpm install
pnpm run build
npm publish --access publicfiles field publishes dist/ only (see npm pack --dry-run).
License
MIT
