[DRAFT][PLAN] AI Credits Widget: Buy AI coding credits with G$
Sub-issue of #61 — do not execute until [DRAFT] is removed from the title.
Reference Files
GoodWidget (this repo)
| Purpose |
Path |
| Widget pattern to mirror |
packages/citizen-claim-widget/src/CitizenClaimWidget.tsx |
| Adapter pattern |
packages/citizen-claim-widget/src/adapter.ts |
| Runtime contract / props |
packages/citizen-claim-widget/src/widgetRuntimeContract.ts |
| Integration manifest |
packages/citizen-claim-widget/src/integration.ts |
| Web-component bridge |
packages/citizen-claim-widget/src/element.ts, register.ts |
| Multi-step timeline reference |
packages/staking-migration-widget/src/MigrationProgressTimeline.tsx |
Reusable Stepper |
packages/ui/src/components/Stepper.tsx |
Reusable Card, Button, TokenAmount, Toast, Heading, Text |
packages/ui/src/components/ |
createComponent (named sub-theme targets) |
packages/ui/src/createComponent.ts |
| Provider + wallet hooks |
packages/core/src/provider.tsx, packages/core/src/hooks.ts |
| Storybook fixture (custodial) |
examples/storybook/src/fixtures/custodialEip1193.ts |
| Storybook fixture (injected) |
examples/storybook/src/fixtures/injectedEip1193.ts |
| Story organisation example |
examples/storybook/src/stories/citizen-claim-widget/ |
| Playwright test example |
tests/widgets/staking-migration-widget/states.spec.ts |
Cross-repo (read-only reference)
- GoodWallet / GoodProtocolUI – integration targets; embed widget via
<ai-credits-widget> web-component or React import.
- AntseedBuyerOperator contract (Base) –
depositFor(buyerKey, amount) called by backend after Celo payment settles.
- EIP-712 operator consent signed by buyer key:
AntseedBuyerOperator set as deposits operator.
Required States, Flows, and Behaviours
Widget states
| ID |
Name |
Description |
| S1 |
disconnected |
No wallet connected; show connect CTA |
| S2 |
connected_empty |
Wallet connected; G$ balance = 0; show balance + warn |
| S3 |
quote_ready |
G$ balance > 0; user has set deposit/stream amounts ≥ min; show cost breakdown + bonus badge |
| S4 |
payment_pending |
Celo buy tx submitted; spinner active |
| S5 |
payment_confirmed |
Celo tx mined; backend settling USDC on Base |
| S6 |
has_credits |
Credits landed on Base; show balance card + setup snippet |
| S7 |
usage_empty |
Credits = 0 after prior purchase; upsell |
| S8 |
usage_active |
Credits > 0, usage log visible |
| S9 |
insufficient_g_balance |
G$ balance < minimum; show top-up guidance |
| S10 |
insufficient_ai_credits |
API rejected; credits exhausted |
| S11 |
payment_failed |
Celo tx reverted or backend error |
| S12 |
backend_unavailable |
Mock/real backend unreachable |
| S13 |
unsupported_chain |
Wallet on wrong chain; show switch CTA |
Happy-path flow
- Connect –
disconnected → connected_empty / quote_ready
- GoodID badge – adapter reads GoodID verification; shows "20% streaming bonus" badge when verified.
- Buyer key – user generates (random keypair in-widget) or pastes existing buyer key; must copy + confirm if generated.
- Operator consent – buyer key signs EIP-712 message setting
AntseedBuyerOperator as deposits operator (done in-browser, no custody).
- Amount picker – choose one-time deposit (G$, min ~$1 equivalent, +10% bonus) and/or monthly stream (G$, min ~$1/month, +20% bonus with GoodID). Totals shown in G$ and USD equivalent.
- Pay – payer wallet confirms single Celo tx →
payment_pending.
- Settle – backend polls/webhook; backend calls
depositFor on Base → payment_confirmed → has_credits.
- Setup card – shows AntSeed API key / base URL snippet copyable for Cursor, Cline, etc.
- History – accordion usage log per session.
Edge / error behaviours
- Switching from generated key without copying → block with modal confirmation.
- Backend unavailable → show retry toast, keep UI interactive.
- Tx reverted →
payment_failed with reason string from receipt.
- Unsupported chain → "Switch to Celo" CTA using
switchChain action.
- GoodID not verified → show 10% deposit bonus only; hide stream bonus badge.
Execution Plan
1 — Package scaffold (packages/ai-credits-widget)
- Copy
packages/citizen-claim-widget structure.
package.json: name @goodwidget/ai-credits-widget, workspace deps @goodwidget/core, @goodwidget/ui, @goodwidget/embed, viem.
- Exports:
".", "./element", "./register" (same pattern).
2 — Runtime contract (widgetRuntimeContract.ts)
Define and export:
AiCreditsWidgetStatus union (all 13 states above).
AiCreditsWidgetPrimaryAction union: connect | switch_chain | generate_key | sign_consent | pay | retry | refresh | none.
AiCreditsWidgetAdapterState — includes: status, address, chainId, gBalance, aiCreditsBalance, isGoodIdVerified, buyerKey, depositAmount, streamAmount, bonusPercent, quote, setupSnippet, usageLog[], error.
AiCreditsWidgetAdapterActions — connect, switchChain, generateBuyerKey, pasteBuyerKey, signOperatorConsent, setDepositAmount, setStreamAmount, pay, refresh, retry.
AiCreditsWidgetProps — provider?, environment?, backendUrl?, themeOverrides?, config?, defaultTheme?, onPaySuccess?, onPayError?.
3 — Mock backend client (mockBackendClient.ts)
- Interface
AiCreditsBackendClient with methods: getQuote(address, depositG$, streamG$), getCreditsBalance(buyerKey), getUsageLog(buyerKey).
- Mock implementation returns deterministic fake data so stories and Playwright work without a live Worker.
- Production implementation will call
backendUrl REST endpoints (stubbed).
4 — Adapter hook (adapter.ts)
useAiCreditsAdapter({ environment, backendUrl }):
- Uses
useWallet() from @goodwidget/core for wallet state.
- Reads G$ balance on Celo via
viem public client.
- Reads GoodID verification status (mock for now).
- Manages local state machine for the 13 widget states.
- Exposes all
AiCreditsWidgetAdapterActions.
- Buyer key stored in component state (never leaves browser for signing).
5 — New components in packages/ai-credits-widget/src/
All built with createComponent from @goodwidget/ui to participate in the sub-theme system.
| Component |
Description |
Reuse from packages/ui? |
AiCreditsHeroCard |
G$ amount input + bonus badge |
No – widget-specific layout |
BuyerKeyPanel |
Generate/paste buyer key + copy-confirm guard |
No – widget-specific |
OperatorConsentStep |
EIP-712 sign prompt + status |
No – widget-specific |
AmountPicker |
Deposit + stream sliders/inputs + totals |
No – widget-specific |
CreditsBalanceCard |
Shows AI credits balance + usage bar |
No – widget-specific |
SetupSnippetCard |
Copyable API key / base URL block |
No – widget-specific |
UsageLog |
Accordion list of usage entries |
No – widget-specific |
AiCreditsFlowStepper |
Wraps Stepper from packages/ui with widget-specific step config |
Wraps Stepper |
AiCreditsStatusNotice |
Error / warning banners |
Wraps Text + Card from packages/ui |
Stepper, Card, Button, TokenAmount, Heading, Text, Toast*, Spinner, XStack, YStack, Separator, Anchor come from @goodwidget/ui — do not duplicate them.
6 — Main widget component (AiCreditsWidget.tsx)
- Outer:
GoodWidgetProvider wrapper (same pattern as CitizenClaimWidget).
- Inner (
AiCreditsWidgetInner): state-driven render — hero → stepper (steps: connect, buyer key, sign, pick amount, pay) → balance/setup card → usage log.
- Toast notifications for pay events via
createToast / updateToast.
7 — Web-component bridge
element.ts — defines <ai-credits-widget> custom element via @goodwidget/embed.
register.ts — auto-registers the element.
index.ts — re-exports AiCreditsWidget + types.
8 — Integration manifest (integration.ts)
export const aiCreditsIntegration = {
id: 'ai-credits',
chains: [42220], // Celo (payer side)
settlementChains: [8453], // Base (buyer credits)
states: [ /* all 13 state IDs */ ],
events: ['pay-success', 'pay-error'],
} as const
9 — Storybook stories (examples/storybook/src/stories/ai-credits-widget/)
AiCreditsWidget.mdx — overview doc.
AiCreditsWidgetShowcase.stories.tsx — per-state stories using custodialEip1193 and injectedEip1193 fixtures; mock adapter via adapterFactory prop pattern (same as staking-migration-widget).
AiCreditsWidgetQA.stories.tsx — interactive happy-path story.
- Cover all 13 states as individual story variants.
10 — Playwright smoke tests (tests/widgets/ai-credits-widget/)
states.spec.ts — one test() per widget state; visits the Storybook story, takes page.screenshot().
- Screenshots saved to
tests/widgets/ai-credits-widget/test-results/.
- Cover: disconnected, connected_empty, quote_ready, payment_pending, has_credits, payment_failed, backend_unavailable, unsupported_chain.
11 — GoodWallet / GoodProtocolUI integration notes (not in scope of this plan)
- Once widget is published, consuming apps import
@goodwidget/ai-credits-widget or @goodwidget/ai-credits-widget/register for the web-component.
- Props:
backendUrl, provider, environment.
Acceptance Criteria
Human-Reviewer Checklist
[DRAFT][PLAN] AI Credits Widget: Buy AI coding credits with G$
Sub-issue of #61 — do not execute until
[DRAFT]is removed from the title.Reference Files
GoodWidget (this repo)
packages/citizen-claim-widget/src/CitizenClaimWidget.tsxpackages/citizen-claim-widget/src/adapter.tspackages/citizen-claim-widget/src/widgetRuntimeContract.tspackages/citizen-claim-widget/src/integration.tspackages/citizen-claim-widget/src/element.ts,register.tspackages/staking-migration-widget/src/MigrationProgressTimeline.tsxStepperpackages/ui/src/components/Stepper.tsxCard,Button,TokenAmount,Toast,Heading,Textpackages/ui/src/components/createComponent(named sub-theme targets)packages/ui/src/createComponent.tspackages/core/src/provider.tsx,packages/core/src/hooks.tsexamples/storybook/src/fixtures/custodialEip1193.tsexamples/storybook/src/fixtures/injectedEip1193.tsexamples/storybook/src/stories/citizen-claim-widget/tests/widgets/staking-migration-widget/states.spec.tsCross-repo (read-only reference)
<ai-credits-widget>web-component or React import.depositFor(buyerKey, amount)called by backend after Celo payment settles.AntseedBuyerOperatorset as deposits operator.Required States, Flows, and Behaviours
Widget states
disconnectedconnected_emptyquote_readypayment_pendingpayment_confirmedhas_creditsusage_emptyusage_activeinsufficient_g_balanceinsufficient_ai_creditspayment_failedbackend_unavailableunsupported_chainHappy-path flow
disconnected→connected_empty/quote_readyAntseedBuyerOperatoras deposits operator (done in-browser, no custody).payment_pending.depositForon Base →payment_confirmed→has_credits.Edge / error behaviours
payment_failedwith reason string from receipt.switchChainaction.Execution Plan
1 — Package scaffold (
packages/ai-credits-widget)packages/citizen-claim-widgetstructure.package.json: name@goodwidget/ai-credits-widget, workspace deps@goodwidget/core,@goodwidget/ui,@goodwidget/embed,viem.".","./element","./register"(same pattern).2 — Runtime contract (
widgetRuntimeContract.ts)Define and export:
AiCreditsWidgetStatusunion (all 13 states above).AiCreditsWidgetPrimaryActionunion:connect | switch_chain | generate_key | sign_consent | pay | retry | refresh | none.AiCreditsWidgetAdapterState— includes:status,address,chainId,gBalance,aiCreditsBalance,isGoodIdVerified,buyerKey,depositAmount,streamAmount,bonusPercent,quote,setupSnippet,usageLog[],error.AiCreditsWidgetAdapterActions—connect,switchChain,generateBuyerKey,pasteBuyerKey,signOperatorConsent,setDepositAmount,setStreamAmount,pay,refresh,retry.AiCreditsWidgetProps—provider?,environment?,backendUrl?,themeOverrides?,config?,defaultTheme?,onPaySuccess?,onPayError?.3 — Mock backend client (
mockBackendClient.ts)AiCreditsBackendClientwith methods:getQuote(address, depositG$, streamG$),getCreditsBalance(buyerKey),getUsageLog(buyerKey).backendUrlREST endpoints (stubbed).4 — Adapter hook (
adapter.ts)useAiCreditsAdapter({ environment, backendUrl }):useWallet()from@goodwidget/corefor wallet state.viempublic client.AiCreditsWidgetAdapterActions.5 — New components in
packages/ai-credits-widget/src/All built with
createComponentfrom@goodwidget/uito participate in the sub-theme system.packages/ui?AiCreditsHeroCardBuyerKeyPanelOperatorConsentStepAmountPickerCreditsBalanceCardSetupSnippetCardUsageLogAiCreditsFlowStepperStepperfrompackages/uiwith widget-specific step configStepperAiCreditsStatusNoticeText+Cardfrompackages/ui6 — Main widget component (
AiCreditsWidget.tsx)GoodWidgetProviderwrapper (same pattern asCitizenClaimWidget).AiCreditsWidgetInner): state-driven render — hero → stepper (steps: connect, buyer key, sign, pick amount, pay) → balance/setup card → usage log.createToast/updateToast.7 — Web-component bridge
element.ts— defines<ai-credits-widget>custom element via@goodwidget/embed.register.ts— auto-registers the element.index.ts— re-exportsAiCreditsWidget+ types.8 — Integration manifest (
integration.ts)9 — Storybook stories (
examples/storybook/src/stories/ai-credits-widget/)AiCreditsWidget.mdx— overview doc.AiCreditsWidgetShowcase.stories.tsx— per-state stories usingcustodialEip1193andinjectedEip1193fixtures; mock adapter viaadapterFactoryprop pattern (same as staking-migration-widget).AiCreditsWidgetQA.stories.tsx— interactive happy-path story.10 — Playwright smoke tests (
tests/widgets/ai-credits-widget/)states.spec.ts— onetest()per widget state; visits the Storybook story, takespage.screenshot().tests/widgets/ai-credits-widget/test-results/.11 — GoodWallet / GoodProtocolUI integration notes (not in scope of this plan)
@goodwidget/ai-credits-widgetor@goodwidget/ai-credits-widget/registerfor the web-component.backendUrl,provider,environment.Acceptance Criteria
packages/ai-credits-widgetbuilds cleanly withpnpm run buildfrom repo root.AiCreditsFlowStepperadvances through all steps in the happy-path QA story.payment_failed,backend_unavailable,unsupported_chainstates render correct error UI.tests/widgets/ai-credits-widget/test-results/.packages/ui; all reused via import.<ai-credits-widget>renders correctly in theexamples/htmldemo.pnpm run lintpasses with no new errors.Human-Reviewer Checklist
citizen-claim-widgetstructure exactly (same file list, same exports).widgetRuntimeContract.tsexports are complete and typed — noany.if (mock)sprinkled in adapter).createComponentused for every custom layout element (enables theme overrides).custodialEip1193andinjectedEip1193fixture variants.tests/widgets/ai-credits-widget/test-results/.