| name | fly-sprites |
| description | Launch and manage fly.io Sprites (sprites.dev sandboxes) from a Lovable app via the Sprites REST API — creating sprites, writing files, running a Python HTTP server, and warming URLs for Discord verification and similar wake-on-request use cases. |
fly.io Sprites (sprites.dev) from Lovable
Sprites are fly.io micro-sandboxes managed via https://api.sprites.dev/v1. This skill captures the endpoints/headers that actually work — the docs are ambiguous in several places and each mistake costs a full round trip.
Use TanStack createServerFn for every call (never from the browser — the token is a bearer secret). Reference implementation lives at references/rest-api.md, references/deploy-flow.md, and references/gotchas.md.
Invariants (memorize)
- Token shape.
SPRITES_TOKEN must be the 4-part org-slug/org-id/token-id/token-value value copied from https://sprites.dev/account. A raw Fly.io org token → 401 authentication failed. Never prompt for a Fly token; always send the user to sprites.dev/account.
- Base URL & auth.
https://api.sprites.dev/v1, Authorization: Bearer <SPRITES_TOKEN>. Do NOT add Accept: application/octet-stream to exec calls — that returns 406 Not Acceptable.
- Create is POST-only.
POST /sprites with { name, url_settings: { auth: "public" } }. PUT /sprites/{name} returns 404 (create-via-PUT is not supported). The public URL comes back in the response body.
- Services are named, PUT-addressed.
PUT /sprites/{name}/services/{service} with { cmd, args, dir, needs: [], http_port }. POST /sprites/{name}/services returns 405. http_port is REQUIRED for wake-on-request — omit it and the sprite shows "Running" but every request 502s.
- Serve from
/root/www. /home/sprite may not exist and the service fails to start with cd: No such file or directory. Root's home always exists.
- Discord's Spritebot times out at ~10s. Every launch flow must warm-poll the public URL before handing it to the user.
Minimal working recipe
const API = "https://api.sprites.dev/v1";
const auth = () => ({ Authorization: `Bearer ${process.env.SPRITES_TOKEN!}` });
await fetch(`${API}/sprites`, {
method: "POST",
headers: { ...auth(), "Content-Type": "application/json" },
body: JSON.stringify({ name, url_settings: { auth: "public" } }),
});
await fetch(`${API}/sprites/${name}/fs/write?path=/root/www/index.html&workingDir=/`, {
method: "PUT",
headers: { ...auth(), "Content-Type": "application/octet-stream" },
body: html,
});
await fetch(`${API}/sprites/${name}/services/webapp`, { method: "DELETE", headers: auth() });
(, {
: ,
: { ...(), : },
: .({
: , : [, , ],
: , : [], : ,
}),
});
(, {
: ,
: { ...(), : },
});
Exec over HTTP (works despite docs implying WS-only)
const qs = new URLSearchParams();
qs.append("cmd", "bash"); qs.append("cmd", "-lc"); qs.append("cmd", script);
const res = await fetch(`${API}/sprites/${name}/exec?${qs}`, {
method: "POST",
headers: auth(),
});
const bytes = new Uint8Array(await res.arrayBuffer());
const exitCode = bytes.length >= 2 && bytes[bytes.length - 2] === 3 ? bytes[bytes.length - 1] : null;
const stdout = new TextDecoder().decode(exitCode === null ? bytes : bytes.slice(0, -2));
Prefer PUT /fs/write for file placement — it creates parents and avoids exec entirely.
When something breaks
Read references/gotchas.md first; nearly every failure in this project mapped to one entry there.