SDK Product Model Redesign Questionnaire
Questionnaire to inform the redesign of the SolvaPay SDK for the Product-centric backend model. See also the backend Schema Design and Schema Questionnaire.
Context
The SDK is a monorepo with 6 published packages (@solvapay/core, @solvapay/server, @solvapay/auth, @solvapay/react, @solvapay/next, @solvapay/react-supabase) and 5 example apps. Current version: 1.0.0-preview.18. No external users.
The backend is replacing the Agent/MCP Server dual-entity model with a unified Product entity (backend decisions D1-D18). The SDK currently references agent/agentRef in ~40 touchpoints across all packages. PaymentIntent stays as the SDK payment flow concept — there is no separate Payment entity. Each Purchase serves as the billing record for its period.
Pre-launch status: No backward compatibility required. No deprecated aliases. Clean cut on all code. This is the opportunity to eliminate all technical debt.
Current agent/agentRef Touchpoints
@solvapay/server (packages/server/):
PayableOptions.agent,PayableOptions.agentRef— payable configPaywallMetadata.agent— internal paywall metadataPaywallStructuredContent.agent— error response fieldSolvaPayPaywall.resolveAgent()— internal methodSolvaPayPaywall.trackUsage(agentRef)— internal methodSolvaPayPaywall.protect()passesagentRef: agenttocheckLimits()SolvaPayClient.listAgents(),.createAgent(),.deleteAgent(agentRef)SolvaPayClient.listPlans(agentRef),.createPlan({ agentRef })SolvaPayClient.createPaymentIntent({ agentRef }),.processPaymentIntent({ agentRef })SolvaPay.createPaymentIntent({ agentRef }),.processPaymentIntent({ agentRef }),.checkLimits({ agentRef }),.trackUsage({ agentRef }),.createCheckoutSession({ agentRef })createPaymentIntentCore(request, { agentRef }),processPaymentIntentCore(request, { agentRef }),createCheckoutSessionCore(request, { agentRef })listPlansCore()readsagentReffrom query params, returns{ plans, agentRef }- Factory
payable()resolves agent fromoptions.agentRef || options.agent || env || package.json - HTTP/Next.js error responses include
agentfield
@solvapay/react (packages/react/):
PurchaseInfo.agentNameuseCheckout(planRef, agentRef?)PaymentFormProps.agentRefPlanSelectorProps.agentRefUsePlansOptions.agentRefSolvaPayContextValue.createPaymentIntent({ agentRef }),.processPaymentIntent({ agentRef })SolvaPayProviderProps.processPaymentIntent({ agentRef })
@solvapay/next (packages/next/):
createPaymentIntent(request, { agentRef }),processPaymentIntent(request, { agentRef }),createCheckoutSession(request, { agentRef })listPlansreturn includesagentRefPurchaseCheckResult.purchases[].agentName
Generated types (packages/server/src/types/generated.ts):
CheckLimitRequest.agentRefORCheckLimitRequest.mcpServerRefbranching/v1/sdk/agentspaths,AgentSdkController_*operation namesCreateAgentRequest,UpdateAgentRequestschemasPurchaseResponse.agentRef,.agentNameUsageEvent.agentRef
Decisions Made
D1. Replace all agent/agentRef with product/productRef
Clean rename across all packages. No deprecated aliases. product is the short form for config, productRef is the explicit reference identifier.
What changes:
PayableOptions.agent->PayableOptions.productPayableOptions.agentRef->PayableOptions.productRefSolvaPay.payable({ agent: 'agt_xxx' })->SolvaPay.payable({ product: 'prd_xxx' })- All
SolvaPayClientmethods:agentRefparam ->productRef - All
SolvaPayinterface methods:agentRefparam ->productRef - All Core helpers:
agentRefparam ->productRef - All React hooks/components:
agentRefprop ->productRef - All Next.js helpers:
body.agentRef->body.productRef - Environment variable:
SOLVAPAY_AGENT->SOLVAPAY_PRODUCT
Rationale: Product is the backend's commercial entity (D1). The SDK should use the same term. No users to migrate. Clean cut eliminates confusion between agent (future operational concept) and product (what you sell).
D2. Reference prefix is prd_
Products use prd_ prefix (e.g. prd_8XK2M4), matching the backend schema design. Purchases use pur_.
Rationale: Consistent with backend. No reason to diverge.
D3. Eliminate agentRef/mcpServerRef branching in CheckLimitRequest
The current generated CheckLimitRequest has agentRef?: string OR mcpServerRef?: string. The new version will have a single productRef: string (required).
What changes:
checkLimits({ customerRef, agentRef })->checkLimits({ customerRef, productRef })- No more optional fields or branching logic
Rationale: Backend unifies behind productId (backend D1). The SDK's limit check becomes simpler — one required field instead of two optional mutually exclusive fields.
D4. No backward compatibility. No deprecated aliases. Clean cut.
Pre-launch. No external users. No agent compatibility layer. No shimming. Remove all Agent CRUD from the SDK.
Rationale: Confirmed pre-launch status. Backend D7 applies to SDK as well. Technical debt elimination.
D5. createPaymentIntent and processPaymentIntent keep their existing names
PaymentIntent stays as the SDK's payment flow concept. No method renames. Only the request parameters change to use productRef instead of agentRef/mcpServerRef.
What changes:
createPaymentIntent({ agentRef, ... })->createPaymentIntent({ productRef, ... })processPaymentIntent({ agentRef, ... })->processPaymentIntent({ productRef, ... })createPaymentIntentCore(req, { agentRef })->createPaymentIntentCore(req, { productRef })processPaymentIntentCore(req, { agentRef })->processPaymentIntentCore(req, { productRef })- Next.js helpers: same param renames (
agentRef->productRef) - React context/hooks: same param renames (
agentRef->productRef)
What stays the same:
- All method names:
createPaymentIntent,processPaymentIntent,createPaymentIntentCore,processPaymentIntentCore - Return shape from
createPaymentIntent:{ id, clientSecret, publishableKey, accountId }— the client still usesclientSecretwith Stripe.js - The payment flow: create payment intent -> confirm on client with Stripe.js -> process on server -> purchase activated
Backend route mapping:
| SDK method | Backend route |
|---|---|
createPaymentIntent() | POST /v1/sdk/payment-intents |
processPaymentIntent() | POST /v1/sdk/payment-intents/:id/process |
Rationale: PaymentIntent accurately describes what the SDK creates (a Stripe PaymentIntent wrapper). No need to rename — the concept is correct. Only the entity reference parameters change from agent to product. There is no separate Payment entity in the SDK; each Purchase serves as the billing record for its period.
D6. Stay in 1.0.0-preview.X range. No major version bump needed.
Bump to 1.0.0-preview.19 (or next available). Pre-1.0-stable means breaking changes are expected. No semver major needed.
Rationale: Preview range signals instability. Breaking changes are normal. No users to communicate migration to.
D7. No Payment entity exposed in the SDK
There is no Payment entity in the SDK. No listPayments method. No PaymentInfo type. Billing history is accessed via listPurchases filtered by product — each Purchase IS a billing record for its period.
What this means:
- No
SolvaPayClient.listPayments()— uselistPurchases({ productRef?, customerRef? })instead - No
SolvaPay.listPayments()convenience method - No
usePayments()hook — useusePurchase()which already returnspurchases: PurchaseInfo[] PurchaseInfoincludes billing fields:amount,currency,transactionId,paymentIntentId
Rationale: A per-period Purchase already captures all billing information (amount, currency, status, dates). Exposing a separate Payment entity would duplicate data and add complexity. Integrators query purchases to show billing history — each purchase row IS an invoice/receipt for its period.
D8. SDK has zero awareness of MCP Pay
The SDK is for providers who integrate code. MCP Pay is a no-code product where SolvaPay hosts everything. The SDK should not reference isMcpPay, MCP Pay product types, or any MCP Pay-specific config.
The payable.mcp() adapter stays — it's for providers running their own MCP servers with SDK integration. The adapter name describes the framework (MCP protocol), not the product type.
Rationale: MCP Pay users never touch the SDK. Mixing the concepts would confuse both audiences. The backend uses
isMcpPayas an internal flag (backend D4) — it should not leak into the SDK.
D9. PaywallMetadata and PaywallStructuredContent use product
Internal paywall types rename agent to product:
PaywallMetadata.agent->PaywallMetadata.productPaywallStructuredContent.agent->PaywallStructuredContent.productPaywallErrorstructured content exposesproductnotagent- HTTP/Next.js error responses return
productfield instead ofagent
Rationale: The paywall protects access to a product. The term should match. Error responses seen by integrators should use consistent terminology.
D10. PurchaseInfo updated for long-lived purchase model
The React PurchaseInfo type and Next.js PurchaseCheckResult update to reflect the new model:
Fields renamed:
agentName->productName
Fields added:
productReference: stringplanType: string—'recurring' | 'usage-based' | 'one-time' | 'hybrid'isRecurring: booleannextBillingDate?: stringbillingCycle?: string
Fields added (usage-based plans):
usage?: { used: number, quota: number | null, unit: string, remaining: number | null }
Fields kept:
reference,planName,status,startDate,endDate,cancelledAt,cancellationReason,amount
Rationale: Long-lived purchases carry more state (billing, usage). Integrators need this for UI (show remaining usage, next billing date, plan type badges). Matches backend Purchase schema (backend D9, D17, D18).
D11. Purchase statuses are a union type
SDK exposes all 8 statuses from backend D11 as a TypeScript union, not a free-form string:
type PurchaseStatus =
| 'pending'
| 'active'
| 'trialing'
| 'past_due'
| 'cancelled'
| 'expired'
| 'suspended'
| 'refunded'
Rationale: Type safety for integrators. Matches backend D11 exactly. No additional statuses needed in the SDK.
D12. Rename SolvaPayClient agent methods to product methods
| Current method | New method |
|---|---|
listAgents() | listProducts() |
createAgent(params) | createProduct(params) |
deleteAgent(agentRef) | deleteProduct(productRef) |
listPlans(agentRef) | listPlans(productRef) |
createPlan({ agentRef, ... }) | createPlan({ productRef, ... }) |
deletePlan(agentRef, planRef) | deletePlan(productRef, planRef) |
createPaymentIntent({ agentRef, ... }) | createPaymentIntent({ productRef, ... }) |
processPaymentIntent({ agentRef, ... }) | processPaymentIntent({ productRef, ... }) |
checkLimits({ agentRef | mcpServerRef }) | checkLimits({ productRef }) |
trackUsage({ agentRef, ... }) | trackUsage({ productRef, ... }) |
New methods added:
listPurchases({ productRef?, customerRef? })— billing history (each Purchase is a billing record)
Methods removed:
- None removed (agent methods are renamed, not dropped)
Rationale: 1:1 rename. Agent -> Product. No methods lost. One method gained (listPurchases). PaymentIntent methods keep their names — only params change. checkLimits simplified from two optional fields to one required field.
D13. SolvaPay factory interface method updates
All convenience methods on the SolvaPay interface update agentRef -> productRef:
interface SolvaPay {
payable(options?: PayableOptions): PayableFunction
ensureCustomer(customerRef, externalRef?, options?): Promise<string>
createPaymentIntent(params: { productRef: string, planRef: string, customerRef: string, idempotencyKey?: string }): Promise<...>
processPaymentIntent(params: { paymentIntentId: string, productRef: string, customerRef: string, planRef?: string }): Promise<...>
checkLimits(params: { customerRef: string, productRef: string }): Promise<...>
trackUsage(params: { customerRef: string, productRef: string, planRef: string, ... }): Promise<void>
createCustomer(params): Promise<...>
getCustomer(params): Promise<...>
createCheckoutSession(params: { productRef: string, customerRef: string, planRef?: string, returnUrl?: string }): Promise<...>
createCustomerSession(params): Promise<...>
listPurchases(params: { productRef?: string, customerRef?: string }): Promise<...> // NEW — billing history
apiClient: SolvaPayClient
}
Rationale: Mirrors SolvaPayClient renames. PaymentIntent methods keep their names — only params change. listPurchases added for billing history (no separate Payment entity).
D14. Factory payable() auto-resolution changes
Current: options.agentRef || options.agent || process.env.SOLVAPAY_AGENT || packageJsonName || 'default-agent'
New: options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || 'default-product'
Drop package.json name auto-detection. This was a convenience that assumed one product per app. With Product as the explicit entity, require explicit configuration or env var.
Rationale: Auto-detecting product from package.json name is confusing ("why is my product called my-express-app?"). Products have explicit references like
prd_8XK2M4. Require explicit configuration.
D15. Core route helpers rename agentRef -> productRef
| Current function | Param change |
|---|---|
createPaymentIntentCore(req, { planRef, agentRef }) | { planRef, productRef } |
processPaymentIntentCore(req, { paymentIntentId, agentRef }) | { paymentIntentId, productRef } |
createCheckoutSessionCore(req, { agentRef, planRef? }) | { productRef, planRef? } |
listPlansCore(req) — reads agentRef from query | reads productRef from query, returns { plans, productRef } |
Validation messages update: "agentRef is required" -> "productRef is required".
Rationale: Straightforward param rename. No logic changes.
D16. React PurchaseInfo.agentName -> PurchaseInfo.productName
All React types and components:
| Current | New |
|---|---|
PurchaseInfo.agentName | PurchaseInfo.productName |
useCheckout(planRef, agentRef?) | useCheckout(planRef, productRef?) |
PaymentFormProps.agentRef | PaymentFormProps.productRef |
PlanSelectorProps.agentRef | PlanSelectorProps.productRef |
UsePlansOptions.agentRef | UsePlansOptions.productRef |
SolvaPayContextValue.createPaymentIntent({ agentRef? }) | SolvaPayContextValue.createPaymentIntent({ productRef? }) |
SolvaPayContextValue.processPaymentIntent({ agentRef }) | SolvaPayContextValue.processPaymentIntent({ productRef }) |
SolvaPayProviderProps.processPaymentIntent({ agentRef }) | SolvaPayProviderProps.processPaymentIntent({ productRef }) |
Rationale: React types must match server types. Clean rename throughout.
D17. usePurchase hook updates for long-lived purchases
The hook return shape stays structurally similar but uses updated PurchaseInfo (D10):
interface PurchaseStatus {
loading: boolean
customerRef?: string
email?: string
name?: string
purchases: PurchaseInfo[] // updated type
hasPlan: (planName: string) => boolean
activePurchase: PurchaseInfo | null
hasPaidPurchase: boolean
activePaidPurchase: PurchaseInfo | null
}
No new hooks required in v1. There is no separate Payment entity — usePurchase() already returns purchases: PurchaseInfo[] which serves as billing history. Integrators can also use solvaPay.listPurchases() directly.
Rationale: The hook shape is still correct for the new model. Long-lived purchases just have richer data. Each Purchase is a billing record for its period — no separate Payment entity needed.
D18. Next.js helpers rename agentRef -> productRef
| Current | New |
|---|---|
createPaymentIntent(req, { planRef, agentRef }) | createPaymentIntent(req, { planRef, productRef }) |
processPaymentIntent(req, { paymentIntentId, agentRef }) | processPaymentIntent(req, { paymentIntentId, productRef }) |
createCheckoutSession(req, { agentRef, planRef? }) | createCheckoutSession(req, { productRef, planRef? }) |
listPlans returns { plans, agentRef } | returns { plans, productRef } |
PurchaseCheckResult.purchases[].agentName | PurchaseCheckResult.purchases[].productName |
Cache key strategy unchanged — still userId-based. Cache invalidation points stay the same (after createPaymentIntent, processPaymentIntent, cancelRenewal).
Rationale: Param renames propagate from Core helpers. Cache layer is user-scoped, unaffected by entity rename.
D19. @solvapay/auth requires no changes
Auth adapters (AuthAdapter, SupabaseAuthAdapter, MockAuthAdapter, SolvapayAuthAdapter) deal with user identity extraction, not product/agent references. No changes needed.
Rationale: Auth is entity-agnostic. It extracts user IDs from tokens/sessions, independent of what the user purchases.
D20. @solvapay/react-supabase requires no changes
The createSupabaseAuthAdapter wraps Supabase auth. It returns an AuthAdapter for token/userId extraction. No product/agent references exist in this package.
Rationale: Same as D19. Auth adapter, not entity-aware.
D21. @solvapay/core requires minimal changes
Only change: remove version export if unused, or update it. No agent references exist in @solvapay/core today.
Rationale: Core package contains config, error class, and env validation. None reference agents.
D22. Generated types regenerate automatically
After the backend deploys new OpenAPI spec (with /v1/sdk/products routes replacing /v1/sdk/agents), run pnpm generate:types to regenerate packages/server/src/types/generated.ts. All AgentSdkController_* operation names, CreateAgentRequest schemas, and /v1/sdk/agents paths will be replaced automatically.
Sequencing: Backend API must deploy first. Then SDK regenerates types. Then hand-written SDK code updates to reference new generated types.
Rationale: generated.ts is auto-produced from OpenAPI. The change cascades from backend. No manual editing of generated.ts needed.
D23. Update all 5 example apps simultaneously
All examples are small and reference agent terminology. Update in one pass:
| Example | Changes |
|---|---|
express-basic | payable({ agent }) -> payable({ product }), env vars |
mcp-basic | payable({ agent }) -> payable({ product }), env vars |
checkout-demo | agentRef in payment flow -> productRef |
hosted-checkout-demo | agentRef in checkout flow -> productRef |
nextjs-openai-custom-gpt-actions | agentRef in API routes -> productRef |
Also update examples/shared/ if it contains shared config referencing agents.
Rationale: Examples are documentation. They must reflect the current API. Small enough to update in one pass.
D24. Purchase utility functions require no logic changes
filterPurchases, getActivePurchases, getCancelledPurchasesWithEndDate, getMostRecentPurchase, getPrimaryPurchase, isPaidPurchase — these operate on PurchaseInfo[] by checking status, amount, startDate, cancelledAt, endDate. None reference agentName in their logic.
The only change is the PurchaseInfo type they operate on (D10), which is a type-level change, not a logic change.
Rationale: Utility functions are entity-agnostic. They filter by status and dates, not by entity name.
D25. ProcessPaymentResult keeps its existing name
No rename needed since PaymentIntent methods are unchanged (D5). The type stays ProcessPaymentResult:
interface ProcessPaymentResult {
type: 'recurring' | 'one-time'
purchase?: PurchaseInfo // updated PurchaseInfo (D10)
oneTimePurchase?: OneTimePurchaseInfo
status: 'completed'
}
OneTimePurchaseInfo.productRef replaces any agent reference (currently has productRef already — no change needed there).
Rationale: Matches the unchanged
processPaymentIntent()method. No method rename means no result type rename. PurchaseInfo carries the new fields from D10.
Questions
A. Public API Naming and Terminology
Q1. What should the payable() auto-resolution fallback be when no product/productRef is provided?
Current: falls back to process.env.SOLVAPAY_AGENT, then package.json name, then 'default-agent'.
Options:
- a)
process.env.SOLVAPAY_PRODUCT->'default-product'(drop package.json detection) - b)
process.env.SOLVAPAY_PRODUCT-> throw error (require explicit config) - c)
process.env.SOLVAPAY_PRODUCT->package.json name->'default-product'
Recommendation: (a). Env var fallback for single-product apps, explicit
productReffor multi-product. Drop package.json detection — it was a cute hack but produces confusing product names. Don't throw — stub mode should still work for local dev without any config.
Q2. Should the SDK validate product reference format (prd_*) or accept any string?
Options:
- a) Accept any string (flexible, works during development)
- b) Validate
prd_prefix, warn on mismatch - c) Validate
prd_prefix, throw on mismatch
Recommendation: (a). Accept any string. During development and testing, integrators may use arbitrary strings. The backend validates the reference — the SDK shouldn't duplicate validation. This also allows backward-compatible reference formats if the backend ever changes prefixes.
Q3. What should the SOLVAPAY_PRODUCT env var behavior be in the payable.mcp() adapter specifically?
MCP servers often monetize a single product. Should the env var apply to all adapters equally, or should MCP have different defaults?
Recommendation: Same behavior across all adapters.
SOLVAPAY_PRODUCTapplies universally. No adapter-specific env var logic. The adapter is a framework choice, not a product identity choice.
B. Entity Model and Type System
Q4. Should the SDK expose a Product type for CRUD operations, or only use productRef as an opaque string?
Options:
- a) Expose
Producttype with full fields (reference,name,description,status,planIds, etc.) - b) Only expose
productRefas a string identifier — Product CRUD handled in dashboard only - c) Expose a slim
ProductInfotype (reference,name,description,status) for read-only listing
Recommendation: (c). SDK integrators need to list their products (for admin dashboards, product selectors). Full CRUD is a dashboard concern. Expose
listProducts()returningProductInfo[]andcreateProduct()/deleteProduct()as optional management methods (same pattern as current agent methods). Keep the return type slim — no planIds, no balance, no isMcpPay.
Q5. Should PurchaseInfo include the planSnapshot from the backend, or keep the current flat fields?
Backend stores planSnapshot: { name, price, currency, planType, features, limits } on each Purchase.
Options:
- a) Expose as nested
planSnapshotmatching backend - b) Keep flat:
planName,amount,currency,planTypeas top-level fields - c) Both: flat convenience fields + optional
planSnapshotfor full access
Recommendation: (b). Keep flat fields for simplicity. SDK integrators don't need the raw snapshot — they need
planName,amount,planTypeat the top level for UI rendering. If someone needs features/limits, they can query the Plan directly. Flat fields are easier to destructure in React components.
Q6. How should usage data be exposed on PurchaseInfo?
Backend Purchase has usage: { used, quota, unit, periodStart, periodEnd, resetDate, overageUnits, overageCost, carriedOverUnits }.
Options:
- a) Expose full usage subdocument
- b) Expose slim usage:
{ used, quota, unit, remaining }(computedremaining = quota - used) - c) Don't expose usage on PurchaseInfo — use a separate
useUsage()hook
Recommendation: (b). Slim usage with computed
remaining. Integrators need "50 of 100 used, 50 remaining" for progress bars. Period dates, overage costs, and carryover are backend billing concerns, not SDK display concerns. If integrators need the full breakdown, they can query the backend API directly.
Q7. Should the SDK add a PaymentInfo type for the listPayments response?
N/A. No PaymentInfo type. No listPayments method. There is no separate Payment entity in the SDK (D7). Purchase serves as the billing record. PurchaseInfo includes billing fields (amount, currency, transactionId, paymentIntentId) so integrators can render invoices/receipts directly from purchase data.
C. SDK Architecture
Q8. Should the backend API and SDK update be deployed simultaneously, or does the SDK need a compatibility window?
Options:
- a) Simultaneous deploy — backend + SDK + examples all update together
- b) Backend deploys first with dual endpoints (old + new), SDK updates after
- c) SDK ships first with feature flag, backend deploys after
Recommendation: (a). Simultaneous. No external users. Backend can deploy new endpoints and deprecate old ones in the same release. SDK regenerates types from new spec and updates in lockstep. Ship as one coordinated release.
Q9. Should generated types (generated.ts) be committed to the repo, or generated at build time?
Currently committed. After the backend API changes, the generated file will have a large diff.
Options:
- a) Keep committing generated types (current pattern)
- b) Generate at build time only, gitignore the file
Recommendation: (a). Keep committing. Committed types make diffs reviewable, work without backend access, and don't require build-time code generation. The large diff from this redesign is a one-time event.
Q10. What is the implementation order across packages?
Options:
- a) Bottom-up: core -> server -> react -> next -> examples
- b) Server-first: server (types + client + paywall) -> next -> react -> core -> examples
- c) Types-first: regenerate generated.ts -> update server types -> update server impl -> update next -> update react -> examples
Recommendation: (c). Types-first. The generated types from the backend OpenAPI spec drive everything downstream. Once
generated.tsis updated, the hand-written types update to match, then implementations, then consumers. This is the natural dependency order.
D. React Package Specifics
Q11. Should useCheckout change its function signature?
Current: useCheckout(planRef: string, agentRef?: string)
Options:
- a)
useCheckout(planRef: string, productRef?: string)— same positional args - b)
useCheckout({ planRef: string, productRef?: string })— options object - c)
useCheckout(planRef: string, options?: { productRef?: string })— hybrid
Recommendation: (b). Options object. The current positional signature is fragile as we add more options. An options object is more extensible and self-documenting:
useCheckout({ planRef: 'pln_xxx', productRef: 'prd_xxx' }).
Q12. Should PurchaseGate accept productRef in addition to (or instead of) requirePlan?
Current: <PurchaseGate requirePlan="Premium">. Gates access by plan name.
Options:
- a) Keep
requirePlanonly — plan name is sufficient for gating - b) Add
requireProductas an alternative — gate by product ownership regardless of plan - c) Add both
requireProductandrequirePlan— gate by product OR plan
Recommendation: (c). Both.
requireProductgates "does the customer own this product at all?" (any active purchase).requirePlangates "does the customer have this specific plan?". Both are valid use cases.requireProductis the more common one in the new model.
Q13. Should PaymentForm require productRef or auto-detect from context?
Current: <PaymentForm planRef="pln_xxx" agentRef="agt_xxx" />
Options:
- a) Require
productRefprop explicitly - b) Auto-detect from nearest
SolvaPayProvidercontext, allow override via prop - c) Require
productRefbut allow env var fallback
Recommendation: (a). Require explicitly. PaymentForm is used in checkout flows where the product is known. Auto-detection adds indirection. Explicit is better than implicit for payment flows — you want to be sure which product you're charging for.
E. Paywall and Error Handling
Q14. What should the paywall HTTP error response shape be?
Current:
{
"success": false,
"error": "Payment required",
"agent": "agt_xxx",
"checkoutUrl": "...",
"message": "..."
}
Options:
- a) Same shape, rename
agenttoproduct - b) Restructure:
{ "error": "payment_required", "product": "...", "checkoutUrl": "...", "details": "..." } - c) Use RFC 7807 Problem Details:
{ "type": "payment_required", "title": "...", "detail": "...", "product": "...", "checkoutUrl": "..." }
Recommendation: (a). Same shape, rename field. The current shape works. Don't over-engineer error responses during this rename pass. If error response format improvement is needed, do it as a separate effort.
Q15. Should PaywallError.structuredContent be renamed or restructured?
Current PaywallStructuredContent:
{ kind: 'payment_required', agent: string, checkoutUrl: string, message: string }
Recommendation: Rename
agent->product. Keep structure.kind: 'payment_required'is correct.productidentifies which product triggered the paywall.checkoutUrlprovides the redirect. No structural change needed.
F. Documentation and Examples
Q16. Should the SDK docs in docs/ be updated in this pass, or deferred?
The docs/ folder has getting-started guides, API references (generated by typedoc), and example overviews.
Options:
- a) Update guides and regenerate API docs in the same pass
- b) Regenerate API docs only (automatic from types), defer guide updates
- c) Defer all docs updates to a follow-up
Recommendation: (b). API docs regenerate automatically. Guides reference
agentterminology and need updating but are lower priority than working code. Update guides as a fast follow-up.
Q17. Should example apps demonstrate billing history via purchases, or just rename agent -> product?
Options:
- a) Just rename agent -> product (minimum viable)
- b) Add purchase-based billing history display to checkout-demo and hosted-checkout-demo
- c) Create a new example specifically for purchase/billing management
Recommendation: (a) for this pass. Rename only. Purchase-based billing history examples can be added when the
listPurchasesAPI is stable and tested. Keep the scope of this refactor focused.
Status
25 decisions made (D1-D25). 16 questions answered with recommendations, 1 N/A (Q1-Q17). Updated to Option E: no Payment entity, PaymentIntent methods keep their names, per-period Purchase serves as billing record.
Next Steps
- Coordinate backend API deployment — new OpenAPI spec with
/v1/sdk/productsroutes - Regenerate
generated.tsfrom new backend spec - Update
@solvapay/server— types, client, paywall, factory, helpers - Update
@solvapay/next— helpers, cache types - Update
@solvapay/react— types, hooks, components - Update all 5 example apps
- Regenerate API docs via typedoc
- Publish
1.0.0-preview.19