13 KiB
Cloud Apps — deploy + end-to-end validation runbook
This is the operator checklist for taking the three example apps
(edad, clone-ur-crush,
x402-image-gen) live on an example account and validating the
full money loop: payment in (card + crypto) → charging → payment out
(redemption / points).
Everything below requires production credentials (Cloudflare/Steward/Stripe/a funded wallet) and is intentionally not automated — it moves real money and mutates production. The code-side pieces (the apps, the x402→earnings binding, the nav rename) are committed and unit/flow-tested in the repo; this runbook is the remaining operator-only work.
Live status at the time of writing (read-only probes):
| surface | check | result |
|---|---|---|
| Cloud API | GET https://api.elizacloud.ai/api/health |
200 |
| Steward providers | GET https://api.elizacloud.ai/steward/auth/providers |
200 — passkey/email/sms/totp/SIWE/SIWS + Google/Discord/GitHub/Twitter |
| Login page | GET https://elizacloud.ai/login |
200 |
| x402 support | GET https://api.elizacloud.ai/api/v1/x402 |
200 |
Validated live this session (against production, with the example account API key)
- All three apps registered live on the example account with monetization
enabled (20% markup, 10% purchase share):
PayPerPixel(229e3b2d…),eDad Example(dd2b5647…),Clone Ur Crush Example(c38794b8…). The bare names "eDad"/"Clone Ur Crush" are globally taken, so example-suffixed names were used. Confirm under Dashboard → Apps. - Live x402 payment request created and app-bound —
POST /api/v1/x402/requestswithappIdreturned a real request (amountUsd 0.05, networkeip155:8453, a realpayTo). The payment-in challenge is real; on-chain settlement needs a funded wallet (operator). - Browser e2e (chromium) passed for all three miniapps, 0 console/page errors: PayPerPixel full flow (home → x402 payment card → settled image), eDad landing+config, clone-ur-crush landing+cloning funnel.
- ✅ CHARGING proven live end-to-end. 3 chat completions (
zai-glm-4.7, ~1,417 tokens) deducted $0.004648 from the org credit balance ($4.967245 → $4.962597). The charge → meter → deduct loop works. - ✅ PAYMENT-OUT machinery validated (read path).
GET /api/v1/redemptions/balancereturns the full structure + eligibility/limits and correctly gates (canRedeem:false, "Minimum redemption is $1.00. You have $0.00 available.");/api/v1/apps/<id>/earningsreturns the summary. The actual token payout can't fire only because redeemable balance is $0 (earnings require funded-wallet x402 settlement) — a resource constraint, not a code gap. "Points" = this redeemable balance (1 point = 1¢).
⚠️ Weaknesses found live (each has a follow-up task)
- Image generation returns HTTP 500 — ROOT CAUSE: exhausted provider balance.
Reproduced the fal provider call directly with the prod
FAL_KEY: it returns403 "User is locked. Reason: Exhausted balance. Top up at fal.ai/dashboard/billing". The adapter threw a raw Error →failureResponsemapped it to a blanket 500. Fixed inpackages/cloud/api/v1/generate-image/route.ts: provider failures now log the real detail and return a retryable 503 (no provider detail leaked). Operational fix to unblock live image charging: top up the fal / bitrouter / atlascloud provider balances. - Pricing-catalog gaps — major models 500 "Pricing unavailable".
openai/gpt-5.5(the documented default),anthropic/claude-haiku-4.5,x-ai/grok-4.20, andopenai/gpt-5-miniall 500 when used;openai/gpt-oss-120b:free500s "no route found";gpt-oss-120b429s (rate-limited); only some (zai-glm-4.7) work. The models registry is out of sync with the pricing catalog (ai-pricing/lookup.ts:146). Needs real prices reconciled (flagged as a task). - Monetization PUT silently ignores wrong-cased keys (snake_case → 200 no-op; use camelCase — see §1).
- Stripe
success_urlallowlist is strict — onlyhttps://elizacloud.aiis accepted bygetDefaultPlatformRedirectOrigins();www.andapp.subdomains are rejected ("Invalid success_url"). A UX footgun if the frontend ever passes a www/app origin.
Money loop — now proven live except the two user-only money-moves
- Card IN — validated both ends.
POST /api/v1/credits/checkout({credits, success_url, cancel_url},success_urlorigin must behttps://elizacloud.ai) created a real live Stripe checkout session (https://checkout.stripe.com/c/pay/cs_live_…); and the credit-grant webhook (checkout.session.completed→ record + enqueue org credit) is unit-tested green (packages/cloud/api/__tests__/stripe-webhook-route.test.ts, 4/4). Driving a card through the middle needs either a real card (live) or ask_testkey + Stripe CLI (test mode) — neither is in this env (onlysk_live, no Stripe CLI). **"local dev test mode" requires provisioning Stripe test keys- the Stripe CLI for webhook forwarding.**
- Earning → redeemable points — proven live. Inference attributed to
PayPerPixel (
X-App-Id, markup temporarily raised) grew the creator's redeemable balance $0 → $1.0812 (bySource: miniapp) with matchingapp_earnings; markup reset to 20% after. So charging → markup →app_earnings→redeemable_earnings(points) all fire live, and eligibility flips tocanRedeem:true. - Payment OUT — EXECUTED (user-authorized) → found a prod bug. Generated a
Base wallet and ran the real redemption (
POST /api/v1/redemptions, 100 pts → base). It returned HTTP 500 — andGET /redemptions/quote+/statusalso 500 — while balance/eligibility are fine and the balance is NOT locked on failure. The TWAP eliza-token-price oracle / payout-status service throws (seeredemptions/quote/route.ts→twapPriceOracle.getRedemptionQuote/payoutStatusService.isNetworkAvailable). Added error logging to both catches (was opaque). So the on-chain payout is blocked by a production payout-infrastructure failure, not authorization — flagged as a task; fix = feed the TWAP oracle / configure the payout hot wallet, or fail gracefully.
Example app fixes shipped this session
- clone-ur-crush rendered unstyled — it's on Tailwind v4 but
app/globals.cssused the dead v3@tailwind base/components/utilitiesdirectives (zero utilities generated). Fixed to v4@import "tailwindcss"+@config. Verified live.
0. Steward: confirm login + signup work (goal: "make sure we can log in and create an account")
The login UI, JWT verification, user-sync, and logout allowlist are correct in this repo. The only blockers live in the external Steward service config:
- Signup is open. New-user signup is gated by Steward
tenant_configs.join_modefor theelizacloudtenant. It must be'open'(not'invite'). This is set in Steward's own DB, not this repo.- Verify with a real signup: open https://elizacloud.ai/login → "Magic Link"
with a fresh email → confirm an org + initial free credits are created
(
syncUserFromSteward,INITIAL_FREE_CREDITS). If signup says "needs an invite", flipjoin_modeto'open'in the Steward DB.
- Verify with a real signup: open https://elizacloud.ai/login → "Magic Link"
with a fresh email → confirm an org + initial free credits are created
(
- Secrets match.
STEWARD_JWT_SECRET(or legacySTEWARD_SESSION_SECRET) on the Cloud API Worker must equal Steward's signing secret, or every authed request 401s.STEWARD_API_URLandSTEWARD_REQUEST_SIGNING_SECRETmust be set (else provider discovery / magic-link send fail). - Logout (
POST /api/auth/logout) is inpublicPathPrefixes— confirm it returns 200 and clears thesteward-tokencookies.
Acceptance: a brand-new email can sign up, land in the dashboard, see free credits, and log out cleanly.
1. Register the three apps on the example account
Sign in as the example creator, grab an API key
(Dashboard → API Keys, or Dashboard → Apps → Register App). Then for each
app, register it and capture { appId, apiKey }:
export KEY=eliza_... # example account API key
register () {
curl -s -X POST https://www.elizacloud.ai/api/v1/apps \
-H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d "{\"name\":\"$1\",\"app_url\":\"$2\"}" | jq '{id, slug, api_key_id}'
}
register "eDad" "https://edad.example"
register "Clone Ur Crush" "https://cloneurcrush.example"
register "PayPerPixel" "https://payperpixel.example"
Turn on monetization for each (markup + purchase share). NOTE: the endpoint expects camelCase keys — snake_case is silently ignored (the route 200s and applies nothing):
curl -s -X PUT https://api.elizacloud.ai/api/v1/apps/<appId>/monetization \
-H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"monetizationEnabled":true,"inferenceMarkupPercentage":20,"purchaseSharePercentage":10}'
Confirm each appears under Dashboard → Apps (the renamed nav section) with an Overview / Monetize / Earnings tab set.
2. Deploy each app
Each app is a standalone server. Two paths:
- Container (managed): swap
"@elizaos/cloud-sdk": "workspace:*"for a published version in the app'spackage.json, build the image, and deploy viaPOST /api/v1/containers(or thebuild-monetized-appskill). Set the app's env as container secrets, then patchapp_url/allowed_origins. - Self-host: run the Bun server anywhere and point
app_urlat it.
Per-app env:
| app | required env |
|---|---|
edad |
ELIZA_APP_ID, ELIZA_AFFILIATE_CODE (optional), ELIZA_CLOUD_URL; users sign in and spend their own org credits |
clone-ur-crush |
AFFILIATE_API_KEY, model/image provider keys (Fal or OpenAI), ELIZA_CLOUD_URL |
x402-image-gen |
ELIZAOS_CLOUD_API_KEY, ELIZA_APP_ID, X402_NETWORK, X402_PRICE_USD |
Smoke each deploy: GET /health → ok, GET /api/config → expected app id.
3. Validate PAYMENT IN
3a. Credit cards (Stripe) — funds the buyer's org credits
- Dashboard → Billing → choose a credit pack or custom amount → Card.
- Use a Stripe test card in test mode (
4242 4242 4242 4242) or a real card in live mode. Complete checkout. - Confirm the Stripe webhook (
/api/stripe/webhook) credits the org balance (credit_transactions) and thatDashboard → Billingshows the new balance.
3b. Crypto top-up (x402 USDC) — funds the buyer's org credits
- Dashboard → Billing → Crypto → pay USDC on Base/BSC/Solana via the
connected wallet (
/api/v1/topup/*). - Confirm the balance increases after settlement.
3c. Per-image x402 (the PayPerPixel app) — no account needed
- Open the deployed
x402-image-genapp, enter a prompt, click Generate. - The app returns a 402 with the x402 challenge (amount, network,
payTo). - Pay with a funded wallet on the chosen network (a wallet integration such as
x402-fetch, or paste the settled payload into the UI for manual testing). - The image is returned;
seendedupe prevents a second image per payment.
4. Validate CHARGING (usage → creator earnings)
- edad / clone-ur-crush: each message/generation debits the user's org
credits and records the creator's inference markup (
recordCreatorEarnings). - PayPerPixel: each settled x402 payment credits the creator's earnings via
recordAppScopedPaymentEarnings(verified bycloud/shared/.../__tests__/x402-app-earnings.test.ts). - Confirm
Dashboard → Apps → <app> → Earnings(andDashboard → Earnings) shows lifetime / withdrawable / by-source totals climbing after step 3.
5. Validate PAYMENT OUT (redemption / points)
"Points" = redeemable earnings, denominated 1 point = 1¢ USD.
- Dashboard → Earnings → confirm the withdrawable balance ≥ the payout threshold ($25 app withdraw, or redeem any amount of redeemable balance).
- Redeem for elizaOS tokens: Earnings → Redeem → pick a network
(Base/Solana/Eth/BNB) → confirm the live quote (
/api/v1/redemptions/quote). - Submit; the
process-redemptionscron +payout-processorsend tokens on-chain (token_redemptions). Confirm the redemption row reachescompletedand the tokens arrive at the destination address. - Confirm
available_balancedropped andtotal_redeemedrose.
6. Per-page e2e of each miniapp
For each deployed app, walk every page and assert no console/page errors:
edad: landing → sign-in → chat turn → history reload.clone-ur-crush: landing → cloning flow → photo analysis → character create → generated photo.x402-image-gen: prompt → 402 challenge card → settle → image → earnings panel refresh.
The cloud dashboard's own pages are covered by the visual-review harness
(bun run --cwd packages/cloud-frontend audit:cloud) — run it after any UI change.
Done = every box checked
Card in ✓ · crypto in ✓ · x402 per-action ✓ · charging→earnings ✓ · redeem→tokens out ✓ · each app's pages error-free ✓ · signup/login/logout ✓.