Metered Buyer
The endpoint answers unpaid POSTs with a 402 and an MPP challenge; choreographers sends AlphaUSD on chain 42431 and the server checks the receipt before delivering anything.
The kernel.
An agent runs the protocol server-side and choreographers get a signed, auditable result they can act on without leaving the app.
Why this primitiveTempo MPP on Tempo Moderato fits facility management in Dance & Choreography because choreographers can settle 0.05 AlphaUSD via a TIP-20 transfer with an on-chain Transfer receipt before the buyer endpoint returns anything.
Required key.
Add this in your Lovable project under Settings → Secrets before pasting the prompt below.
The build prompt.
Paste into a fresh Lovable project. Make sure the key above is set first. read the build strategy →
Build "Metered Buyer · mpp" as a ONE-SHOT Lovable build. The participant has only
5 credits — this single message must produce a working demo with no follow-ups.
Single-page TanStack Start app. Cut scope ruthlessly.
CONCEPT
Lovable AI runs the agent that purchases specialized dance flooring via UCP so studios safely upgrade rehearsal spaces.
Reframed as a paid, agent-callable HTTP endpoint on Tempo Moderato MPP (0.05 AlphaUSD via TIP-20 transfer + pay-receipt-effect verification).
Discipline: Dance & Choreography (facility management).
LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
The participant has FIVE Lovable credits for the whole build. This prompt MUST
ship a working demo on the FIRST message with zero follow-ups. Engineer for that.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No extra pages, no auth, no nav.
- ONE TanStack server route under `src/routes/api/public/` that runs the protocol.
- ONE client surface (a button + flow log) that triggers the payment and renders the result.
- NO database, NO Lovable Cloud, NO extra integrations beyond what's listed below.
- NO tests, NO docs pages, NO settings screens.
- Demo-fallback contract: every code path must boot with NO secrets and return
realistic simulated envelopes flagged `simulated: true`. Real signing activates
only when the user signs in and the wallet is funded.
PROTOCOL SURFACE — MPP on Tempo Moderato (self-contained; participant has no skills)
CONFIG (src/lib/mpp/config.ts) — copy exactly:
```ts
export const MPP_RECIPIENT = process.env.MPP_RECIPIENT ?? "0x0000000000000000000000000000000000000000";
export const TEMPO_CHAIN_ID = 42431;
export const TEMPO_RPC_URL = "https://rpc.moderato.tempo.xyz";
export const TEMPO_EXPLORER = "https://explore.testnet.tempo.xyz";
export const TEMPO_FAUCET_URL = "https://faucet.moderato.tempo.xyz";
export const ALPHAUSD_ADDRESS = "0x20c0000000000000000000000000000000000001" as const;
export const CHARGE_AMOUNT_USD = "0.05";
export const CHARGE_AMOUNT_UNITS = 50_000n; // 0.05 * 10^6 (AlphaUSD is 6 decimals)
```
TEMPO MODERATO GOTCHAS — get these wrong and it silently fails:
1. Explorer path is `/receipt/{hash}`, NOT `/tx/{hash}`. Every UI link and every
server response uses `${TEMPO_EXPLORER}/receipt/${txHash}`. `/tx/…` renders
"Page Not Found" with no redirect on Tempo's explorer.
2. Tempo DISALLOWS native value transfers. `sendTransaction({ to, value })` with
any non-zero `value` reverts. All AlphaUSD payments move as TIP-20
`transfer(to, amount)` calls on the AlphaUSD contract:
```ts
import { encodeFunctionData, parseAbi } from "viem";
const data = encodeFunctionData({
abi: parseAbi(["function transfer(address to, uint256 amount)"]),
functionName: "transfer",
args: [MPP_RECIPIENT, CHARGE_AMOUNT_UNITS],
});
const txHash = await provider.sendTransaction({
to: ALPHAUSD_ADDRESS, // TOKEN address, not recipient
data,
value: 0n, // MUST be zero
});
```
3. Server verification MUST check ALL FIVE in this order — missing any is a
silent-accept bug:
(a) `receipt.status === "success"`
(b) `tx.chainId === 42431`
(c) `tx.to.toLowerCase() === ALPHAUSD_ADDRESS`
(d) `tx.from.toLowerCase() === expectedPayer.toLowerCase()` ← replay guard
(e) Receipt has a `Transfer(from, to, value)` log emitted by `ALPHAUSD_ADDRESS`
with `args.to === MPP_RECIPIENT` and `args.value >= CHARGE_AMOUNT_UNITS`.
Use viem's `parseEventLogs` + `parseAbi(["event Transfer(address indexed from, address indexed to, uint256 value)"])`
and filter by `log.address === ALPHAUSD_ADDRESS`. Missing (d) lets anyone
replay someone else's payment tx.
4. Privy dashboard: enable "Allow transactions from the client" for Tempo Testnet
under Chains. Without it, `sendTransaction` HANGS FOREVER with no error.
Wrap the call in a 45s `Promise.race` timeout whose rejection message names
this exact dashboard setting.
5. Privy chain config MUST be a real viem `defineChain`, NOT a `{id, name} as never`
stub — the stub compiles but breaks embedded-wallet transport init. Define
once and pass to BOTH `defaultChain` and `supportedChains`:
```ts
import { defineChain } from "viem";
export const tempoModerato = defineChain({
id: 42431,
name: "Tempo Moderato",
nativeCurrency: { name: "AlphaUSD", symbol: "USD", decimals: 6 },
rpcUrls: { default: { http: [TEMPO_RPC_URL] } },
blockExplorers: { default: { name: "Tempo Explorer", url: TEMPO_EXPLORER } },
});
```
6. Privy mount is SSR-forbidden. `@privy-io/react-auth` explodes under SSR —
load it via `lazy(() => import("./privy-client-entry"))` inside
`<ClientOnly>` + `<Suspense>` in `src/components/privy-root.tsx`. NEVER
import the Privy package at the module scope of a route file.
PAY → RECEIPT → EFFECT (the entire trust story):
SERVER ROUTE (src/routes/api/public/mpp-charge.ts) — the side effect ONLY runs
after receipt verification. Preserve HTTP 402 + `WWW-Authenticate` end-to-end.
Unpaid POST (no `X-Payment-Tx` header) — return the 402 challenge:
```
HTTP/1.1 402 Payment Required
WWW-Authenticate: MPP intent="charge", chainId=42431, recipient="0x…", amount="0.05", currency="USD", asset="native"
Content-Type: application/json
{ "scheme": "MPP", "intent": "charge", "network": "tempo-moderato",
"recipient": MPP_RECIPIENT, "chainId": 42431, "asset": "native",
"amount": "0.05", "amountUnits": "50000", "currency": "USD",
"description": "Unlock the facility management pack" }
```
Ship BOTH the `WWW-Authenticate` header AND the JSON body — mppx clients need
the header to retry-with-payment; skipping it makes the endpoint useless.
NEVER map 402 → 500. `amount` is human-readable ("0.05"); `amountUnits` is the
atomic string ("50000"). Servers verify against `amountUnits`; UIs display `amount`.
Paid POST (`X-Payment-Tx: 0x…` + `X-Payer: 0x…`) — verify then execute:
```ts
import { createPublicClient, http, parseAbi, parseEventLogs } from "viem";
import { tempoModerato, ALPHAUSD_ADDRESS, MPP_RECIPIENT, CHARGE_AMOUNT_UNITS, TEMPO_CHAIN_ID } from "@/lib/mpp/config";
const client = createPublicClient({ chain: tempoModerato, transport: http(TEMPO_RPC_URL) });
const [tx, receipt] = await Promise.all([
client.getTransaction({ hash: txHash }),
client.getTransactionReceipt({ hash: txHash }),
]);
if (receipt.status !== "success") return json402("tx_not_success");
if (tx.chainId !== TEMPO_CHAIN_ID) return json402("wrong_chain");
if (tx.to?.toLowerCase() !== ALPHAUSD_ADDRESS.toLowerCase()) return json402("wrong_token");
if (tx.from.toLowerCase() !== expectedPayer.toLowerCase()) return json402("payer_mismatch");
const transfers = parseEventLogs({
abi: parseAbi(["event Transfer(address indexed from, address indexed to, uint256 value)"]),
logs: receipt.logs,
}).filter((l) => l.address.toLowerCase() === ALPHAUSD_ADDRESS.toLowerCase());
const paid = transfers.some((l) =>
l.args.to.toLowerCase() === MPP_RECIPIENT.toLowerCase() &&
l.args.value >= CHARGE_AMOUNT_UNITS,
);
if (!paid) return json402("insufficient_payment");
// side effect happens ONLY past this point
return Response.json({
ok: true,
txHash,
receipt: `${TEMPO_EXPLORER}/receipt/${txHash}`,
unlocked: { /* the deliverable for facility management */ },
});
```
USER FLOW (the entire app):
1. Sign In (email/google) — Privy creates an embedded EVM wallet on Tempo Moderato.
2. Show the wallet address + `TEMPO_FAUCET_URL` link.
3. "Pay & unlock" button — POSTs to `/api/public/mpp-charge`, catches the 402,
parses the challenge, calls the TIP-20 transfer, then re-POSTs with
`X-Payment-Tx` + `X-Payer` headers. Show a live log: Challenge · Sign
Transfer · Verify · Unlock.
4. On success, render the unlocked deliverable + a link to
`${TEMPO_EXPLORER}/receipt/${txHash}`.
SIMULATE MODE (default when signed out): the flow walks the four steps with a
fake `0x…` tx hash flagged `simulated: true`, no real chain call, no signing
prompt. The button label switches from "Pay & unlock (simulate)" to
"Pay & unlock (live · Tempo Moderato)" once the wallet is ready and funded.
STACK
- TanStack Start app, the index route only.
- Tailwind + shadcn. Editorial look: gold accent on a dark or warm-cream background,
generous type, one strong headline, one primary action.
- Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14".
- Libraries: template defaults + `@privy-io/react-auth` + `viem` + `zod`. Nothing else.
CREDIT (UI footer + JSDoc on the server handler): Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Market sizing.
Indicative figures for hackathon pitches — refine with your own research before raising.