Skip to content

feat: credit/entitlement ledger + Stake2Pay demo page - #48

Merged
jaswinder6991 merged 2 commits into
mainfrom
feat/stake2pay-credits
Jul 28, 2026
Merged

jaswinder6991 merged 2 commits into
mainfrom
feat/stake2pay-credits

Conversation

@jaswinder6991

Copy link
Copy Markdown
Contributor

Summary

  • Adds a canonical credit/entitlement ledger (entitlements + entitlement_transactions, NUMERIC balances, NULLS NOT DISTINCT dedup for personal credits, unique source_ref idempotency) with an EntitlementService for atomic grant/balance updates.
  • New creditList (GET /credits) and subscriptionCreditSync (POST /subscriptions/{provider}/credits/sync) API routes — sync re-reads the subscription from the provider plugin itself (never trusts a client-supplied amount), grants keyed on the on-chain lock id, and retries briefly when the chain shows a subscription but the lock hasn't propagated yet.
  • New dedicated /subscriptions/stake2pay demo page: connect NEAR wallet → stake → live credit counter with optimistic update reconciled against the server. Guards against re-staking when a subscription is already active (shows "sync credits" instead).
  • Extracted signWalletIntent out of /subscriptions into lib/wallet-intent.ts so both pages sign transactions identically — no behavior change to the existing page.
  • Includes a changeset (api: minor, ui: minor) to trigger the release pipeline.

Related to #47 (credit/entitlement tracking) — this covers the stake2pay one-time-grant slice for an upcoming launch demo; the payment-checkout/webhook grant flow from #47 is intentionally deferred to a follow-up.

Test plan

  • bun typecheck — clean across api/ui (pre-existing, unrelated type errors in api/tests/integration/* from a dependency version bump are untouched by this PR)
  • bun lint — clean
  • bun run test — 49/49 API tests pass
  • Verified live in browser: page renders standalone (not nested under /subscriptions), creditList returns real per-user data, wallet-connect opens the NEAR selector, no regression on /subscriptions
  • Manually staked on testnet end-to-end: sync grants credits, re-sync correctly reports "already synced" instead of granting again, plan selection is deterministic across the 6 available price tiers
  • Recommend a second manual pass on testnet before the launch demo, given the on-chain RPC read has some inherent eventual-consistency risk

🤖 Generated with Claude Code

https://claude.ai/code/session_01CthJ9b2Zdz9P7er2sebiPe

Adds a canonical credit layer (entitlements + entitlement_transactions,
NUMERIC balances, NULLS NOT DISTINCT dedup on personal credits, unique
source_ref idempotency) and a dedicated /subscriptions/stake2pay page
that turns a confirmed NEAR stake into visible inference credits.

- api: EntitlementService with atomic grant/balance updates; creditList
  and subscriptionCreditSync routes. The sync route re-reads the
  subscription from the provider plugin itself rather than trusting a
  client-supplied amount, grants keyed on the on-chain lock id, and
  retries briefly when the chain shows a subscription but the lock
  hasn't propagated yet.
- ui: new stake2pay page with wallet connect, stake flow, live credit
  counter, and a guard that switches to sync-only (no re-staking) when
  the wallet already has an active subscription. Extracted
  signWalletIntent into lib/wallet-intent.ts so /subscriptions and the
  new page share identical signing logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CthJ9b2Zdz9P7er2sebiPe

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a canonical credit/entitlement ledger in the API (entitlements + transactions) and wires it into a Stake2Pay credit-sync flow, alongside a new dedicated UI demo page at /subscriptions/stake2pay. It also extracts shared NEAR wallet-intent signing logic into a reusable UI helper.

Changes:

  • Add Postgres schema + migrations for entitlements and entitlement_transactions, plus an EntitlementService for atomic grants and balance reads.
  • Add API routes GET /credits and POST /subscriptions/{provider}/credits/sync (provider re-read + idempotent grant by source_ref).
  • Add a new /subscriptions/stake2pay demo page and refactor wallet-intent signing into ui/src/lib/wallet-intent.ts.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ui/src/routeTree.gen.ts Registers the new /subscriptions/stake2pay route in the generated TanStack Router tree.
ui/src/routes/subscriptions.tsx Removes inline wallet-intent typing/signing and switches to the shared wallet-intent helper.
ui/src/routes/subscriptions_.stake2pay.tsx New Stake2Pay demo page: wallet connect, staking flow, credit display + sync flow.
ui/src/lib/wallet-intent.ts New shared wallet-intent types and NEAR transaction signing helper.
api/src/lib/credits.ts New entitlement/credits service: yocto→NEAR conversion, idempotent grant, balance query.
api/src/index.ts Wires EntitlementService into plugin init; adds creditList + subscriptionCreditSync handlers with RPC retry.
api/src/db/schema.ts Adds Drizzle table definitions for entitlements + entitlement transactions with constraints/indexes.
api/src/db/migrations/meta/0001_snapshot.json Drizzle migration snapshot for the new credit tables.
api/src/db/migrations/meta/_journal.json Adds migration journal entry for the new migration.
api/src/db/migrations/0001_flowery_sleeper.sql Migration SQL creating entitlements and entitlement_transactions.
api/src/contract.ts Adds contract definitions for creditList and subscriptionCreditSync.
.changeset/stake2pay-credit-sync.md Changeset to publish API/UI minors with the new credits + demo page.
Comments suppressed due to low confidence (3)

ui/src/routes/subscriptions_.stake2pay.tsx:137

  • This setQueryData call should use the same (org-scoped) credits query key as the credits useQuery, otherwise it may update the wrong cache entry after an org switch.
      queryClient.setQueryData(["credits"], sync.balances);

ui/src/routes/subscriptions_.stake2pay.tsx:189

  • This setQueryData call should use the same (org-scoped) credits query key as the credits useQuery, otherwise it may update the wrong cache entry after an org switch.
      queryClient.setQueryData(["credits"], sync.balances);

ui/src/routes/subscriptions_.stake2pay.tsx:98

  • The credits query is cached under a global ["credits"] key, but the API response is organization-scoped (server uses the activeOrganizationId from the session). If the user switches orgs, the UI can show the wrong cached balance. Use the same org-scoped creditsQueryKey for this query (and for the setQueryData calls).
  const { data: balances, refetch: refetchBalances } = useQuery({
    queryKey: ["credits"],
    queryFn: () => apiClient.creditList() as Promise<CreditBalance[]>,
  });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +66
useEffect(() => {
authClient.near.setNetwork("testnet");
setNearAccountId(authClient.near.getAccountId());
}, [authClient]);
Comment on lines +54 to +58
function Stake2PayPage() {
const apiClient = useApiClient();
const authClient = useAuthClient();
const queryClient = useQueryClient();

Comment on lines +167 to +169
const staked = yoctoToNear(amountYocto.toString());
setOptimisticBalance((Number(defaultBalance) + Number(staked)).toString());
toast.info("Stake sent — waiting for the chain to confirm");
Comment thread api/src/index.ts
Comment on lines +220 to +224
creditList: builder.creditList.handler(async ({ context }) => {
const userId = requireUserId(context);
const organizationId = context.organization?.activeOrganizationId ?? null;
return await runEffect(services.credits.getBalances({ userId, organizationId }));
}),
- bind subscriptionCreditSync's payerRef to the caller's linked NEAR
  accounts instead of trusting the client-supplied value
- always grant/read stake2pay credits as personal (org=null) on both
  the write and read path, avoiding a balance that reverts to 0 when
  an org is active
- exclude "ended" from grant eligibility and from "already staked",
  so a withdrawn lock can't be credited and a finished subscription
  doesn't block re-staking
- retry the RPC read through all backoffs instead of giving up the
  instant a lagging node reports "none"
- reject subscriptionCreditSync for any provider but stake2pay
- scope the UI credits cache key by organization, restore the NEAR
  network on unmount, do optimistic balance math in yoctoNEAR bigint
  instead of lossy Number(), and close the double-stake race by
  writing the confirmed subscription status synchronously
- add integration coverage for grant idempotency, payerRef ownership,
  withdrawn-lock exclusion, and provider guarding

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CthJ9b2Zdz9P7er2sebiPe
@jaswinder6991
jaswinder6991 merged commit 365923d into main Jul 28, 2026
2 checks passed
@jaswinder6991
jaswinder6991 deleted the feat/stake2pay-credits branch July 28, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants