AI
maypop.ai gives an app AI models without an API key. Calls run as the
person using the app and draw on their Maypop credits, so a popular app
costs its creator nothing. Anonymous visitors can’t use AI; signing in
unlocks it.
| You want to… | Use |
|---|---|
| Write text, answer, summarise, plan, roleplay | chat / stream |
| Classify, route, gate, score — get a label or yes/no | decide |
| Make or edit a picture | image |
| Make a video clip | video |
| Make speech, music or sound effects | audio |
| Turn speech into text | transcribe |
| An assistant that acts on the app’s data | maypop.agent |
interface MaypopAi { models(): Promise<{ data: Array<{ id: MaypopAiModel; description: string }> }>; stream( request: MaypopAiRequest, onDelta: (text: string, event: Record<string, unknown>) => void, ): Promise<string>; chat(request: MaypopAiRequest): Promise<Record<string, unknown>>; decide(request: MaypopAiDecisionRequest): Promise<MaypopAiDecisionResponse>; image(request: MaypopAiImageRequest): Promise<{ data: MaypopAiImage[]; [key: string]: unknown }>; audio(request: MaypopAiAudioRequest): Promise<MaypopAiAudioResult>; video(request: MaypopAiVideoRequest): Promise<MaypopAiVideoResult>; transcribe(request: MaypopAiTranscriptionRequest): Promise<{ text: string; [key: string]: unknown }>;}Models
Section titled “Models”You pick a tier, not a specific model. Maypop maps each tier to a strong model and upgrades it over time, so choose by the shape of the task:
| Tier | Best for | Example |
|---|---|---|
"fast" |
Small, quick, high-volume calls: short rewrites, pulling out a field, naming things, suggestions | an RPG suggests character names |
"smart" |
Real reasoning, creativity, long coherent output, where quality matters more than speed | the RPG’s game master narrating the world |
maypop.ai.models() lists the tiers with these descriptions, handy for a
model picker. Apps can’t request a raw model name. For prices, see
Models and costs.
Requests follow the familiar OpenAI chat-completions format; common fields
such as temperature, tools and response_format pass through.
type MaypopAiModel = "fast" | "smart";
interface MaypopAiMessage { role: "system" | "user" | "assistant" | "tool"; content: string | MaypopAiContentPart[];}
type MaypopAiContentPart = | { type: "text"; text: string } | { type: "image_url"; image_url: { url: string } };
interface MaypopAiRequest { model: MaypopAiModel; messages: MaypopAiMessage[]; [key: string]: unknown;}await maypop.ready();
let reply = "";await maypop.ai.stream( { model: "smart", messages: [ { role: "system", content: "You suggest one recipe from the pantry list." }, { role: "user", content: pantry.join(", ") }, ], }, (text) => { reply += text; output.textContent = reply; },);chatreturns the whole response;streamcallsonDeltawith each piece and resolves to the full text.- Vision: add
image_urlparts to a message to send pictures. - Live web: when a question needs current information — news, prices,
scores — Maypop searches the web for the model automatically. Sources come
back as
url_citationannotations on the message. - JSON output: ask for it in the prompt, but models sometimes wrap JSON
in a code fence. Strip fences and
JSON.parseinsidetry. If all you want is a label, a yes/no or a score, usedecideinstead.
Decisions
Section titled “Decisions”decide asks a decision model one or more closed questions about some
state and returns calibrated probabilities — never text. Use it to classify
input, route a message to a screen, gate a risky action, triage, score
sentiment or quality, or check for duplicates. It answers in well under a
second, costs a small fraction of a chat call, and there’s nothing to parse.
interface MaypopAiDecisionRequest { state: string | Record<string, unknown> | unknown[]; questions: Record<string, MaypopAiDecisionQuestion>; session_id?: string; [key: string]: unknown;}
type MaypopAiDecisionQuestion = | { type: "noul"; instructions: Instructions; criteria: { true: Instructions; false: Instructions } } | { type: "choice"; instructions: Instructions; criteria: Record<string, Instructions> } | { type: "score"; instructions: Instructions; criteria: string[] };
type MaypopAiDecisionAnswer = | { type: "noul"; noul: number } | { type: "choice"; choice: string; confidence?: number; probabilities?: Record<string, number> } | { type: "score"; score: number; confidence?: number; probabilities?: Record<string, number>; legend?: Record<string, string> };
interface MaypopAiDecisionResponse { id: string; model: string; answers: Record<string, MaypopAiDecisionAnswer>; usage: { input_tokens: number; output_tokens: number; cost: number }; [key: string]: unknown;}// Instructions: a string, or structured JSON.Three question types:
noul— yes or no. The answer is the probability of “true”.choice— pick one option. Include a catch-all (“other”) for input that fits none.score— a position on an ordered scale, lowest level first.
const { answers } = await maypop.ai.decide({ state: { message: input.value, screen: currentScreen }, questions: { route: { type: "choice", instructions: "Which screen should handle this message?", criteria: { inventory: "asks about items, gear, or stock", quests: "asks about quests, goals, or objectives", chat: "anything else — small talk, questions for the narrator", }, }, destructive: { type: "noul", instructions: "Does the user want to delete or reset data?", criteria: { true: "an explicit request to remove, wipe, or start over", false: "no removal intent", }, }, },});
if (answers.destructive.noul > 0.35) return confirmDelete();navigate(answers.route.choice);- Each question is answered independently and comes back under its key.
- Describe each option as it would appear in the state (“an explicit request to remove data”), not as a bare label — the criteria matter more than the instructions.
- Set thresholds by the cost of a mistake, not at 0.5: confirm
something irreversible at a low probability (0.2–0.4); auto-apply an
undoable label only when it’s high (0.8+). When the top two
choiceprobabilities are close, show both. statecan be up to about 32,000 tokens. Pass structured JSON as JSON.decidecan’t write. Decide first (which screen? is it safe?), then generate withstream.- Decisions are new: the fields above are stable, and responses may gain optional fields.
Images
Section titled “Images”type MaypopAiImageRequest = MaypopAiFastImageRequest | MaypopAiQualityImageRequest;
interface MaypopAiImageOptions { prompt: string; image?: string | string[]; response_format?: "url" | "b64_json"; [key: string]: unknown;}
interface MaypopAiFastImageRequest extends MaypopAiImageOptions { tier?: "fast"; size?: "2K" | "3K" | "4K" | `${number}x${number}`;}
interface MaypopAiQualityImageRequest extends MaypopAiImageOptions { tier: "quality"; size?: "2K" | `${number}x${number}`; n?: 1;}
interface MaypopAiImage { url?: string; b64_json?: string;}const { data } = await maypop.ai.image({ prompt: "A watercolour fox reading under a lamp", size: "2K",});img.src = data[0].url!;tier: "fast"(default) goes up to 4K;"quality"makes one carefully rendered 2K image.imagetakes one or up to 10 picture URLs to edit, restyle or combine.- Results are temporary. Save keepers to Drive.
interface MaypopAiVideoRequest { prompt: string; model?: "fast" | "quality"; duration?: number; resolution?: "480p" | "720p"; ratio?: "16:9" | "9:16" | "4:3" | "3:4" | "1:1" | "21:9"; images?: string[]; generate_audio?: boolean; seed?: number;}
interface MaypopAiVideoResult { video: string; mime_type: "video/mp4"; duration: number;}const { video } = await maypop.ai.video({ prompt: "Paper boats drifting down a rainy street, slow camera push-in", duration: 6, ratio: "9:16", generate_audio: true,});fast(default) runs 4–15 seconds,qualityup to 30; default 5.- Up to 9 reference
imagesto animate or keep a character consistent. - Describe the scene, the motion, the camera and the sound.
- Video takes a while — up to several minutes. Show progress and don’t start a second one.
interface MaypopAiAudioRequest { prompt: string; references?: MaypopAiAudioReference[]; audio_config?: { format?: "wav" | "mp3" | "pcm" | "ogg_opus"; sample_rate?: number; speech_rate?: number; loudness_rate?: number; pitch_rate?: number; enable_subtitle?: boolean; };}
interface MaypopAiAudioResult { audio: string; url?: string; duration?: number; [key: string]: unknown;}const { audio } = await maypop.ai.audio({ prompt: "A calm narrator says: 'Your timer is done.' Soft chime after.", audio_config: { format: "mp3" },});- Speech, music, sound effects and ambience, up to 120 seconds; prompts up to 3,000 characters.
referencessteers the voice or style: a namedspeaker, up to three audio samples (audio_url/audio_data), or one image.
Transcription
Section titled “Transcription”interface MaypopAiTranscriptionRequest { audio: Blob | string; format?: string; language?: string; [key: string]: unknown;}const { text } = await maypop.ai.transcribe({ audio: recordingBlob });Errors
Section titled “Errors”| Code | What to do |
|---|---|
maypop/ai-limit |
The person is out of credits. Say so — never auto-retry. |
maypop/sign-in-required |
An anonymous visitor. Offer Sign in to ask with maypop.signIn(). |
maypop/ai-blocked |
Moderation refused a generation. Change the prompt. |
maypop/forbidden |
This viewer has view-only access. |
models() never asks for sign-in: listing what could run isn’t running it.
Every call uses the signed-in person’s credits: chat by tokens, decisions by input only, media per image or per second. Rates are on Models and costs.