@paysmith/contracts
v0.0.1
Published
Paysmith contract foundation: zod schemas for the wire contracts, prefixed ids, decimal-string money, RFC 8785-style canonical JSON, Ed25519 event signing, and idempotency primitives.
Maintainers
Readme
@paysmith/contracts
Shared wire contracts for Paysmith, an embedded-finance sandbox that gives any app a believable
$1 paywall/unlock demo. This package defines the vocabulary every other Paysmith package and every
generated app speaks: zod schemas for sandbox activation, payment intents, the signed
paysmith.event/v1 envelope, receipts and entitlements; decimal-string Money (never floats);
prefixed identifiers; RFC 8785-style canonical JSON with sha256 digests; Ed25519 event signing; and
the idempotency primitives every write is scoped by. Zero runtime dependencies beyond zod and
Node's built-in node:crypto.
All amounts this contract describes are sandbox money: fake, deterministic, and scoped to a
72-hour sandbox lifetime. SANDBOX_ENVIRONMENT is the only environment Phase 0 mints resources in.
Install
pnpm add @paysmith/contracts
# or
npm i @paysmith/contractsQuick start
Validate a signed event envelope
import { EventEnvelopeSchema } from "@paysmith/contracts";
const event = EventEnvelopeSchema.parse({
schema: "paysmith.event/v1",
event_id: "evt_01J9ZAW4QK4M6VXNTS0P6ZC9G7",
type: "payment.succeeded",
environment: "sandbox",
resource: { type: "payment_intent", id: "pi_sbx_01J9ZAW4QK4M6VXNTS0P6ZC9G7" },
sequence: 1,
occurred_at: "2026-08-08T12:00:00Z",
data: {
amount: { value: "1.00", currency: "USD" },
product_reference: "paysmith-demo-lifetime-access",
},
});
// event.type: "payment.succeeded" | "payment.failed" | "payment.refunded"Canonical JSON + digest
import { canonicalJson, canonicalDigest, sha256Digest } from "@paysmith/contracts";
canonicalJson({ b: 1, a: "x" }); // '{"a":"x","b":1}' — sorted keys, no whitespace
canonicalDigest({ b: 1, a: "x" }); // "sha256:<hex>" over the canonical bytes
sha256Digest("raw bytes or a string"); // "sha256:<hex>"This is the exact byte-for-byte serialization events are signed over: two independent
implementations of canonicalJson must agree, which is what makes signCanonical / verifyCanonical
and signEvent / verifyEventSignature trustworthy across languages.
Money helpers (decimal strings, never floats)
import { fromCents, toCents, addMoney, toDisplayBalance } from "@paysmith/contracts";
const price = fromCents(100); // { value: "1.00", currency: "USD" }
toCents(price); // 100
addMoney(price, fromCents(50)); // { value: "1.50", currency: "USD" }
toDisplayBalance({ value: "100.00", currency: "USD" }); // { amount: "100.00", currency: "USD" }Arithmetic always happens in integer minor units internally, so no IEEE-754 rounding can reach a ledger posting.
Sign and verify a webhook delivery
import { generateSigningKeyPair, signEvent, verifyEventSignature } from "@paysmith/contracts";
const { keyId, privateKeyPem, publicKeyPem } = generateSigningKeyPair();
const rawBody = JSON.stringify(event);
const timestamp = Math.floor(Date.now() / 1000);
const signature = signEvent({
rawBody,
environment: "sandbox",
sandboxId: "sbx_01J9ZAW4QK4M6VXNTS0P6ZC9G7",
timestamp,
keyId,
privateKeyPem,
});
verifyEventSignature({
rawBody,
environment: "sandbox",
sandboxId: "sbx_01J9ZAW4QK4M6VXNTS0P6ZC9G7",
timestamp,
keyId,
signature,
publicKeyPem,
}); // { valid: true }@paysmith/webhook wraps this together with header parsing for the receiving side — use that
package instead of hand-rolling verification in a webhook handler.
API
- Environment —
ENVIRONMENTS,SANDBOX_ENVIRONMENT,isEnvironment,isSandbox. - Ids —
newId,isId,assertId,ID_PREFIXES(sbx_,pi_/pi_sbx_,evt_,rcp_,ent_,ins_,rb_,act_,trg_), plus per-resource minters:newSandboxId,newPaymentIntentId,newEventId,newReceiptId,newEntitlementId,newInstallationId,newRepositoryBindingId,newActivationId,newTriggerId. - Money —
Money,DisplayBalance,isValidMoneyValue,isMoney,assertMoney,toCents,fromCents,normalizeMoney,addMoney,subtractMoney,negateMoney,compareMoney,moneyEquals,zeroMoney,toDisplayBalance,fromDisplayBalance. - Canonicalization —
canonicalJson,canonicalBytes,sha256Digest,canonicalDigest,isSha256Digest. - Signing —
generateSigningKeyPair,signCanonical,verifyCanonical,buildEventSigningMaterial,signEvent,verifyEventSignature,toUnixSeconds,DEFAULT_TIMESTAMP_TOLERANCE_SECONDS(300s), and the header name constants (SIGNATURE_HEADER,SIGNATURE_TIMESTAMP_HEADER,SIGNATURE_KEY_ID_HEADER, etc.). - Idempotency —
IDEMPOTENCY_KEY_HEADER,normalizedRequestDigest,idempotencyScopeKey,classifyIdempotency("fresh" | "replay" | "conflict"),isIdempotencyKey. - Schemas —
AnonymousSandboxRequestSchema/AnonymousSandboxResponseSchema,CreatePaymentIntentRequestSchema/CreatePaymentIntentResponseSchema,ConfirmSandboxOutcomeSchema,EventEnvelopeSchema,ReceiptSchema,EntitlementSchema,TriggerEnvelopeSchema,AdapterManifestSchema, plus shared primitives (MoneySchema,IsoTimestampSchema,prefixedIdSchema, …). Every schema has an inferredz.infertype of the same name without theSchemasuffix. - JSON Schema —
buildJsonSchemaDocuments()projects the zod schemas to JSON Schema for non-TypeScript consumers (./schemas-json/*.jsonis also published as static files). - Errors —
ContractError(base class,.codeis one ofCONTRACT_ERROR_CODES),isContractError, and typed subclasses:InvalidMoneyError,CurrencyMismatchError,MoneyOverflowError,InvalidIdError,CanonicalizationError,InvalidSigningInputError,IdempotencyConflictError.
How it fits
@paysmith/contracts is the shared vocabulary behind every step of the demo: a product becomes
a payment intent (CreatePaymentIntentRequestSchema), which resolves into a signed event
(EventEnvelopeSchema, verified by @paysmith/webhook), which an app turns into an entitlement
(EntitlementSchema) and can look up as a receipt (ReceiptSchema).
License
MIT
