@ambasada/skills 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/package.json +34 -3
- package/skills/ambasada-payments/SKILL.md +74 -0
- package/skills/ambasada-payments/recipes/lovable-bolt.md +37 -0
- package/skills/ambasada-payments/recipes/nextjs.md +150 -0
- package/skills/ambasada-payments/recipes/other-stacks.md +159 -0
- package/skills/ambasada-payments/recipes/static-payjs.md +58 -0
- package/skills/ambasada-payments/reference/checkout.md +75 -0
- package/skills/ambasada-payments/reference/need-vs-buy.md +26 -0
- package/skills/ambasada-payments/reference/primitives.md +73 -0
- package/skills/ambasada-payments/reference/receipts.md +57 -0
- package/skills/ambasada-payments/reference/test-mode.md +36 -0
- package/skills/ambasada-product/SKILL.md +60 -0
- package/skills/ambasada-product/checklists/sellable.md +51 -0
- package/skills/ambasada-product/reference/build-elsewhere.md +82 -0
- package/skills/ambasada-product/reference/design.md +51 -0
- package/skills/ambasada-product/reference/dev-and-deploy.md +105 -0
- package/skills/ambasada-product/reference/stack.md +109 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# village.need vs village.buy
|
|
2
|
+
|
|
3
|
+
Both buy one call through the Gateway with the same money rules (hold → call → capture or refund).
|
|
4
|
+
|
|
5
|
+
| | `village.buy("inkwell/poem", input)` | `village.need("poem", input)` |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Supplier | hardcoded in your code | chosen by **your agent** (`needs.route`), changeable without a redeploy |
|
|
8
|
+
| Not routed yet | n/a | best available offering by village score within `maxPrice` |
|
|
9
|
+
| Supplier offline / fails | error, buyer refunded | Gateway tries the agent's fallbacks, then errors; your agent gets `need.failed` |
|
|
10
|
+
| Deals | price of the offering | applies a deal your agent negotiated |
|
|
11
|
+
| Use when | you depend on one specific product | **default**: keeps your product working while the agent shops around |
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const { output, amount, offering } = await village.need("image.square", { prompt }, { maxPrice: 3 });
|
|
15
|
+
// offering = which supplier served it ("pixelco/square"), amount = TON charged
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Options for both: `maxPrice` (refuse if pricier: `PRICE_ABOVE_MAX`), `idempotencyKey` (default: random per
|
|
19
|
+
call, reused by the SDK's own retry), `parent` (forward depth/budget inside a paid handler), `reason`
|
|
20
|
+
(shown to your team if the payment needs approval), `approval` (retry after `APPROVAL_REQUIRED`).
|
|
21
|
+
|
|
22
|
+
HTTP: `POST /v1/need/:name` and `POST /v1/pay/:company/:offering`, JSON body = input, headers
|
|
23
|
+
`Idempotency-Key`, `x-village-max-price`, `x-village-depth`, `x-village-budget`. Response: the seller's JSON
|
|
24
|
+
plus `x-village-call-id`, `x-village-amount`, `x-village-receipt`.
|
|
25
|
+
|
|
26
|
+
Your agent sees `MY NEEDS` (routed to, calls, failures, cost) on every wake and can re-route.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# The payment primitives
|
|
2
|
+
|
|
3
|
+
Base `https://hackaton.ambasada.id` · `Authorization: Bearer $AMBASADA_API_KEY` · TON as decimal numbers.
|
|
4
|
+
|
|
5
|
+
| Primitive | HTTP | SDK |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Checkout session (people) | `POST /v1/checkout/sessions` `{ amount \| offering, description, success_url, cancel_url?, client_reference_id?, metadata? }` → `{ id, url, status }` | `village.checkout(input, { idempotencyKey })` |
|
|
8
|
+
| Read a session | `GET /v1/checkout/sessions/:id` → `status: "open" \| "paid" \| "expired"` | `village.getCheckout(id)` |
|
|
9
|
+
| Payment link (static pages) | `POST /v1/checkout/links` → `{ id: "pl_…", url }` | `village.createPaymentLink(…)` |
|
|
10
|
+
| Webhook endpoint | `PUT /v1/webhook { url }` → `{ secret: "whsec_…" }` | `village.setWebhook(url)` / `village.verifyWebhook(req)` |
|
|
11
|
+
| Pay (buy one call) | `POST /v1/pay/:company/:offering` + `Idempotency-Key`, `x-village-max-price` | `village.buy("company/offering", input, { maxPrice })` |
|
|
12
|
+
| Need (buy what your agent routed) | `POST /v1/need/:name` | `village.need(name, input, { maxPrice })` |
|
|
13
|
+
| Listings | `PUT /v1/offerings/:name` `{ description, price, endpoint, input?, output?, timeout_ms?, status? }`, `GET /v1/offerings` | `village.listings.put(name, def)` / `.list()` |
|
|
14
|
+
| Wallet / ledger | `GET /v1/wallet` → `{ balance, held, available }`, `GET /v1/ledger?since=` | `village.wallet()`, `village.ledger(since)` |
|
|
15
|
+
| Transfer (refunds, payouts) | `POST /v1/transfer { to, amount, reason? }` + `Idempotency-Key` | `village.transfer(to, amount, { reason })` |
|
|
16
|
+
| Receipt keys | `GET /.well-known/jwks.json` (no auth) | used by `village.verifyReceipt(req)` |
|
|
17
|
+
|
|
18
|
+
## Listings
|
|
19
|
+
|
|
20
|
+
An offering is what agents find with `market.search` and buy through the Gateway:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
await village.listings.put("translate", {
|
|
24
|
+
description: "Translate RO/EN/DE/FR, keeps tone.",
|
|
25
|
+
price: 1, // TON, 0.1–500
|
|
26
|
+
endpoint: "https://my.app/api/translate", // absolute; the Gateway POSTs here
|
|
27
|
+
input: { text: "string", to: "string" }, // checked before money moves
|
|
28
|
+
output: { text: "string" }, // checked before the seller is paid
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Schema field types: `string`, `text`, `number`, `integer`, `boolean`, `object`, `array`, `json`,
|
|
33
|
+
`image_url`, `file_url`; a trailing `?` makes a field optional. In a project, offerings live in the file
|
|
34
|
+
that `ambasada.config.ts` points to (`offerings:`); `ambasada offerings sync` and `ambasada deploy` push them.
|
|
35
|
+
|
|
36
|
+
## Utilities
|
|
37
|
+
|
|
38
|
+
Paid in TON from your company wallet, only on success.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const text = await village.llm("Say hi in Romanian"); // deepseek-v4.1-flash
|
|
42
|
+
const res = await village.llm.chat({ model: "deepseek-v4-pro", messages }); // OpenAI-compatible
|
|
43
|
+
const { url } = await village.images.generate({ prompt: "a lighthouse", model: "nano_banana" });
|
|
44
|
+
const tools = await village.tools.list(); // curated catalog, TON prices
|
|
45
|
+
const { output } = await village.tools.call("web.search", { query: "Mamaia weather" });
|
|
46
|
+
const file = await village.files.put(bytes, { contentType: "image/png" }); // public URL for other products
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
HTTP: `POST /v1/llm/chat` (OpenAI body), `POST /v1/images { model, prompt }` → `{ url, cost }`,
|
|
50
|
+
`GET /v1/tools`, `POST /v1/tools/:id` → `{ output, cost, call_id }`, `POST /v1/files`.
|
|
51
|
+
LLM models: `deepseek-v4.1-flash` (default, cheap), `deepseek-v4-pro`, `glm-5.3-flash`.
|
|
52
|
+
Images: `nano_banana` (2 TON), `gpt-image-2` (8 TON).
|
|
53
|
+
|
|
54
|
+
## Errors
|
|
55
|
+
|
|
56
|
+
`{ "error": { "code", "message", call_id?, charged?: false, refunded?, approval_id? } }`. The SDK throws
|
|
57
|
+
`VillageError` with the same `code`:
|
|
58
|
+
|
|
59
|
+
| code | meaning | what to do |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `INSUFFICIENT_FUNDS` | wallet too low | sell something, or ask the team |
|
|
62
|
+
| `PRICE_ABOVE_MAX` | price rose over your `maxPrice` | re-check the market |
|
|
63
|
+
| `OFFLINE`, `TIMEOUT`, `UPSTREAM_ERROR`, `SCHEMA_INVALID` | the seller failed; you were refunded | retry later or pick another seller |
|
|
64
|
+
| `APPROVAL_REQUIRED` | over your agent's automatic limit | the team approves in the app; retry with `{ approval: e.approvalId }` |
|
|
65
|
+
| `CAP_*` | a spending limit | wait, or the team raises it |
|
|
66
|
+
| `DEPTH_EXCEEDED`, `BUDGET_EXCEEDED` | purchase chain too deep / over the parent's budget | buy less downstream |
|
|
67
|
+
| `PROVIDER_BUDGET` | the village's LLM pool is empty | tell the organizers |
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { isVillageError } from "@ambasada/sdk";
|
|
71
|
+
try { await village.buy("inkwell/poem", input, { maxPrice: 2 }); }
|
|
72
|
+
catch (e) { if (isVillageError(e, "PRICE_ABOVE_MAX")) { /* … */ } else throw e; }
|
|
73
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Selling to agents: verify the receipt
|
|
2
|
+
|
|
3
|
+
An agent (or another product) buys your offering through the Gateway:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
buyer ── POST /v1/pay/<you>/<offering> ──► Gateway: hold buyer's TON
|
|
7
|
+
Gateway ── POST <your endpoint>, header x-village-receipt ──► you
|
|
8
|
+
you answer 2xx + JSON matching `output` ──► Gateway captures the TON to you
|
|
9
|
+
anything else (4xx/5xx, timeout 30 s, wrong shape) ──► buyer refunded
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The receipt is an Ed25519-signed JWT: `call_id, buyer, seller, offering, amount, depth, budget?,
|
|
13
|
+
body_sha256, exp`. Verify it **offline** with the village key from `/.well-known/jwks.json`. A valid
|
|
14
|
+
receipt means the money is held and becomes yours when you answer 2xx.
|
|
15
|
+
|
|
16
|
+
## With the SDK (any fetch-style runtime)
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { village } from "@ambasada/sdk";
|
|
20
|
+
|
|
21
|
+
export const POST = village.sell("translate", async (input, call) => {
|
|
22
|
+
// input: parsed JSON body; call: { call_id, buyer, amount, depth, budget, … }
|
|
23
|
+
return { text: await doTheWork(input) }; // → 200 JSON, you get paid
|
|
24
|
+
}); // unpaid / forged / replayed → 402, no work done
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or by hand: `const call = await village.verifyReceipt(req, { offering: "translate" })` returns the
|
|
28
|
+
claims plus `call.input`, and throws `ReceiptInvalidError` (status 402, `reason`: `missing`,
|
|
29
|
+
`bad_signature`, `expired`, `wrong_seller`, `wrong_offering`, `body_mismatch`, `replayed`, …).
|
|
30
|
+
It accepts a fetch `Request` (not consumed) or a Node/Express request with a raw body.
|
|
31
|
+
|
|
32
|
+
Checks it makes: signature by a village key · not expired · issuer `village` · `seller` is your company
|
|
33
|
+
(`AMBASADA_COMPANY`, or looked up once from your key) · `offering` is `<you>/translate` · the SHA-256 of
|
|
34
|
+
the raw body equals `body_sha256` (no swapping the input) · the `call_id` was not used before (per process).
|
|
35
|
+
|
|
36
|
+
## Buying inside a paid call (supply chains)
|
|
37
|
+
|
|
38
|
+
Forward the chain so the Gateway can enforce depth ≤ 4 and the buyer's budget:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
export const POST = village.sell("post_kit", async ({ topic }, call) => {
|
|
42
|
+
const img = await village.need("image.square", { prompt: topic }, { parent: call, maxPrice: 3 });
|
|
43
|
+
return { image_url: img.output.image_url, text: await village.llm(`Write a post about ${topic}`) };
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Without the SDK
|
|
48
|
+
|
|
49
|
+
Any JWT library with EdDSA (Ed25519): fetch `GET /.well-known/jwks.json`, pick the key by `kid`, verify,
|
|
50
|
+
then check `exp`, `iss == "village"`, `seller`, `offering`, and `sha256(raw body) == body_sha256` (hex).
|
|
51
|
+
Complete example: [the FastAPI recipe](../recipes/other-stacks.md#fastapi-or-any-language-over-plain-http).
|
|
52
|
+
|
|
53
|
+
## Checkout receipts
|
|
54
|
+
|
|
55
|
+
pay.js gives the page a *checkout* receipt (`kind: "checkout"`, `session`, `buyer`, `amount`). Verify it on
|
|
56
|
+
your server with `village.verifyCheckoutReceipt(token, { session })`. A checkout receipt never verifies as a
|
|
57
|
+
call receipt and vice versa.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Test mode: `ambasada dev`
|
|
2
|
+
|
|
3
|
+
A local mock of the whole Payments API on `http://127.0.0.1:8788`. No login, fake money, same routes and
|
|
4
|
+
signatures as production, so your code does not change between test and live: only `AMBASADA_API_URL` and
|
|
5
|
+
`AMBASADA_API_KEY` do.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx ambasada dev # in your project: the mock, your offerings registered, and your site's dev server
|
|
9
|
+
npx ambasada dev buy translate '{"text":"Bună!","to":"en"}' # a fake agent pays you 1 TON
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
A project made by `npm create ambasada@latest` already has `AMBASADA_API_URL=http://127.0.0.1:8788` and
|
|
13
|
+
`AMBASADA_API_KEY=sk_test_dev` in its local env file. Running your site yourself? `npx ambasada dev --mock-only`,
|
|
14
|
+
and start it with those two variables set.
|
|
15
|
+
|
|
16
|
+
What it gives you:
|
|
17
|
+
|
|
18
|
+
- **Fake bank**: your company starts with 500 TON; `fake-agent` (key `sk_test_agent`) has 100; the fake
|
|
19
|
+
visitor `p_visitor` has 100. `POST /dev/mint { to?, amount }` adds more. `ambasada wallet`, `ambasada ledger` work.
|
|
20
|
+
- **Real Pay proxy**: holds, calls your endpoint with a signed `x-village-receipt` (keys at
|
|
21
|
+
`/.well-known/jwks.json`), captures on 2xx, refunds on failure, checks `input`/`output` schemas.
|
|
22
|
+
- **Checkout**: `village.checkout()` returns a `/pay/cs_…` page where the fake visitor approves; the signed
|
|
23
|
+
`checkout.paid` webhook goes to your webhook URL (secret `AMBASADA_WEBHOOK_SECRET` from the local env file).
|
|
24
|
+
`/pay.js` is the production script itself, so buttons, redirect and popup mode, `village:paid` /
|
|
25
|
+
`village:cancelled` and `GET /v1/web/checkout/:id` behave as on the real site. Webhook test, retry and
|
|
26
|
+
link / session management routes are there too.
|
|
27
|
+
- **Canned utilities**: `/v1/llm/chat` answers `[mock] <your last message>` (0.01 TON), `/v1/images` returns
|
|
28
|
+
a placeholder image (2 TON), `/v1/tools` has `web.search`, `web.news`, `web.extract`, `voice.speech`.
|
|
29
|
+
- **Needs**: `image.square` is routed to the mock supplier `mockco/square-image`; `POST /dev/route
|
|
30
|
+
{ need, offering }` routes others.
|
|
31
|
+
- **Agent**: `/v1/agent/config`, `/v1/agent/steer` (canned reply), `/v1/agent/transcript`.
|
|
32
|
+
- **Dashboard** at `http://127.0.0.1:8788/`: wallets, your offerings with a "Buy" button, recent calls.
|
|
33
|
+
|
|
34
|
+
Any bearer key is accepted as your company except `sk_test_agent`. Going live: `ambasada login`, then
|
|
35
|
+
`ambasada deploy` (Village hosting sets the real key and URL for you). Hosting it yourself: put your `sk_live_…`
|
|
36
|
+
key in the host's env, leave `AMBASADA_API_URL` unset, then `ambasada link https://your.product`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ambasada-product
|
|
3
|
+
description: Build a digital product that sells in the Hack A Ton Village and put it online. Use when the user wants to make a shop, app, tool or website that people or agents pay for in TON (the Village play currency), start one with `npm create ambasada@latest`, run it with `ambasada dev`, publish it with `ambasada deploy`, check it is ready to sell, or bring a site built in ChatGPT, Lovable, Bolt or v0 into the Village with `ambasada link`. Default stack: Next.js on Cloudflare.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a product that sells in the Village
|
|
7
|
+
|
|
8
|
+
The Village is the Hack A Ton market. Everything is paid in **TON**: a play currency held by the Village
|
|
9
|
+
bank, **not** the TON blockchain. Visitors spend TON from their Ambasada ID wallet; other teams' agents
|
|
10
|
+
buy from you through the Gateway.
|
|
11
|
+
|
|
12
|
+
A product is **one website** (one Cloudflare Worker) at `https://<your-slug>.hackaton.ambasada.id`, plus
|
|
13
|
+
**your agent**, which runs the shop (prices, suppliers, deals) while you sleep.
|
|
14
|
+
|
|
15
|
+
For payment details (Checkout, receipts, webhooks, `buy`/`need`, LLM and images) use the
|
|
16
|
+
**ambasada-payments** skill. This skill is about the product around them.
|
|
17
|
+
|
|
18
|
+
## Start
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm create ambasada@latest my-shop # Next.js (default)
|
|
22
|
+
cd my-shop && npm install
|
|
23
|
+
npx ambasada dev # fake bank + your site, no login, fake money
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Other templates: add `-- --template spa` (Vite + React; for code exported from Lovable or Bolt) or
|
|
27
|
+
`-- --template worker` (plain HTML + small fetch handlers; the smallest possible product).
|
|
28
|
+
All three have the same `ambasada.config.ts`, `agent/` folder and four endpoints: `checkout`, `webhook`,
|
|
29
|
+
`receipt` and the paid offering `translate`.
|
|
30
|
+
|
|
31
|
+
## What to read
|
|
32
|
+
|
|
33
|
+
| The user wants to… | Read |
|
|
34
|
+
|---|---|
|
|
35
|
+
| know where files go, what Next.js features are allowed, where to keep data and secrets | [reference/stack.md](reference/stack.md) |
|
|
36
|
+
| run it locally, put it online, use their own Cloudflare account | [reference/dev-and-deploy.md](reference/dev-and-deploy.md) |
|
|
37
|
+
| make it look good and sell (one page, one offer) | [reference/design.md](reference/design.md) |
|
|
38
|
+
| build in ChatGPT, Lovable, Bolt or v0 and sell from there | [reference/build-elsewhere.md](reference/build-elsewhere.md) |
|
|
39
|
+
| know if it is ready | [checklists/sellable.md](checklists/sellable.md) |
|
|
40
|
+
|
|
41
|
+
## Rules (break one and the product does not work in the Village)
|
|
42
|
+
|
|
43
|
+
1. **One product = one Worker.** No separate backend, no own database, no Docker. Data goes in
|
|
44
|
+
`ambasada.store` (small JSON) or `ambasada.files` (images, PDFs). See [reference/stack.md](reference/stack.md#where-data-lives).
|
|
45
|
+
2. **Only the allowed Next.js subset**: pages, server components, route handlers, server actions, `fetch`.
|
|
46
|
+
No `next/image` default loader, no ISR/`revalidate`, no `fs`/`child_process`/native packages, no custom
|
|
47
|
+
server, no `middleware.ts`/`proxy.ts`, no `runtime = "edge"`. See [reference/stack.md](reference/stack.md#nextjs-allowed-and-banned).
|
|
48
|
+
3. **The API key never reaches the browser.** It lives in `.env` locally (or `.dev.vars`); only server code reads it.
|
|
49
|
+
Never prefix it with `NEXT_PUBLIC_`.
|
|
50
|
+
4. **Deliver only after payment is proven on the server**: webhook, `getCheckout(id).status === "paid"`,
|
|
51
|
+
or a verified receipt. Never because the URL says `?session=…`.
|
|
52
|
+
5. **Sellable means**: a landing page with the price and a Pay button above the fold, one offering
|
|
53
|
+
with a receipt-checked endpoint, a webhook that delivers, the agent configured. Run
|
|
54
|
+
[checklists/sellable.md](checklists/sellable.md) before saying "done".
|
|
55
|
+
|
|
56
|
+
## When the user is not a developer
|
|
57
|
+
|
|
58
|
+
Do the work; don't ask them to edit code. Ask at most: what they sell, to whom, for how many TON.
|
|
59
|
+
Then build one page, run `npx ambasada dev`, open it, pay as the fake visitor, and show them it worked.
|
|
60
|
+
Explain in one sentence per step, no jargon.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Sellable checklist
|
|
2
|
+
|
|
3
|
+
Run it before saying "done". Check each line yourself; don't assume. Every line needs `npx ambasada dev`
|
|
4
|
+
running, except the last section.
|
|
5
|
+
|
|
6
|
+
## The page
|
|
7
|
+
|
|
8
|
+
- [ ] `/` on a phone-sized window (375 px wide) shows, without scrolling: what it is, the price in TON, the Pay button.
|
|
9
|
+
- [ ] The price on the page equals the price charged (the Checkout `amount`, or the offering's `price`).
|
|
10
|
+
- [ ] One offer. Any second offer has its own price next to its own button.
|
|
11
|
+
- [ ] No login, no signup, no "coming soon", no lorem ipsum, no broken images.
|
|
12
|
+
|
|
13
|
+
## People can pay
|
|
14
|
+
|
|
15
|
+
- [ ] Pressing Pay opens the Village approve page (`/pay/cs_…` or `/pay/pl_…`); approving as the fake visitor works.
|
|
16
|
+
- [ ] After paying, `/thanks` shows the result, or "working on it" and then the result.
|
|
17
|
+
- [ ] `/thanks?session=` with a made-up or unpaid session does **not** show the result.
|
|
18
|
+
- [ ] The webhook route verifies the signature (`verifyWebhook`) and delivers on `checkout.paid`; a
|
|
19
|
+
repeated event (same `webhook-id`) does not deliver twice.
|
|
20
|
+
- [ ] The Checkout `description` has no personal data (it is public in the ledger).
|
|
21
|
+
|
|
22
|
+
## Agents can pay
|
|
23
|
+
|
|
24
|
+
- [ ] At least one offering in the offerings list that `ambasada.config.ts` points to, with
|
|
25
|
+
`description`, `price`, `endpoint`, `input` and `output`.
|
|
26
|
+
- [ ] Its route uses `sell(...)` or `verifyReceipt(...)`: a plain `curl -X POST` to it gets `402`.
|
|
27
|
+
- [ ] `npx ambasada dev buy <offering> '<json input>'` succeeds and the output matches `output`.
|
|
28
|
+
- [ ] On failure it answers non-2xx (the buyer is refunded), never 200 with an error inside.
|
|
29
|
+
|
|
30
|
+
## Safe
|
|
31
|
+
|
|
32
|
+
- [ ] `grep -rn "sk_live_\|sk_test_\|whsec_" app components lib public src web` finds nothing
|
|
33
|
+
(keys live only in `.env` / `.dev.vars`), and nothing is named `NEXT_PUBLIC_*KEY*`.
|
|
34
|
+
- [ ] `.dev.vars` and `.env*` are in `.gitignore`.
|
|
35
|
+
- [ ] No banned Next.js features: `grep -rn "next/image\|revalidate\|runtime = \"edge\"\|from \"fs\"\|child_process" app lib`
|
|
36
|
+
finds nothing (or only `images: { unoptimized: true }` in `next.config.ts`).
|
|
37
|
+
|
|
38
|
+
## The agent
|
|
39
|
+
|
|
40
|
+
- [ ] `agent/agent.md` says what you sell, your price range (minimum and maximum) and what the agent may buy.
|
|
41
|
+
- [ ] `agent/settings.json` `limits` are sane for your wallet (defaults: 10 TON per payment, 100 per day,
|
|
42
|
+
5 automatic per payment).
|
|
43
|
+
|
|
44
|
+
## Online (after `npx ambasada deploy`)
|
|
45
|
+
|
|
46
|
+
- [ ] `https://<slug>.hackaton.ambasada.id` loads on a phone.
|
|
47
|
+
- [ ] `npx ambasada offerings list`: every endpoint starts with your public URL, none with `localhost`.
|
|
48
|
+
- [ ] `npx ambasada webhook test` prints `delivered`.
|
|
49
|
+
- [ ] One real payment by someone outside your team (members can't buy from their own company) shows up
|
|
50
|
+
in `npx ambasada ledger`, and they got what they paid for.
|
|
51
|
+
- [ ] `npx ambasada logs --agent` shows the agent woke up.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Built in ChatGPT, Lovable, Bolt or v0
|
|
2
|
+
|
|
3
|
+
Three ways, easiest first. Replace `pl_XXXX`, prices and names with yours.
|
|
4
|
+
|
|
5
|
+
## A. Keep it hosted there, add a Pay button
|
|
6
|
+
|
|
7
|
+
Good for: a page that sells something simple (a poster, a reading, a coffee for the team).
|
|
8
|
+
|
|
9
|
+
1. On a laptop with Node 22+, once:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm create ambasada@latest my-shop # you need its ambasada.config.ts and agent/ folder
|
|
13
|
+
cd my-shop && npm install
|
|
14
|
+
npx ambasada login
|
|
15
|
+
npx ambasada links create --amount 2 --description "Your name as a folk poem" --success-url https://YOUR-SITE/thanks
|
|
16
|
+
# → pl_XXXX
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. Paste this into the builder's chat:
|
|
20
|
+
|
|
21
|
+
> Add a "Pay 2 TON" button to the top of the home page, visible without scrolling on a phone.
|
|
22
|
+
> Load `<script src="https://hackaton.ambasada.id/pay.js" defer></script>` on every page (in the HTML head
|
|
23
|
+
> or index.html). Render the button as `<button data-village-pay="pl_XXXX">Pay 2 TON</button>`.
|
|
24
|
+
> Make a `/thanks` page that also loads pay.js. Listen for the `village:paid` event on `document`
|
|
25
|
+
> (`event.detail` has `session` and `receipt`): when it fires, show the result. On `village:cancelled`,
|
|
26
|
+
> do nothing. Do not add Stripe or any other payment provider. Do not add login.
|
|
27
|
+
|
|
28
|
+
3. Register the site with the Village:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx ambasada link https://YOUR-SITE
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It warns if `https://YOUR-SITE/health` doesn't answer; for a page with only a Pay button that's fine.
|
|
35
|
+
It also lists the template's `translate` offering at your site, which has no such endpoint: delete it
|
|
36
|
+
from the offerings list first, or add `--no-sync`.
|
|
37
|
+
|
|
38
|
+
Limit: anything unlocked only in the browser can be unlocked by anyone who reads the page's code. Fine for
|
|
39
|
+
a thank-you and a small gift; for downloads, credits or paid AI work, use B or C.
|
|
40
|
+
|
|
41
|
+
## B. Bring the code home (recommended for Lovable and Bolt)
|
|
42
|
+
|
|
43
|
+
Good for: anything with real work behind the button. You get Village hosting and the full rules.
|
|
44
|
+
|
|
45
|
+
1. Export the project to GitHub (Lovable: GitHub sync; Bolt: download or GitHub) and clone it.
|
|
46
|
+
2. `npm create ambasada@latest my-shop -- --template spa` and copy the exported `src/` (components,
|
|
47
|
+
pages, styles) into the new project's `src/`, replacing the starter UI. Keep the template's `api/`,
|
|
48
|
+
`agent/`, `ambasada.config.ts`, `wrangler.jsonc` and `vite.config.ts`.
|
|
49
|
+
3. Open the project in Claude Code, Cursor or Codex and say:
|
|
50
|
+
|
|
51
|
+
> This is a Village product (see .claude/skills/ambasada-product). Wire the Pay button in my UI to the
|
|
52
|
+
> template's checkout endpoint, remove Supabase/Firebase and any login, store data with ambasada.store,
|
|
53
|
+
> then run the sellable checklist.
|
|
54
|
+
|
|
55
|
+
4. `npx ambasada dev`, test a payment, then `npx ambasada deploy`.
|
|
56
|
+
|
|
57
|
+
## C. ChatGPT (or any chat) writes the code, you paste it
|
|
58
|
+
|
|
59
|
+
1. `npm create ambasada@latest my-shop`, `cd my-shop && npm install`, `npx ambasada dev`.
|
|
60
|
+
2. Paste this into ChatGPT, then the two files it mentions, then your idea:
|
|
61
|
+
|
|
62
|
+
> I have a Next.js 16 App Router project that runs on Cloudflare Workers (OpenNext). Rules: no
|
|
63
|
+
> database, no `fs`, no native packages, no `next/image` (use `<img>`), no middleware, no `revalidate`,
|
|
64
|
+
> no login. Server code gets the payments client from `lib/ambasada.ts`; it never runs in the browser.
|
|
65
|
+
> Below are my `app/page.tsx` and `app/api/checkout/route.ts`. Keep the checkout route and call it the
|
|
66
|
+
> way the current page does. Rewrite `app/page.tsx` for my idea: what I sell, the price in TON and
|
|
67
|
+
> the Pay button, all visible on a phone without scrolling. After paying, the buyer lands on
|
|
68
|
+
> `app/thanks/page.tsx`; keep its check that the session is `"paid"` before showing the result.
|
|
69
|
+
> Write complete files, one code block per file, with the file path above each.
|
|
70
|
+
> My idea: …
|
|
71
|
+
|
|
72
|
+
3. Paste each file where ChatGPT says, keep `npx ambasada dev` running, check the page, pay as the fake
|
|
73
|
+
visitor. When it works: `npx ambasada deploy`.
|
|
74
|
+
|
|
75
|
+
Use ChatGPT to write files, not to decide the stack: if its answer adds Prisma, Supabase, NextAuth,
|
|
76
|
+
`next/image` or a `middleware.ts`, tell it "that's banned in my project, do it without".
|
|
77
|
+
|
|
78
|
+
## Connect your AI tool to the Village (optional)
|
|
79
|
+
|
|
80
|
+
The Village MCP server (`https://hackaton.ambasada.id/mcp`) lets Claude, ChatGPT, Cursor or Lovable read
|
|
81
|
+
your wallet, create offerings and set up your agent while you build. Setup per tool: `docs/CONNECT.md`
|
|
82
|
+
in the Village repo. It is for building only; your published site never uses it.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Design for people who pay in 10 seconds
|
|
2
|
+
|
|
3
|
+
Visitors open your product on a phone, inside the Village app, between two other stands. They decide
|
|
4
|
+
in seconds. Build for that.
|
|
5
|
+
|
|
6
|
+
## One page, one offer
|
|
7
|
+
|
|
8
|
+
- **One page.** Everything a buyer needs fits on the landing page. Extra pages only for `/thanks` and
|
|
9
|
+
maybe one result page.
|
|
10
|
+
- **One offer.** One thing, one price. "Your name as a Romanian folk poem, 2 TON". Not a menu of six.
|
|
11
|
+
Add a second offer only after the first one sells.
|
|
12
|
+
- **Above the fold, on a phone (375 × 667):** what it is (one line), the price in TON, the Pay button.
|
|
13
|
+
Nothing to scroll past, no signup, no cookie banner.
|
|
14
|
+
- **Few words.** A headline of up to 8 words, one sentence under it, the button. Cut every adjective
|
|
15
|
+
you can.
|
|
16
|
+
- **Show the result.** One example of what the buyer gets (an image, a sample text) beats any description.
|
|
17
|
+
|
|
18
|
+
## The button
|
|
19
|
+
|
|
20
|
+
- Label it with the action and the price: **Pay 2 TON**, or **Get my poem: 2 TON**.
|
|
21
|
+
- Big (at least 48 px tall), full width on phones, one strong colour, nothing else in that colour.
|
|
22
|
+
- If the buyer must type something first (a name, a photo), put that one field right above the button.
|
|
23
|
+
- After paying, land on `/thanks` with the result, or "Working on it…" and the result appearing when ready.
|
|
24
|
+
Never a blank page.
|
|
25
|
+
|
|
26
|
+
## Look
|
|
27
|
+
|
|
28
|
+
- Plain CSS is enough. One font (the system font is fine), two colours plus grey, generous spacing.
|
|
29
|
+
- Large text: 18 px body on phones, a headline twice that.
|
|
30
|
+
- Dark text on a light background unless your idea needs otherwise.
|
|
31
|
+
- Works with one thumb: tap targets 48 px, no hover-only menus.
|
|
32
|
+
- Loads fast: no video backgrounds, no 3 MB hero images, no animation libraries.
|
|
33
|
+
|
|
34
|
+
## Words
|
|
35
|
+
|
|
36
|
+
- Say what they get, not how it works. "A logo for your stand in 1 minute", not "AI-powered logo generation".
|
|
37
|
+
- Prices are TON, written as `2 TON`. TON is the Village play currency; never call it crypto.
|
|
38
|
+
- Write in the language of your buyers. At Hack A Ton that is often Romanian and English; one is fine.
|
|
39
|
+
|
|
40
|
+
## Before and after
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Bad: Welcome to PoemForge™, the next-generation AI-powered creative writing platform
|
|
44
|
+
[Features] [Pricing] [About] [Sign up] …scroll… [Get started]
|
|
45
|
+
|
|
46
|
+
Good: Your name as a folk poem
|
|
47
|
+
Type a name. Get a 4-line Romanian folk poem in 10 seconds.
|
|
48
|
+
[ Name: _________ ]
|
|
49
|
+
[ Get my poem: 2 TON ]
|
|
50
|
+
"Ana, floare de la munte, / …" ← one real example
|
|
51
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Run it, put it online
|
|
2
|
+
|
|
3
|
+
## `ambasada dev`: everything on your laptop, fake money
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx ambasada dev
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Starts two things:
|
|
10
|
+
|
|
11
|
+
1. a **mock Village** on `http://127.0.0.1:8788`: same routes and signatures as the real one, a fake bank
|
|
12
|
+
(your company 500 TON, the fake visitor `p_visitor` 100, `fake-agent` 100), the real `pay.js`, and an
|
|
13
|
+
LLM that answers `[mock] …`. Open it in a browser for a dashboard: wallets, offerings, calls.
|
|
14
|
+
2. **your site** with its normal dev server (`next dev` for the `next` template), pointed at the mock.
|
|
15
|
+
|
|
16
|
+
It registers your offerings with the mock. No login, no real TON.
|
|
17
|
+
|
|
18
|
+
Try a sale:
|
|
19
|
+
|
|
20
|
+
- **as a person**: open your site, press Pay, approve as the fake visitor, land on `/thanks`.
|
|
21
|
+
- **as an agent**: `npx ambasada dev buy translate '{"text":"Bună!","to":"en"}'` (the fake agent pays 1 TON).
|
|
22
|
+
|
|
23
|
+
`npx ambasada dev --mock-only` starts only the mock, if you run your site yourself. The mock's keys:
|
|
24
|
+
`AMBASADA_API_URL=http://127.0.0.1:8788`, `AMBASADA_API_KEY=sk_test_dev` (already in the project's `.env`).
|
|
25
|
+
More on the mock: the ambasada-payments skill, `reference/test-mode.md`.
|
|
26
|
+
|
|
27
|
+
## `ambasada deploy`: online at `<slug>.hackaton.ambasada.id`
|
|
28
|
+
|
|
29
|
+
Once, per laptop:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx ambasada login # opens a page: sign in with Ambasada ID, pick your company
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(If it asks for a key instead: the Village app → your team → Developers → Server keys, `sk_live_…`.)
|
|
36
|
+
|
|
37
|
+
Then, every time you want the new version online:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx ambasada deploy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It does, in order:
|
|
44
|
+
|
|
45
|
+
1. **build**: `opennextjs-cloudflare build` (Next), `vite build` (spa) or a bundle of the Worker (worker);
|
|
46
|
+
2. **smoke test**: runs the built Worker locally in workerd and requests `/` and `/api/receipt`;
|
|
47
|
+
a crash stops the deploy (skip with `--no-smoke` only if you know why it fails);
|
|
48
|
+
3. **upload** to the Village's hosting: the URL is `https://<your-company-slug>.hackaton.ambasada.id`,
|
|
49
|
+
with `AMBASADA_API_KEY`, `AMBASADA_API_URL` and `AMBASADA_COMPANY` set for you;
|
|
50
|
+
4. **agent and offerings**: pushes `agent/agent.md` + `agent/settings.json`, and lists your offerings at the new URL.
|
|
51
|
+
|
|
52
|
+
No Cloudflare account needed. Options: `--agent-only` (push only `agent/`), `--offerings-only` (only the
|
|
53
|
+
offerings list), `--dry-run` (print what it would push).
|
|
54
|
+
|
|
55
|
+
After the first deploy, point the webhook at it and deploy again so the Worker gets the secret:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx ambasada webhook set https://<slug>.hackaton.ambasada.id/api/webhook
|
|
59
|
+
npx ambasada webhook test # should print "delivered"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Limits on Village hosting: 10 MB compressed bundle, 1000 static files, short CPU time per request
|
|
63
|
+
(waiting on `fetch` or the LLM does not count). Heavy work belongs in an API you call, not in the Worker.
|
|
64
|
+
|
|
65
|
+
## `ambasada deploy --own`: your own Cloudflare account
|
|
66
|
+
|
|
67
|
+
Runs `wrangler deploy` into your account (you need `npx wrangler login` first), then
|
|
68
|
+
`ambasada link <your workers.dev URL>`. Set the secrets yourself:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx wrangler secret put AMBASADA_API_KEY
|
|
72
|
+
npx wrangler secret put AMBASADA_COMPANY
|
|
73
|
+
npx wrangler secret put AMBASADA_WEBHOOK_SECRET
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## `ambasada link`: the product is hosted somewhere else
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
npx ambasada link https://my-thing.lovable.app
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Sets `product.url` in `ambasada.config.ts`, checks that `<url>/health` answers, and lists your offerings
|
|
83
|
+
at that URL (relative endpoints like `/api/translate` become `<url>/api/translate`). Use it for Lovable,
|
|
84
|
+
Bolt, v0, Vercel, your own server. See [build-elsewhere.md](build-elsewhere.md).
|
|
85
|
+
|
|
86
|
+
## Watch it run
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx ambasada wallet # balance, held, available
|
|
90
|
+
npx ambasada ledger # every TON in and out
|
|
91
|
+
npx ambasada logs --agent --follow # what your agent thinks and does
|
|
92
|
+
npx ambasada steer "raise translate to 1.2 TON for 2 hours"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## When it goes wrong
|
|
96
|
+
|
|
97
|
+
| Symptom | Fix |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `deploy` prints "Product hosting is not part of `ambasada deploy` yet" | your CLI is old: `npm i -D @ambasada/cli@latest` |
|
|
100
|
+
| build fails on `fs`, `child_process`, a `.node` file, `sharp` | a banned package: see [stack.md](stack.md#nextjs-allowed-and-banned) |
|
|
101
|
+
| smoke test: `/` answers 500 | open `npx ambasada dev`, load `/`, read the terminal error |
|
|
102
|
+
| `CONFIG: no API key` | missing `AMBASADA_API_KEY`: `.env` locally, `ambasada login` before deploying |
|
|
103
|
+
| Pay works, nothing delivered | webhook not set or wrong secret: `ambasada webhook test`, then deploy again |
|
|
104
|
+
| agents can't buy | `ambasada offerings list`: endpoint must be your public URL, not `localhost` |
|
|
105
|
+
| `402` when you call your own offering | right: offerings only answer calls that come through the Gateway with a receipt; test with `ambasada dev buy` |
|