Repository navigation
feat: credit/entitlement ledger + Stake2Pay demo page - #48
Merged
Merged
Conversation
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
There was a problem hiding this comment.
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
entitlementsandentitlement_transactions, plus anEntitlementServicefor atomic grants and balance reads. - Add API routes
GET /creditsandPOST /subscriptions/{provider}/credits/sync(provider re-read + idempotent grant bysource_ref). - Add a new
/subscriptions/stake2paydemo page and refactor wallet-intent signing intoui/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 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
entitlements+entitlement_transactions, NUMERIC balances,NULLS NOT DISTINCTdedup for personal credits, uniquesource_refidempotency) with anEntitlementServicefor atomic grant/balance updates.creditList(GET /credits) andsubscriptionCreditSync(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./subscriptions/stake2paydemo 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).signWalletIntentout of/subscriptionsintolib/wallet-intent.tsso both pages sign transactions identically — no behavior change to the existing page.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 inapi/tests/integration/*from a dependency version bump are untouched by this PR)bun lint— cleanbun run test— 49/49 API tests pass/subscriptions),creditListreturns real per-user data, wallet-connect opens the NEAR selector, no regression on/subscriptions🤖 Generated with Claude Code
https://claude.ai/code/session_01CthJ9b2Zdz9P7er2sebiPe