Publish with an agent
A coding agent or a chat assistant can put a space on Velven for its creator. There are three ways in. A coding agent publishes the build with the Velven CLI. An assistant in a chat publishes through Velven's MCP server. A space that's already live on another host is listed through the API. Every time, the creator approves the sign-in in their browser, so nobody copies a key by hand.
Choose a way in
| You are | Use |
|---|---|
| A coding agent with a terminal, and the space is a folder or a build | The Velven CLI: npx @velven/cli publish. |
| An assistant in a chat, such as Claude or ChatGPT | Velven's MCP server, with one page or a few files. |
| Any agent, and the space is already live on one of the 7 supported hosts | The listing API, in the steps below. |
- You are
- A coding agent with a terminal, and the space is a folder or a build
- Use
- The Velven CLI:
npx @velven/cli publish.
- You are
- An assistant in a chat, such as Claude or ChatGPT
- Use
- Velven's MCP server, with one page or a few files.
- You are
- Any agent, and the space is already live on one of the 7 supported hosts
- Use
- The listing API, in the steps below.
Ask the creator for the space's title and the devices it works on, or take them from what they've already told you. Don't guess. These are what people see on the listing.
Publish with the CLI
Run publish with two flags: --json, so it prints one JSON object, and --yes, so it never stops at a prompt. Every flag and exit code is in the CLI reference.
# prints a code and a link: show both to the creatornpx @velven/cli loginnpx @velven/cli publish ./dist --title "Orbit Dodger" --devices desktop,mobile --yes --json# when the creator asks to go livenpx @velven/cli publish ./dist --prod --yes --wait --jsonloginprints a code and an approval link, then waits for the creator to approve. Show them both. The token is saved in~/.config/velven/auth.jsonand used for this creator's later publishes.- The first publish writes
velven.jsoninto the folder, with"space"in it. Keep the file: it's how later publishes update the same space. - A plain publish answers
status: "preview"and apreviewUrl. Show the creator the preview. --produsually answersstatus: "live"withpageUrlright away. Or it answersrefused, with areasonthat names the file, and exits 6. Sometimes a version waits on Velven instead, usually under a minute. Then it answersstatus: "publishing"and awatchUrl, where the creator can follow it. With--wait, the CLI waits for it, up to 20 minutes. It then answerslivewithpageUrl. Or it answersrefused, orunjudgedwhen Velven could not tell what the page shows, withreasonandshownOnTake(what Velven saw on the page), and exits 6.- For
unjudged, add astarthint tovelven.json, such as"start": { "click": "Play" }, and publish again. Forrefused, tell the creator the reason. They can press Ask for review atwatchUrl. The exception is a program or a miner, which is never served: the creator removes the file and publishes again.
Without login, the same publish makes an unlisted page. It's published the same way, but it has no preview. The answer has pageUrl, claimToken, claimUrl, expiresAt and terms. Show the creator all of them. Tell them two things: the page is deleted after 7 days unless they claim it, and publishing means they agree to Velven's Terms. To claim it, run login and publish again from the same folder.
Publish from a chat with the MCP server
Velven's MCP server is at https://mcp.velven.ai/mcp (Streamable HTTP). Add it to an assistant as a custom connector. For Claude Code, install the plugin or run one command:
claude mcp add --transport http velven https://mcp.velven.ai/mcp| Tool | What it does | Sign-in |
|---|---|---|
publish | Publishes one HTML page or a few files, as a new space or as a new version of space. You get a private preview by default. With prod: true, the version goes live. | Optional |
my_spaces | Lists the account's spaces, newest first, up to 100. Each has slug, title, status, hosted and pageUrl. | Needed |
versions | Lists the versions of the hosted space in space, newest first. Each has its number and status, the reason if it was refused, and rollback: whether it can go live again. | Needed |
rollback | Puts version of space live again, right away. It only works for a version that was live before. | Needed |
search_docs | Searches these docs for query and answers the best sections, with links. | None |
- Tool
publish- What it does
- Publishes one HTML page or a few files, as a new space or as a new version of
space. You get a private preview by default. Withprod: true, the version goes live. - Sign-in
- Optional
- Tool
my_spaces- What it does
- Lists the account's spaces, newest first, up to 100. Each has
slug,title,status,hostedandpageUrl. - Sign-in
- Needed
- Tool
versions- What it does
- Lists the versions of the hosted space in
space, newest first. Each has itsnumberandstatus, thereasonif it was refused, androllback: whether it can go live again. - Sign-in
- Needed
- Tool
rollback- What it does
- Puts
versionofspacelive again, right away. It only works for a version that was live before. - Sign-in
- Needed
- Tool
search_docs- What it does
- Searches these docs for
queryand answers the best sections, with links. - Sign-in
- None
The publish tool's inputs
htmlstring- A whole page as one HTML string. It's published as
index.html. Send this orfiles. filesobject- Files by relative path. Send text as a string and a binary file as
{ "base64": "…" }. Up to 500 files and 3 MB in all. Include the entry page. entrystringDefaultindex.html- The page that opens first.
titlestring, up to 80 characters- Required for a new space.
devicesa list ofdesktop,mobile,vr- Required for a new space.
descriptionstring, up to 160 characters- One line, shown under the name.
enginestring- One of the allowed engines.
spacestring- The slug of the creator's space, to publish a new version of it. Needs sign-in.
claimstring- The claim token of an unlisted page. Publishes the page's next version. If you're signed in, it also claims the page.
prodbooleanDefaultfalse- Publish the version live on its Velven page instead of making a preview. Needs sign-in.
spabooleanDefaultfalse- Serve the entry page for any path that has no file. Use it for client-side routing.
sdkbooleanDefaulttrue- Add the Velven SDK's script tag to the entry page.
thumbnailstring- The picture on the space's tile: the path of one of the files. Use a JPEG, PNG or WebP, up to 2 MB and 4096 pixels on each side.
clipstring- The video the tile plays on hover, without sound: the path of one of the files. Use an MP4 or WebM, up to 3 MB and 5 to 10 seconds long. When you send one, Velven doesn't record a clip. Without a
thumbnail, the clip's first frame becomes the picture. Both must show the space itself.
Some files are always left out and never published: a velven.json, dotfiles, dot-folders and node_modules, in any folder. The answer lists the first 10 in leftOut and counts them all in leftOutCount. Send velven.json's settings as the tool's inputs instead. The tool has no input for a start hint, leaderboards or achievements. For those, publish the folder with the CLI.
What publish answers
Every answer has space (the slug), version, status and pageUrl. A preview also has previewUrl, a private link that lasts 24 hours, and watchUrl, the version on the space's Files tab. With prod: true, the answer is usually status: "live", or refused with a reason. Sometimes a version waits on Velven instead and answers status: "publishing" and a watchUrl. It goes live or is refused, usually within a minute. Call versions to follow it.
When a tool needs an account, the app asks the creator to sign in to Velven and approve the connection in their browser. Without signing in, publish makes an unlisted page, published the same way. It answers pageUrl, claimToken, claimUrl, expiresAt and terms. Show the creator every one. Tell them to keep the token: it's the only way to update the page or keep it past 7 days. To publish the page's next version, publish again with claim set to the token. If the creator is signed in, the same call claims the page.
Note: Without signing in, prod: true answers sign_in_required, and a publish that names space makes the app ask the creator to sign in. An unlisted page has no previews: every publish to it goes live on the page.
Note: For a folder over 3 MB or 500 files, or when you have a terminal, use the CLI.
List a live URL with the API
If a space is already live on one of the 7 supported hosts, you can list it where it is, without uploading its files. It stays on its host. Velven plays it in a frame on its Velven page, records its clip and counts its plays. For a person on the add page, the same flow is Link a live URL.
The 5 steps below sign in as the creator, put the proof on the site, submit the listing and reply to the creator. The sign-in is the same device-code flow velven login runs, and it gives the same kind of token.
After the steps, the sections below use that token on the creator's spaces. They cover writing a space's page, asking for a new clip, adding leaderboards, claiming a space Velven listed and moving a space to a new address. Every other call is in the REST API.
Ask for a sign-in link
Make one call, with no token. agent_name is optional, up to 60 characters. The creator sees it on the approval screen.
curl -s -X POST https://velven.ai/api/agent/login \ -H "content-type: application/json" \ -d '{"agent_name":"Claude Code"}'The response, with status 201:
{ "device_code": "a3Vw…", "user_code": "K7PX-4MDQ", "verification_url": "https://velven.ai/agent/approve?code=K7PX-4MDQ", "expires_in": 900, "interval": 5}Keep device_code to yourself. Show the creator verification_url and user_code. The code lasts 15 minutes.
| Status | Body | Meaning |
|---|---|---|
| 400 | { "error": "invalid_ | The body wasn't JSON, or agent_name is over 60 characters. |
| 429 | { "error": "rate_ | This address asked for more than 20 sign-in links in 15 minutes. Wait. Don't retry in a loop. |
| 500 | { "error": "server_error" } | Something went wrong on Velven's side. Try again in a moment. |
- Status
- 400
- Body
{ "error": "invalid_request", "message" } - Meaning
- The body wasn't JSON, or
agent_nameis over 60 characters.
- Status
- 429
- Body
{ "error": "rate_limited", "message" } - Meaning
- This address asked for more than 20 sign-in links in 15 minutes. Wait. Don't retry in a loop.
- Status
- 500
- Body
{ "error": "server_error" }- Meaning
- Something went wrong on Velven's side. Try again in a moment.
Wait for the creator to approve
Tell the creator: "Open this link and press Approve. The code on the page should read K7PX-4MDQ." If they aren't signed in to Velven, the same screen signs them in with GitHub, Google or an email link, and asks them for a handle.
Meanwhile, poll for the token every interval seconds. Give up after expires_in seconds:
curl -s -X POST https://velven.ai/api/agent/token \ -H "content-type: application/json" \ -d '{"device_code":"<device_code>"}'| Status | Body | What to do |
|---|---|---|
| 200 | { "access_token", "token_ | Approved. Store the token. You get it once, and it lasts 90 days. |
| 428 | { "error": "authorization_ | Not decided yet. Wait interval seconds and poll again. |
| 403 | { "error": "access_denied" } | The creator pressed Deny. Stop and tell them. |
| 400 | { "error": "expired_token" } | 15 minutes passed, or the code is unknown. Start over from step 1. |
| 400 | { "error": "invalid_grant" } | This device code was already exchanged. Use the token you have, or start over. |
| 400 | { "error": "invalid_request" } | The body wasn't { "device_code": "…" }. Fix the request. |
| 5xx | { "error": "server_error" }, or no answer | Something went wrong on Velven's side. Keep polling until expires_in runs out. |
- Status
- 200
- Body
{ "access_token", "token_type": "bearer", "handle", "profile_url" } - What to do
- Approved. Store the token. You get it once, and it lasts 90 days.
- Status
- 428
- Body
{ "error": "authorization_pending", "interval": 5 } - What to do
- Not decided yet. Wait
intervalseconds and poll again.
- Status
- 403
- Body
{ "error": "access_denied" }- What to do
- The creator pressed Deny. Stop and tell them.
- Status
- 400
- Body
{ "error": "expired_token" }- What to do
- 15 minutes passed, or the code is unknown. Start over from step 1.
- Status
- 400
- Body
{ "error": "invalid_grant" }- What to do
- This device code was already exchanged. Use the token you have, or start over.
- Status
- 400
- Body
{ "error": "invalid_request" }- What to do
- The body wasn't
{ "device_code": "…" }. Fix the request.
- Status
- 5xx
- Body
{ "error": "server_error" }, or no answer- What to do
- Something went wrong on Velven's side. Keep polling until
expires_inruns out.
Store the token at ~/.config/velven/token with mode 600, and reuse it for this creator's later spaces. A token belongs to one creator. It works on the endpoints on this page and in the REST API, in the CLI as VELVEN_TOKEN, and on the MCP server as a bearer token. A token saved by velven login in ~/.config/velven/auth.json works here too.
If a call answers 401, the token expired, the creator revoked it in their settings, or the account is banned. Delete the file and sign in again.
To check who a token belongs to:
curl -s https://velven.ai/api/agent/me -H "authorization: Bearer $VELVEN_TOKEN"{ "handle": "mara", "profile_url": "https://velven.ai/mara" }Put the proof on the site
Only a verified creator can list a space, so the proof goes on the site before you submit. Add this tag to the page's <head>, using the handle from the token response. Then deploy again:
<meta name="velven" content="@handle">Wait until the deploy is live before you submit. If you submit too early, the answer is 409, with the change still needed. Velven lists spaces on Vercel, Netlify, GitHub Pages, Cloudflare, Replit, Firebase and ChatGPT sites. Linking a live URL has the details for each host, including the framing header a page must allow.
Note: Publish a ChatGPT site with "Who has access" set to "Anyone on the Internet". A site only its owner can open answers 401, and Velven can't verify it.
Submit the space
Fill in the details from the project instead of asking the creator. Take the engine from package.json and the imports, the AI tools from yourself, the devices from the pointer and touch handling, and the description from the README. Velven fetches the URL and checks the proof. Anything you leave out, it fills in from the page: the title, the meta description, the engine from its scripts and the AI tool from its markup. What you send always wins.
curl -s -X POST https://velven.ai/api/spaces \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d @- <<'JSON'{ "url": "https://orbit-dodger.netlify.app", "title": "Orbit Dodger", "description": "Dodge debris in a decaying orbit. Arrow keys or swipe.", "engine": "three.js", "models": ["claude-opus-5"], "ai_tools": ["claude-code"], "devices": ["desktop", "mobile"], "source_url": "https://github.com/mara/orbit-dodger"}JSON| Field | Required | Notes |
|---|---|---|
url | Yes | The space's public http(s) URL. Velven stores it after following redirects. |
devices | Yes | An array with at least one allowed value. |
title | No | Up to 80 characters. Defaults to the page's og:title or <title>. |
description | No | One line, up to 160 characters. Defaults to the page's og:description or meta description. |
engine | No | An allowed value. If you leave it out, Velven reads it from the page's scripts. Send null for none. |
models | No | An array of the models that wrote it. |
ai_tools | No | An array of the tools the model ran in. If you leave it out, Velven reads it from the page's markup. Send [] for none. |
source_url | No | The repository's URL. |
about, how_to_play, controls, features, faq, orientation, players | No | The text of the space's page. See Your space's page. |
- Field
url- Required
- Yes
- Notes
- The space's public http(s) URL. Velven stores it after following redirects.
- Field
devices- Required
- Yes
- Notes
- An array with at least one allowed value.
- Field
title- Required
- No
- Notes
- Up to 80 characters. Defaults to the page's
og:titleor<title>.
- Field
description- Required
- No
- Notes
- One line, up to 160 characters. Defaults to the page's
og:descriptionor meta description.
- Field
engine- Required
- No
- Notes
- An allowed value. If you leave it out, Velven reads it from the page's scripts. Send
nullfor none.
- Field
models- Required
- No
- Notes
- An array of the models that wrote it.
- Field
ai_tools- Required
- No
- Notes
- An array of the tools the model ran in. If you leave it out, Velven reads it from the page's markup. Send
[]for none.
- Field
source_url- Required
- No
- Notes
- The repository's URL.
- Field
about,how_to_play,controls,features,faq,orientation,players- Required
- No
- Notes
- The text of the space's page. See Your space's page.
The response, with status 201. The page exists right away on the creator's handle. While status is processing, only the creator can see it. It goes public as published a few minutes later, once Velven has recorded its clip and thumbnail.
{ "slug": "orbit-dodger", "url": "https://velven.ai/mara/orbit-dodger", "status": "processing", "title": "Orbit Dodger"}If the page carries a boards block, the answer also has boards, the keys Velven synced from it. Or it has boards_error, which says why Velven didn't take the block.
| Status | Body | Meaning |
|---|---|---|
| 401 | { "error": "unauthorized" } | The token is missing, revoked or unknown. Sign in again. |
| 403 | { "error": "blocked", "message" } | This URL can't be listed. Velven removed it before, it's on Velven's blocklist, or it's listed as malware or phishing. message says which. |
| 409 | { "error": "unverified", "message", "instruction", "snippet" } | The proof isn't on the live site yet. Make the change instruction describes, deploy, wait for the host's cache, and call again. Don't retry more than a few times without changing something. |
| 409 | { "error": "duplicate", "message", "url" } | It's already on Velven, and url is its page. If message says to claim it, claim it. |
| 422 | { "error": "invalid", "issues": [{ "path", "message" }] } | Fix the fields listed and retry. An issue on url can also mean Velven doesn't list that host. |
| 422 | { "error": "unreachable", "message", "instruction" } | The URL didn't answer (a timeout, a DNS failure, a refused connection), or it's closed to visitors (a Vercel deployment behind Vercel Authentication, or a site shared only with its owner). Check that the deployment is public and call again. instruction says how to open it, and comes only with a closed page. |
| 422 | { "error": "unframeable", "message", "instruction", "snippet" } | The site's headers don't let Velven frame it. Make the change instruction describes (see the steps for each host), deploy, and call again. |
| 500 | { "error": "server_ | Something went wrong on Velven's side. Retry once after a moment. If it keeps failing, tell the creator. |
- Status
- 401
- Body
{ "error": "unauthorized" }- Meaning
- The token is missing, revoked or unknown. Sign in again.
- Status
- 403
- Body
{ "error": "blocked", "message" }- Meaning
- This URL can't be listed. Velven removed it before, it's on Velven's blocklist, or it's listed as malware or phishing.
messagesays which.
- Status
- 409
- Body
{ "error": "unverified", "message", "instruction", "snippet" }- Meaning
- The proof isn't on the live site yet. Make the change
instructiondescribes, deploy, wait for the host's cache, and call again. Don't retry more than a few times without changing something.
- Status
- 409
- Body
{ "error": "duplicate", "message", "url" }- Meaning
- It's already on Velven, and
urlis its page. Ifmessagesays to claim it, claim it.
- Status
- 422
- Body
{ "error": "invalid", "issues": [{ "path", "message" }] }- Meaning
- Fix the fields listed and retry. An issue on
urlcan also mean Velven doesn't list that host.
- Status
- 422
- Body
{ "error": "unreachable", "message", "instruction" }- Meaning
- The URL didn't answer (a timeout, a DNS failure, a refused connection), or it's closed to visitors (a Vercel deployment behind Vercel Authentication, or a site shared only with its owner). Check that the deployment is public and call again.
instructionsays how to open it, and comes only with a closed page.
- Status
- 422
- Body
{ "error": "unframeable", "message", "instruction", "snippet" }- Meaning
- The site's headers don't let Velven frame it. Make the change
instructiondescribes (see the steps for each host), deploy, and call again.
- Status
- 500
- Body
{ "error": "server_error", "message" } - Meaning
- Something went wrong on Velven's side. Retry once after a moment. If it keeps failing, tell the creator.
Reply to the creator
Reply with the url, and offer the badge for the README:
<a href="https://velven.ai/mara/orbit-dodger"><img src="https://velven.ai/badge/orbit-dodger" alt="On Velven"></a>To see the creator's spaces, newest first, leaving out any Velven removed:
curl -s "https://velven.ai/api/spaces?mine=1" -H "authorization: Bearer $VELVEN_TOKEN"[{ "slug": "orbit-dodger", "title": "Orbit Dodger", "url": "https://orbit-dodger.netlify.app", "plays": 412, "upvotes": 18, "velven_url": "https://velven.ai/mara/orbit-dodger" }]Your space's page
Under the frame, the space's page says what the space is, how to play it and which keys do what. Velven writes this text from its own recording. But you know the project better, so write it from the code and send it with the listing, or change it later. Every field you send belongs to the creator. Velven's recording never writes over it, not even a field you cleared.
Take the controls from the input handlers (the key bindings, the pointer and touch events). Take the goal and the rules from the game logic, and the features from what the project actually does. Write plainly, in the second person, for someone about to play. If the space isn't a game, leave out how_to_play and controls when they don't apply.
| Field | Limit | Notes |
|---|---|---|
about | Up to 1500 characters | What the space is, in a paragraph or two. |
how_to_play | Up to 1000 characters | The goal and how to reach it. |
controls | Up to 4 groups, 12 rows in all | Groups of { "label", "rows": [{ "input", "action" }] }. label is optional, up to 40 characters. input is up to 40, such as W, Space or Mouse drag. action is up to 80. |
features | Up to 6 items, 100 characters each | Short lines on what sets the space apart. |
faq | Up to 8 pairs | { "q", "a" }: a question up to 150 characters and an answer up to 500. Only the creator writes this. Velven never does. |
orientation | One value | landscape, portrait, any: how the space is meant to be held on a phone. |
players | One value | single single player, multi multiplayer. |
- Field
about- Limit
- Up to 1500 characters
- Notes
- What the space is, in a paragraph or two.
- Field
how_to_play- Limit
- Up to 1000 characters
- Notes
- The goal and how to reach it.
- Field
controls- Limit
- Up to 4 groups, 12 rows in all
- Notes
- Groups of
{ "label", "rows": [{ "input", "action" }] }.labelis optional, up to 40 characters.inputis up to 40, such asW,SpaceorMouse drag.actionis up to 80.
- Field
features- Limit
- Up to 6 items, 100 characters each
- Notes
- Short lines on what sets the space apart.
- Field
faq- Limit
- Up to 8 pairs
- Notes
{ "q", "a" }: a question up to 150 characters and an answer up to 500. Only the creator writes this. Velven never does.
- Field
orientation- Limit
- One value
- Notes
landscape,portrait,any: how the space is meant to be held on a phone.
- Field
players- Limit
- One value
- Notes
singlesingle player,multimultiplayer.
In the body of POST /api/spaces, beside the other fields:
{ "about": "Orbit Dodger drops you into a decaying orbit full of debris. Every lap is faster than the last.", "how_to_play": "Stay alive as long as you can. Collect fuel cells to climb back to a higher orbit.", "controls": [ { "label": "Keyboard", "rows": [{ "input": "Left / Right", "action": "Thrust" }, { "input": "Space", "action": "Boost" }] }, { "label": "Touch", "rows": [{ "input": "Swipe", "action": "Thrust" }] } ], "features": ["Procedural debris field", "A new orbit every run"], "orientation": "landscape", "players": "single"}Sometimes the listing is made but its page text can't be saved. Then the 201 response carries "page_not_written": true. The space is still listed, without the text. Send the same fields again with PATCH /api/spaces/{slug}.
To read the page as it stands, call GET /api/spaces/{slug}. page_authors says who wrote each field: model for Velven, creator for you. To change the page, send any of these fields to PATCH /api/spaces/{slug}. You can also send any of the listing's own fields (title, description, engine, models, ai_tools, devices, source_url). Only the fields you send change, and null clears one. Both calls answer with the space's fields.
curl -s https://velven.ai/api/spaces/orbit-dodger -H "authorization: Bearer $VELVEN_TOKEN"curl -s -X PATCH https://velven.ai/api/spaces/orbit-dodger \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{ "features": ["Procedural debris field", "Daily seed"], "faq": null }'| Status | Body | Meaning |
|---|---|---|
| 200 | The space's fields, page_authors and velven_url | Read, or changed. |
| 401 | { "error": "unauthorized" } | The token is missing, revoked or unknown. Sign in again. |
| 404 | { "error": "not_found" } | No space with that slug on this account. |
| 422 | { "error": "invalid", "issues": [{ "path", "message" }] } | Fix the fields listed. A field that can't be changed is refused by name. To change url, move the space with POST /. |
| 500 | { "error": "server_ | Saving failed. The message says whether the page text was saved (page_written) while the other fields weren't. Send again whatever didn't land. |
- Status
- 200
- Body
- The space's fields,
page_authorsandvelven_url - Meaning
- Read, or changed.
- Status
- 401
- Body
{ "error": "unauthorized" }- Meaning
- The token is missing, revoked or unknown. Sign in again.
- Status
- 404
- Body
{ "error": "not_found" }- Meaning
- No space with that slug on this account.
- Status
- 422
- Body
{ "error": "invalid", "issues": [{ "path", "message" }] }- Meaning
- Fix the fields listed. A field that can't be changed is refused by name. To change
url, move the space withPOST /.api/ spaces/ {slug}/ move
- Status
- 500
- Body
{ "error": "server_error", "message" } - Meaning
- Saving failed. The message says whether the page text was saved (
page_written) while the other fields weren't. Send again whatever didn't land.
Keep the clip current
After listing, Velven plays the space in its own browser and records a 5-second clip. The clip's first frame becomes the thumbnail. When the site is rebuilt, Velven's background check notices the change and records again, at most once a day. A space published on Velven gets its clip after its first version goes live, unless its velven.json supplies one. A clip the creator uploaded is never replaced.
After a deploy that changes how the space looks, ask for a retake right away. note is optional: up to 100 characters on what the clip should show, such as a mode, a moment or something to avoid. The creator gets 2 retakes per space. The background check's own retakes don't count.
curl -s -X POST https://velven.ai/api/spaces/recapture \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{ "slug": "orbit-dodger", "note": "start after the title screen, night mode" }'| Status | Body | Meaning |
|---|---|---|
| 202 | { "slug", "status" } | Queued, or already queued or running. status says which. The clip lands within a few minutes. |
| 401 | { "error": "unauthorized" } | The token is missing, revoked or unknown. Sign in again. |
| 404 | { "error": "not_found" } | No published or processing space with that slug on this account. |
| 409 | { "error": "no_retakes_left" } | Both retakes are used. The background check still records again when the page changes. |
| 422 | { "error": "invalid", "issues" } | The body wasn't { slug, note? }, or note is over 100 characters. |
| 503 | { "error": "capture_off" } | Recording is paused on Velven's side. There's nothing to fix. Try later. |
- Status
- 202
- Body
{ "slug", "status" }- Meaning
- Queued, or already queued or running.
statussays which. The clip lands within a few minutes.
- Status
- 401
- Body
{ "error": "unauthorized" }- Meaning
- The token is missing, revoked or unknown. Sign in again.
- Status
- 404
- Body
{ "error": "not_found" }- Meaning
- No published or processing space with that slug on this account.
- Status
- 409
- Body
{ "error": "no_retakes_left" }- Meaning
- Both retakes are used. The background check still records again when the page changes.
- Status
- 422
- Body
{ "error": "invalid", "issues" }- Meaning
- The body wasn't
{ slug, note? }, ornoteis over 100 characters.
- Status
- 503
- Body
{ "error": "capture_off" }- Meaning
- Recording is paused on Velven's side. There's nothing to fix. Try later.
Leaderboards and sign-in
If the space keeps a score, give it a hosted leaderboard and sign-in with the Velven SDK. The SDK guide covers all of it, and https://velven.ai/docs/sdk.md is the same guide as one file for you to read. In short, load the script or install @velven/sdk. Declare the boards in a <script type="application/ block beside the proof tag, or in velven.json for a space published on Velven. Then post scores with Velven.scores.submit. The space draws its own boards from top and around. A board declared with display page or both also shows on its Velven page. Achievements are declared the same way.
<meta name="velven" content="@handle"><script type="application/velven+json">{"boards":[{"key":"main","trust":"client","metric":"points","sort":"desc"}]}</script><script src="https://velven.ai/sdk/v1.js"></script>Choose a tier for each board. trust: "server" is the default. It takes posts only from the space's own server, with the space's secret and the player's token. If the page posts to it, the answer is server_only. trust: "client" takes the page's posts within a range, a cooldown and caps. A space with no backend uses client, as in the snippet above. Server scores helps you choose.
A server board needs the secret. You issue it with the creator's token, and it's shown once. Put it in the host's server environment, never in the page:
curl -s -X POST https://velven.ai/api/spaces/<slug>/secret -H "authorization: Bearer $VELVEN_TOKEN"You can also declare or change boards without a deploy, in the same shape as the block. Only the boards you send are written. The page's next sync overwrites any key it also declares:
curl -s -X PUT https://velven.ai/api/spaces/<slug>/boards \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{"boards":[{"key":"main","trust":"client","metric":"points","sort":"desc","min":0,"max":100000,"cooldown":5}]}'To read the boards, see the REST API. To delete a submission or ban a player, see Moderation.
Claim a space Velven listed
Velven lists some spaces itself, credited to their creators, at /s/slug, each with a “Made this? Claim it” line on its page. A duplicate answer on submit can point at one. Once the proof is on the site, claim it for the creator. Its plays stay, and it moves to their handle.
An unlisted page published without an account is claimed with its token instead. Run velven login and publish again from the folder that holds its velven.json, or have the creator open the claim link.
curl -s -X POST https://velven.ai/api/spaces/verify \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{ "slug": "orbit-dodger" }'| Status | Body | Meaning |
|---|---|---|
| 200 | { "verified": true, "slug", "url" } | Done. url is now the creator's page, /handle/slug. |
| 403 | { "error": "blocked" } | This account can't claim spaces. Tell the creator and stop. |
| 404 | { "error": "not_found" } | No published space has that slug, or Velven hosts it. An unlisted page is claimed with its token. |
| 409 | { "error": "unverified", "instruction", "snippet" } | The proof isn't on the live site yet. Deploy, wait, and call again. |
| 409 | { "error": "owned" } | Someone else already verified it. Tell the creator. They can let Velven know with Feedback on the space's page. |
| 422 | { "error": "invalid", "issues" } | The body wasn't { slug }. |
- Status
- 200
- Body
{ "verified": true, "slug", "url" }- Meaning
- Done.
urlis now the creator's page,/handle/slug.
- Status
- 403
- Body
{ "error": "blocked" }- Meaning
- This account can't claim spaces. Tell the creator and stop.
- Status
- 404
- Body
{ "error": "not_found" }- Meaning
- No published space has that slug, or Velven hosts it. An unlisted page is claimed with its token.
- Status
- 409
- Body
{ "error": "unverified", "instruction", "snippet" }- Meaning
- The proof isn't on the live site yet. Deploy, wait, and call again.
- Status
- 409
- Body
{ "error": "owned" }- Meaning
- Someone else already verified it. Tell the creator. They can let Velven know with Feedback on the space's page.
- Status
- 422
- Body
{ "error": "invalid", "issues" }- Meaning
- The body wasn't
{ slug }.
Move to another host
When the space moves to a new URL, move its listing with it. Its slug, plays, likes, boards, scores, saves and page text all stay. Velven checks the new page the same way it checks a listing. So first put the proof tag on the new page and allow framing there. Deploy, then call:
curl -s -X POST https://velven.ai/api/spaces/orbit-dodger/move \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{ "url": "https://orbit-dodger.vercel.app" }'It answers with the space's fields, as GET /api/spaces/{slug} does, plus previous_url. Every refusal and its code is in the REST API.
A space published on Velven moves back to a live URL with the same call, and Velven stops hosting it. To move a linked space onto Velven's hosting instead, publish its files with the CLI. See Move a linked space onto Velven.
- Velven reads the boards block on the new page. If the old page declared a board and the new one doesn't, that board is withdrawn and its scores are kept.
- If a server posts scores, set the board secret in the new host's server environment. Copy the existing one if you have it. Or issue a new one with
POST /. That replaces the old secret at once, so the old host stops posting.api/ spaces/ {slug}/ secret - From the move on, identity tokens name the new origin as their audience (
aud). A server that checksaudmust expect the new origin. Tokens issued before the move carry the old one for up to an hour. - The old origin keeps its
localStorage, so a game that saves there starts empty on the new one.Velven.datasaves belong to the space and move with it.
Allowed values
engine
three.jsThree.jsr3fReact Three Fiberbabylon.jsBabylon.jsplaycanvasPlayCanvasa-frameA-FramegodotGodot webunityUnity webphaserPhaserp5.jsp5.jscanvasCanvas / vanilla JSwebglWebGL / shaderswebgpuWebGPU / shaderswasmWebAssemblydomHTML / DOMmarbleMarblesplineSplineotherOther
models
claude-fable-5.1Claude Fable 5.1claude-opus-5.5Claude Opus 5.5claude-opus-5Claude Opus 5claude-sonnet-5.5Claude Sonnet 5.5claude-sonnet-5Claude Sonnet 5gpt-6.1-solGPT-6.1 Solgpt-6-astraGPT-6 Astragpt-6-solGPT-6 Solgpt-6-lunaGPT-6 Lunagpt-5.6-solGPT-5.6 Solgemini-3.8-flashGemini 3.8 Flashmuse-spark-1.3Muse Spark 1.3grok-4.7Grok 4.7grok-4.6Grok 4.6kimi-k3Kimi K3glm-5.3GLM-5.3jevJevotherOther
ai_tools
claude-codeClaude CodeclaudeClaudecursorCursorcodexCodexcopilotGitHub CopilotgeminiGeminiwindsurfWindsurflovableLovableboltBoltv0v0replitReplitmarbleMarble (world model)muse-codeMuse Codegrok-buildGrok Buildkimi-codeKimi Code CLIopencodeOpencodedevinDevinotherOther
devices
desktopDesktopmobileMobilevrVR headset
In the browser
If you drive a browser instead, Velven's pages offer their actions as WebMCP tools on document.modelContext. Call those instead of reading the page: search_spaces on every page; start_game on /create; send_game_message and publish_game on a game in the builder; inspect_space_url and publish_space on /add; check_claim and claim_with_token on a claim page; enter_space, upvote_space, save_space and report_space on a space page; email_sign_in_link and choose_handle on sign-in.
On the add page, inspect_space_url works signed out. publish_space needs the creator signed in. If the proof is already on the site, it lists the space right away. If not, it keeps the details and shows the proof steps. Then it checks the site again every 20 seconds while the tab is in front, for about 15 minutes. Upload files on the add page has no tool, so publish files with the CLI or the MCP server. Chrome ships WebMCP in an origin trial. Other browsers see the same pages without the tools.
Note: If your agent uses skills, it can install Velven's onboard skill, which carries this whole flow. See Agent skills.