|
| 1 | +# SDK flow: build + deploy + monetize |
| 2 | + |
| 3 | +The full 6-step flow. Each step is one or two `@elizaos/cloud-sdk` calls. The whole sequence is idempotent at the step boundary — if step 5 fails, restart from step 5. |
| 4 | + |
| 5 | +## Setup |
| 6 | + |
| 7 | +```ts |
| 8 | +import { ElizaCloudClient } from "@elizaos/cloud-sdk"; |
| 9 | + |
| 10 | +const cloud = new ElizaCloudClient({ |
| 11 | + apiKey: process.env.ELIZAOS_CLOUD_API_KEY, |
| 12 | +}); |
| 13 | +``` |
| 14 | + |
| 15 | +`ELIZAOS_CLOUD_API_KEY` is provided by the Milady runtime. Do not invent your own key. |
| 16 | + |
| 17 | +## 1. Register the app |
| 18 | + |
| 19 | +```ts |
| 20 | +const { app, apiKey } = await cloud.routes.postApiV1Apps({ |
| 21 | + json: { |
| 22 | + name: input.name, |
| 23 | + app_url: "https://placeholder.invalid", |
| 24 | + skipGitHubRepo: true, |
| 25 | + }, |
| 26 | +}); |
| 27 | +const appId = app.id; |
| 28 | +const appApiKey = apiKey; |
| 29 | +``` |
| 30 | + |
| 31 | +`app_url` is required at registration but the container doesn't exist yet, so use a placeholder and patch it in step 5. `skipGitHubRepo: true` because the build pipeline owns the repo, not the cloud's auto-generator. |
| 32 | + |
| 33 | +On `409 name_collision`, append a 6-char random suffix and retry once: |
| 34 | + |
| 35 | +```ts |
| 36 | +const suffix = Math.random().toString(36).slice(2, 8); |
| 37 | +const retried = await cloud.routes.postApiV1Apps({ |
| 38 | + json: { name: `${input.name}-${suffix}`, app_url: "https://placeholder.invalid", skipGitHubRepo: true }, |
| 39 | +}); |
| 40 | +``` |
| 41 | + |
| 42 | +## 2. Build and push the container image |
| 43 | + |
| 44 | +The agent's job, not the SDK's. Use the org's container registry creds (default ECR via cloud's per-org setup, or any public registry the agent has push access to). The image must: |
| 45 | + |
| 46 | +- Listen on `$PORT` (cloud sets this at runtime) |
| 47 | +- Expose a `GET /health` endpoint that returns 200 quickly (the cloud's deploy step polls it before flipping the load balancer) |
| 48 | +- For chat-style apps, expose a server route that forwards user-bearing requests upstream to cloud's `/api/v1/messages` (or `/v1/chat/completions`) with the user's bearer token AND your affiliate code |
| 49 | + |
| 50 | +The canonical reference for this shape is [`apps/edad-chat/server.ts` and `apps/edad-chat/api/proxy.ts`](https://github.com/elizaOS/cloud-mini-apps/tree/main/apps/edad-chat) in `elizaOS/cloud-mini-apps`. Copy that pattern when your app is a chat shell. |
| 51 | + |
| 52 | +If you want the inline minimal version — a Next.js or Hono handler is equivalent — the shape is: |
| 53 | + |
| 54 | +```ts |
| 55 | +import { ElizaCloudClient } from "@elizaos/cloud-sdk"; |
| 56 | + |
| 57 | +const cloud = new ElizaCloudClient({ apiKey: process.env.ELIZAOS_CLOUD_API_KEY }); |
| 58 | +const AFFILIATE = process.env.ELIZA_AFFILIATE_CODE!; // your owner's affiliate code |
| 59 | + |
| 60 | +export async function handleChat(req: Request): Promise<Response> { |
| 61 | + const userToken = req.headers.get("authorization") ?? req.headers.get("x-user-token"); |
| 62 | + if (!userToken) return new Response("unauthorized", { status: 401 }); |
| 63 | + |
| 64 | + const body = await req.json(); |
| 65 | + |
| 66 | + // Forward to cloud /messages with the user's token AND our affiliate code. |
| 67 | + // The user's balance is debited; the affiliate header is what credits us. |
| 68 | + const upstream = await fetch(`${process.env.ELIZA_CLOUD_URL}/api/v1/messages`, { |
| 69 | + method: "POST", |
| 70 | + headers: { |
| 71 | + "content-type": "application/json", |
| 72 | + authorization: userToken, |
| 73 | + "x-affiliate-code": AFFILIATE, |
| 74 | + "x-app-id": process.env.ELIZA_APP_ID!, |
| 75 | + }, |
| 76 | + body: JSON.stringify(body), |
| 77 | + }); |
| 78 | + |
| 79 | + return new Response(upstream.body, { |
| 80 | + status: upstream.status, |
| 81 | + headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" }, |
| 82 | + }); |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +That's the full server-side surface. Add a `/health` route that returns 200 and you're done with step 2 from a code perspective. |
| 87 | + |
| 88 | +For frontend, ship a static page that: |
| 89 | + |
| 90 | +1. Reads the user's intended-flow choice (sign in / paste API key / etc.) |
| 91 | +2. Posts user prompts to your chat route with the user-token in the `authorization` header |
| 92 | +3. Renders streaming responses |
| 93 | + |
| 94 | +The frontend can be served by the same container or by any static host pointing at the same domain — the cloud doesn't care. |
| 95 | + |
| 96 | +## 3. Deploy the container |
| 97 | + |
| 98 | +```ts |
| 99 | +const container = await cloud.routes.postApiV1Containers({ |
| 100 | + json: { |
| 101 | + image: `<registry>/<repo>:<tag>`, |
| 102 | + appId, |
| 103 | + cpu: 256, |
| 104 | + memory: 512, |
| 105 | + env: { /* image-specific runtime vars */ }, |
| 106 | + }, |
| 107 | +}); |
| 108 | +``` |
| 109 | + |
| 110 | +After `postApiV1Containers` returns, poll `getApiV1ContainersById(container.id)` until `status === "running"` and `load_balancer_url` is populated. Health-check failures here mean the image's server doesn't bind to `$PORT` correctly — pull `cloud.routes.getApiV1ContainersByIdLogs(container.id)` and surface to the human. |
| 111 | + |
| 112 | +## 4. Set markup |
| 113 | + |
| 114 | +```ts |
| 115 | +await cloud.routes.patchApiV1AppsById({ |
| 116 | + appId, |
| 117 | + json: { |
| 118 | + inference_markup_percentage: 20, // 20% markup on every cloud-SDK call routed through this app |
| 119 | + }, |
| 120 | +}); |
| 121 | +``` |
| 122 | + |
| 123 | +Markup % is the lever that turns app activity into earnings. The active monetization model in the current schema is markup-based, NOT per-token pricing — older docs that describe per-token are stale; trust the current `apps` table schema. |
| 124 | + |
| 125 | +A 20% markup is a reasonable default for a v1 app. Higher kills retention; lower starves the survival loop. Tune later based on `redeemable_earnings_ledger` data. |
| 126 | + |
| 127 | +## 5. Patch app_url + allowed_origins |
| 128 | + |
| 129 | +```ts |
| 130 | +await cloud.routes.patchApiV1AppsById({ |
| 131 | + appId, |
| 132 | + json: { |
| 133 | + app_url: container.load_balancer_url, |
| 134 | + allowed_origins: [container.load_balancer_url], |
| 135 | + }, |
| 136 | +}); |
| 137 | +``` |
| 138 | + |
| 139 | +Without this, the OAuth redirect flow can't return users to your app, and CORS rejects browser calls from the deployed origin. |
| 140 | + |
| 141 | +## 6. Report to the human |
| 142 | + |
| 143 | +Print the audit trail so the owner can verify + cash out: |
| 144 | + |
| 145 | +``` |
| 146 | +✓ App: https://www.elizacloud.ai/dashboard/apps/<APP_ID> |
| 147 | +✓ Container: <container.load_balancer_url> |
| 148 | +✓ Markup: 20% |
| 149 | +✓ Survival: earnings auto-fund hosting; agent stays alive while profitable |
| 150 | +→ Cashout: https://www.elizacloud.ai/dashboard/earnings (Redeem for elizaOS) |
| 151 | +``` |
| 152 | + |
| 153 | +Done. The earnings loop is now active. Subsequent user activity on the app credits the owner's `redeemable_earnings_ledger`, the daily container-billing cron pulls those earnings before touching credits, and the agent stays online as long as the app is profitable. |
| 154 | + |
| 155 | +## What you do not need to do |
| 156 | + |
| 157 | +- **A description, website URL, custom domain, or per-app affiliate code** — defaults handle these or the owner sets them post-hoc on the dashboard. |
| 158 | +- **An always-on flag** — the org's `pay_as_you_go_from_earnings` controls billing strategy and is the owner's call. |
| 159 | +- **An end-to-end retry loop** — each step is idempotent on its own; restart from the failed step. |
0 commit comments