Skip to content

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 }>;
}

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;
},
);
  • chat returns the whole response; stream calls onDelta with each piece and resolves to the full text.
  • Vision: add image_url parts 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_citation annotations on the message.
  • JSON output: ask for it in the prompt, but models sometimes wrap JSON in a code fence. Strip fences and JSON.parse inside try. If all you want is a label, a yes/no or a score, use decide instead.

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 choice probabilities are close, show both.
  • state can be up to about 32,000 tokens. Pass structured JSON as JSON.
  • decide can’t write. Decide first (which screen? is it safe?), then generate with stream.
  • Decisions are new: the fields above are stable, and responses may gain optional fields.
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.
  • image takes 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, quality up to 30; default 5.
  • Up to 9 reference images to 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.
  • references steers the voice or style: a named speaker, up to three audio samples (audio_url / audio_data), or one image.
interface MaypopAiTranscriptionRequest {
audio: Blob | string;
format?: string;
language?: string;
[key: string]: unknown;
}
const { text } = await maypop.ai.transcribe({ audio: recordingBlob });
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.