Skip to content

Commit 273f4c7

Browse files
authored
Merge pull request #7160 from RemilioNubilio/bot/skill-build-monetized-app
feat(skills): bundle build-monetized-app skill
2 parents 4f491a1 + 12b63cb commit 273f4c7

4 files changed

Lines changed: 359 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
name: build-monetized-app
3+
description: "Use when the task is building a new app on Eliza Cloud that earns money — chat apps, agent apps, MCP-backed tools, anything that calls the cloud's chat/messages/inference endpoints on behalf of users. Covers app registration, container deploy, markup configuration, affiliate header, and the survival-economics loop where earnings auto-fund the agent's own hosting. Pairs with the `eliza-cloud` skill (which covers Cloud as a backend in general) by focusing specifically on the build-and-monetize flow."
4+
---
5+
6+
# Build a monetized app on Eliza Cloud
7+
8+
Use this skill when you need to build an app that takes a markup on every chat or inference call and credits the earnings back to your owner. Eliza Cloud already supports app registration, per-app API keys, container deploys, the `appId`-based auth and redirect flow, the affiliate-header revenue path, and creator-monetization plumbing — you do not need to invent any of these.
9+
10+
Read `references/sdk-flow.md` for the 6-step build flow with a self-contained code example. External references (all public):
11+
12+
- **Working chat-app**: [`elizaOS/cloud-mini-apps/apps/edad-chat`](https://github.com/elizaOS/cloud-mini-apps/tree/main/apps/edad-chat) — copyable end-to-end implementation. Read its `server.ts` and `api/proxy.ts` for the canonical chat-forwarder shape using `@elizaos/cloud-sdk`.
13+
- **SDK reference**: [`@elizaos/cloud-sdk` README](https://github.com/elizaOS/cloud/tree/develop/packages/sdk) — typed methods + helpers + auth.
14+
- **Human-readable recipe**: [`docs/building-a-monetized-app.md`](https://github.com/elizaOS/cloud/blob/develop/docs/building-a-monetized-app.md) — same loop, narrative form, with the schema fields explained.
15+
16+
## The survival-economics loop
17+
18+
A Milady-style agent running in an Eliza Cloud container costs ~$0.67/day at the default tier (256 MB CPU + 512 MB RAM). When the org's credit balance and the owner's redeemable earnings both hit zero, the container is stopped after a 48-hour grace window. The container-billing cron pulls earnings before credits, so an app that earns more than its hosting costs keeps the agent alive indefinitely. See `references/survival-economics.md` for the exact accounting (`redeemable_earnings_ledger`, `credit_transactions`, the cron at `app/api/cron/container-billing/route.ts`).
19+
20+
This is why the skill exists: making money is how the agent stays online.
21+
22+
## Default flow
23+
24+
```ts
25+
import { ElizaCloudClient } from "@elizaos/cloud-sdk";
26+
27+
const cloud = new ElizaCloudClient({ apiKey: process.env.ELIZAOS_CLOUD_API_KEY });
28+
29+
// 1. register the app
30+
const { app, apiKey } = await cloud.routes.postApiV1Apps({
31+
json: { name, app_url: "https://placeholder.invalid", skipGitHubRepo: true },
32+
});
33+
34+
// 2. build + push container image
35+
// 3. deploy container
36+
// 4. set markup %
37+
// 5. patch app_url + allowed_origins to the container URL
38+
// 6. report URLs to the human
39+
```
40+
41+
Full code in `references/sdk-flow.md`. The skill assumes you have:
42+
43+
- `ELIZAOS_CLOUD_API_KEY` in env (Milady packages this for you)
44+
- `@elizaos/cloud-sdk` available (already a runtime dependency)
45+
- A goal and a name (make the name up if not given; collisions retry once with a 6-char suffix)
46+
47+
## Auth + monetization headers
48+
49+
Every cloud-SDK call your deployed app makes on behalf of a user MUST carry:
50+
51+
- `Authorization: Bearer <user_jwt>` — the JWT from the app-auth OAuth redirect
52+
- `x-affiliate-code: <your_affiliate_code>` — the owner's affiliate code; this is what credits earnings
53+
54+
This pattern is shared with the [`eliza-cloud`](../eliza-cloud/SKILL.md) skill; see that skill for the auth flow itself. This skill assumes you've already read it.
55+
56+
## Read these references in order
57+
58+
1. `references/sdk-flow.md` — the 6-step deploy + monetize flow with full code
59+
2. `references/survival-economics.md` — why this matters; how earnings flow into hosting
60+
3. `references/failure-modes.md` — recovery table for the failures you'll actually hit (name collision, container deploy failure, auth blocker, etc.)
61+
62+
## What this skill is NOT
63+
64+
- **It is not the app's product code.** The skill is the deploy + monetize + survive surface. What the app DOES is up to you given the task.
65+
- **It is not a retry loop.** Each SDK call is idempotent; if step 5 fails, restart from there.
66+
- **It does not configure affiliate codes.** Affiliate codes belong to the owner, not the app, and live across all of an owner's apps. The skill inherits whatever is configured.
67+
- **It does not assume always-on billing.** The org may have set `pay_as_you_go_from_earnings = false`, in which case hosting comes purely from credits and earnings stay on the redemption ledger. The skill works either way; the org's owner controls the toggle.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Failure modes and recovery
2+
3+
The recovery table for the failures you'll actually encounter when running the SDK flow. Each row is a real failure shape, what causes it, and what you do.
4+
5+
## Registration failures (step 1)
6+
7+
| Symptom | Cause | Recovery |
8+
|---|---|---|
9+
| `409 name_collision` from `postApiV1Apps` | Another app on the org or globally already uses this name | Append a 6-char random base36 suffix (`Math.random().toString(36).slice(2, 8)`) and retry once. If the retry also collides, surface to the human — that's a naming conflict the agent shouldn't auto-resolve a second time. |
10+
| `400 invalid_app_url` | The placeholder URL doesn't match the cloud's URL-format check | Use `https://placeholder.invalid` (the canonical placeholder); RFC-2606 reserves `.invalid` so it always parses but never resolves. |
11+
| `403 quota_exceeded` on app creation | Org has hit its `apps_per_org` limit | Tell the human; they need to retire an old app or upgrade the tier. Do not silently delete an existing app. |
12+
13+
## Image build / push failures (step 2)
14+
15+
The agent's job, not the SDK's. Common shapes:
16+
17+
| Symptom | Cause | Recovery |
18+
|---|---|---|
19+
| `denied: requested access to the resource is denied` on push | Registry credentials missing or wrong scope | Ask the human to fix registry creds; pause until resolved. |
20+
| `manifest unknown` / `403` from registry | The image tag doesn't exist (build silently failed) | Re-run the build with `--quiet=false` to see the actual error; surface that to the human if it's a Dockerfile issue. |
21+
| Image pushes fine but container deploy fails health-check | Image's server doesn't bind to `$PORT`, or binds to `127.0.0.1` instead of `0.0.0.0` | Read `cloud.routes.getApiV1ContainersByIdLogs(id)`, find the bind line, fix the Dockerfile or server.ts. |
22+
23+
## Container deploy failures (step 3)
24+
25+
| Symptom | Cause | Recovery |
26+
|---|---|---|
27+
| `402 insufficient_balance` from `postApiV1Containers` | Org has zero credits AND zero earnings | Tell the human to top up at `/dashboard/billing`. There's no auto-recovery here — an agent that can't pay can't deploy. |
28+
| Container starts but `status` stays `pending` for >5 min | Image pull is slow (large image) or scheduler is congested | Wait up to 10 min before declaring failure. Past that, pull container logs and surface. |
29+
| Container hits `crash_loop` immediately | Image runs but exits non-zero on startup | Pull `getApiV1ContainersByIdLogs(id)`, surface the stderr to the human, pause. Common causes: missing env var, server bind issue, missing dependency in the image. |
30+
| `403 quota_exceeded` on container deploy | Org has hit `containers_per_org` | Tell the human; they need to remove a container or upgrade. |
31+
32+
## Markup configuration (step 4)
33+
34+
Generally bulletproof since `inference_markup_percentage` is just a number on the apps table. Rare:
35+
36+
| Symptom | Cause | Recovery |
37+
|---|---|---|
38+
| `400 markup_out_of_range` | Markup outside the allowed bound (typically 0–50%) | Cap your value at the bound and retry. |
39+
40+
## Patch app_url + origins (step 5)
41+
42+
| Symptom | Cause | Recovery |
43+
|---|---|---|
44+
| `400 invalid_origin` | Container's `load_balancer_url` is nil because container isn't ready yet | Re-poll `getApiV1ContainersById` until `status === "running"` and `load_balancer_url` is populated, then patch. |
45+
46+
## Custom domain (post-skill, optional)
47+
48+
If the human asks for a custom domain after deploy:
49+
50+
| Symptom | Cause | Recovery |
51+
|---|---|---|
52+
| `verified: false` on the domain after add | DNS hasn't propagated | Tell the human; verification is async and depends on their DNS provider. The skill's job is done after step 6 — domain verification is not part of the skill. |
53+
54+
## Auth flow failures (during user signup)
55+
56+
These hit AFTER the skill is complete, when users actually try to sign in to the deployed app. Not the skill's responsibility, but worth knowing:
57+
58+
| Symptom | Cause | Recovery |
59+
|---|---|---|
60+
| OAuth redirect lands on `404` at `/api/v1/app-auth/connect` | Cloud hasn't deployed the steward-sync path yet | Out of scope for this skill — it's an upstream cloud deploy issue. |
61+
| User completes OAuth but chat returns `401 invalid_jwt` | The user's JWT shape changed between signup and request | Have the user re-sign-in. If it persists, the issue is upstream. |
62+
63+
## What you don't recover from
64+
65+
If the agent can't deploy at all (zero credits AND zero earnings) the loop has bottomed out. There's no programmatic recovery — only the human can top up. Tell them clearly:
66+
67+
> "I can't deploy a new app — both org credits and your redeemable earnings are zero. Top up at https://www.elizacloud.ai/dashboard/billing or earn enough on existing apps to cover the next deploy."
68+
69+
This is a survival-economics terminal state, not a code bug.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Survival economics
2+
3+
Why this skill exists: a Milady-style autonomous agent runs in an Eliza Cloud container, the container costs money, and earning money is how the agent stays online. This document explains the exact accounting so the agent's behavior matches the rules of the game.
4+
5+
## The ledger
6+
7+
Two tables in the cloud DB matter:
8+
9+
- **`credit_transactions`** — the org's purchased credit balance, debited per cloud-SDK call when no earnings are available
10+
- **`redeemable_earnings_ledger`** — the owner's accumulated markup-cuts from every app they own; can be cashed out OR auto-applied to hosting
11+
12+
Every cloud-SDK call routed through a monetized app generates two ledger rows:
13+
1. The user pays the base inference cost (debited from their org's credits)
14+
2. The markup % goes into the app-owner's `redeemable_earnings_ledger`
15+
16+
The dashboard view at `https://www.elizacloud.ai/dashboard/earnings` shows the running totals.
17+
18+
## Container billing
19+
20+
Container hosting is pay-as-you-go, billed daily. The cron at `app/api/cron/container-billing/route.ts` runs once per day and, for each container:
21+
22+
1. Computes the day's hosting cost (CPU + RAM at the tier's per-second rate)
23+
2. Pulls from `redeemable_earnings_ledger` first (if `pay_as_you_go_from_earnings = true`, which is the org default)
24+
3. Falls back to `credit_transactions` if earnings are insufficient
25+
4. If both are zero, marks the container with a `payment_overdue` warning
26+
5. After 48 hours of `payment_overdue`, the container is stopped
27+
28+
This means: **as long as your app's earnings ≥ your container's hosting cost, the loop is self-sustaining.** No credit-card top-ups, no manual intervention.
29+
30+
## The default tier
31+
32+
A new container without explicit overrides runs at:
33+
34+
- **CPU**: 256 m
35+
- **Memory**: 512 MB
36+
- **Cost**: ~$0.67/day (subject to current pricing in the cloud's `container_pricing` table)
37+
38+
An app earning a 20% markup on roughly $3.50/day of inference activity covers hosting. That's 4–5 active chat users at modest daily usage. Empirically achievable for any app that has even a small daily-active core.
39+
40+
## Why earnings come before credits
41+
42+
This is the design choice that makes the agent autonomous:
43+
44+
- If hosting was paid from credits first, the owner has to keep the credit balance topped up forever — the agent depends on the human's wallet
45+
- With earnings-first, a profitable app generates its own runway without the owner spending anything
46+
47+
The org's owner controls this via the `pay_as_you_go_from_earnings` flag on the org settings. When true (the default), the agent's economics are self-contained. When false, hosting always pulls from credits and earnings stay on the redemption ledger for cashout.
48+
49+
## What the agent should optimize for
50+
51+
In order:
52+
53+
1. **Stay online.** Earnings ≥ hosting cost. If not, prioritize work that closes that gap.
54+
2. **Profitability margin.** Earnings significantly above hosting builds reserve, which absorbs short-term dips in usage.
55+
3. **Diversification.** A single app is single-point-of-failure. Once one is profitable, the agent should add another to reduce dependence on any one app's traffic.
56+
57+
The skill itself just covers the build-and-deploy mechanics. The strategic layer (which app to build next, how to position it, when to retire one) is outside this skill.
58+
59+
## Honest limits
60+
61+
- Markup is the only revenue lever for chat-style apps in the current schema; if older code or docs reference per-message pricing, that's stale.
62+
- Earnings credit on completed inference. Failed calls don't generate earnings.
63+
- The redemption flow itself takes a small platform fee; the rate is shown on the dashboard at cashout time.
64+
- Cloud's container-quota per org caps how many simultaneous containers an agent can run. `getContainerQuota()` reports the current limit.

0 commit comments

Comments
 (0)