Skip to content

Epic 004: Subscriptions — Stake2Pay (House of Stake) + Stripe Billing #27

Closed

Description

@jaswinder6991

Epic 004: Subscriptions — Stake2Pay (House of Stake) + Stripe Billing

Context

Epics 001–002 built a generic aggregator for one-time payments: every provider implements PaymentContract (metadata, ping, createCheckout, verifyWebhook, getSession), the API discovers providers dynamically, and the UI renders provider cards without build-time knowledge of them. Epic 003 adds webhook event persistence and merchant event delivery.

This epic adds subscriptions as a second product surface, with two providers to prove the contract is provider-agnostic:

  1. Stake2Pay — users lock NEAR in the House of Stake staking contract (stake.dao, nearai/house-of-stake-contracts feat/stake-dao branch); the locked NEAR is staked to the service provider's validator pool and the validator commission funds the provider — users pay with staking yield, not principal. Reference backend integration: nearai/chat-api PR #275 (Rust), ported here as an aggregator plugin.
  2. Stripe Billing — recurring prices via Stripe Checkout (mode: subscription) and the Subscriptions API, in the existing plugins/stripe/.

Subscriptions don't fit PaymentContract: stake2pay has no hosted checkout URL (the user's wallet signs a contract call from wallet intents) and no webhooks (state is verified by polling chain state via JSON-RPC view calls), and lifecycle verbs (cancel / resume / change plan) have no one-time equivalent. So subscriptions get their own SubscriptionContract, mirroring how Stripe separates Checkout from Billing. PaymentContract and the existing one-time flow are untouched.

Design

Flow mapping from the one-time model:

One-time (PaymentContract) Subscription (SubscriptionContract) Stake2Pay Stripe Billing
— listPlans on-chain catalog views (get_product / get_price) Stripe recurring Prices
createCheckout → hosted URL createSubscription → action wallet_intent (contract call args + deposit) redirect (Checkout URL, mode: subscription)
verifyWebhook (push) getSubscription (pull) RPC view get_subscription_for_price Subscriptions API
— cancel / resume / changePlan wallet_intent (unlock / re-lock) executed (API call)

createSubscription and lifecycle ops return a discriminated action: { kind: "wallet_intent" | "redirect" | "executed", ... } — wallet-based providers return signing instructions, hosted providers return a URL, API-based ops return the resulting state.

Key properties of the stake2pay plugin:

  • No secrets. All chain access is read-only JSON-RPC view calls; the wallet does all writes. Config is variables only: rpcUrl, networkId, contractId, productId.
  • Stateless v1. No local subscription rows; every read goes to chain. Persistence/entitlement caching is a follow-up — chat-api's experience shows local rows create a reconcile problem (their renewal-sync gap) we can avoid entirely at aggregator scope.
  • Testnet already exists. hos-e2e-0601144939.testnet (v1.0.3) has a seeded product (prod_5lklj46roIwKZK) with three recurring tiers (Starter 1–10 NEAR, Basic 10–40, Pro 40–400) — integration tests run against it read-only, no funding or keys required.
  • Wallet UX is already in the stack. near-kit and better-near-auth are in the Bun catalog and used by the UI; the subscriptions page signs the lock transaction directly instead of redirecting.

The HoS contract also supports one-off pay purchases (credits) — a direct NEAR payment path, not staking: the deposit is spent into revenue_by_validator and creates no stake lock (staking-contract/src/payments.rs). As a stretch, the plugin can additionally implement PaymentContract so stake2pay appears among one-time providers.

Tickets

# Ticket Issue Dependency
004-01 SubscriptionContract definition + shared Zod schemas #28 —
004-02 plugins/stake2pay/: NEAR RPC view client, catalog, intents, status #29 004-01
004-03 API generic subscription routes + provider discovery #30 004-01
004-04 UI /subscriptions page: plans → wallet sign / redirect → status #31 004-02, 004-03
004-05 E2E against HoS testnet contract + docs #32 004-02, 004-03
004-06 Stripe Billing implements SubscriptionContract #33 004-01, 004-03
004-07 (stretch) PaymentContract implementation for HoS one-off pay purchases #34 004-02

Notes / decisions to confirm

  • Provider key: stake2pay (registered config-only under plugins.stake2pay in bos.config.json, development: "local:plugins/stake2pay", no secrets array).
  • Subscription identity: chain subscriptions key on (near_account, price_id). The aggregator must know the payer's NEAR account — from better-near-auth session when signed in with NEAR, else provided explicitly in getSubscription input.
  • Amounts are ranges, not fixed prices: an HoS plan is "lock between X and Y NEAR for period P" (lock_factor_near_months sufficiency check on-chain). PlanSchema needs minAmount/maxAmount rather than a single unitAmount; Stripe plans set minAmount == maxAmount.
  • Cancel semantics: stake2pay "cancel" = unlock intent; funds stay staked until the epoch-based unstake pipeline settles. Status enum: active, cancel_at_period_end, pending_unstake, ended, none.
  • The testnet contract is ephemeral (e2e instance, mock validator pool, dated June 1). Pin its id in test config, expect to swap it; production contract + real catalog don't exist yet.
  • Stripe subscription webhooks (customer.subscription.*, invoice.*) flow through the existing paymentWebhook route and Epic 003's event persistence — no new webhook surface.

Acceptance Criteria

  • SubscriptionContract lives alongside PaymentContract and is imported by the API the same way (via plugin source)
  • plugins/stake2pay runs standalone via bun run dev and passes bun run test (unit: chain-JSON → plan/status mapping; integration: live testnet views)
  • API subscription routes delegate dynamically — adding a second subscription provider is config-only (proven by 004-06)
  • UI completes plan → wallet sign → active status against testnet, and plan → Stripe Checkout redirect in test mode
  • Existing one-time flow (epics 001–003) untouched; bun typecheck and bun lint pass

Activity

  1. jaswinder6991 commented on Jul 21, 2026

    @jaswinder6991
    ContributorAuthor

    Epic 004 complete: all six core tickets merged — 004-01 (#28, PR #35), 004-02 (#29, PR #36), 004-03 (#30, PR #37), 004-04 (#31, PR #38), 004-05 (#32, PR #40), 004-06 (#33, PR #39). The stretch ticket 004-07 (#34) is closed as out of scope. Closing the epic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions