velven
Docs
Menu

Publish with an agent

A coding agent or a chat assistant can put a space on Velven for its creator. A coding agent publishes the build with the Velven CLI; an assistant in a chat publishes through Velven's MCP server; and a space already live on another host is listed through the API. The creator approves each sign-in in their browser, so no key is ever copied by hand.

View as Markdown

Choose a way in

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, its type and the devices it works on, or take them from what they already said. Never guess them: they are the listing people see.

Publish with the CLI

Run the CLI with --json, so every command prints one JSON object, and --yes, so it never waits on a prompt. The CLI reference has every flag and exit code.

Terminal
npx @velven/cli login                                   # prints a code and a link: show both to the creatornpx @velven/cli publish ./dist --title "Orbit Dodger" --type game --devices desktop,mobile --yes --jsonnpx @velven/cli publish ./dist --prod --yes --wait --json   # when the creator asks to go live
  • login prints the code and the approval link and waits until the creator approves. Show them both; the token is saved for this creator's later publishes.
  • The first publish writes velven.json into the folder, with "space"; keep it, so later publishes update the same space.
  • A plain publish answers status: "preview" and a previewUrl to show the creator. With --prod --wait it answers the verdict: live with pageUrl, or refused or unjudged with reason and shownOnTake, exit 6.
  • For unjudged, add a start hint to velven.json, such as "start": { "click": "Play" }, and publish again. For refused, tell the creator the reason; they can ask for a review from watchUrl.

Without login, the same publish makes an unlisted page and answers pageUrl, claimToken, claimUrl and expiresAt, and terms. Show the creator all of them, say the page is deleted after 7 days unless they claim it, and that publishing means they agree to Velven's Terms. Running login and publishing again from the folder claims it.

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, or to Claude Code with its plugin or one command:

Terminal
claude mcp add --transport http velven https://mcp.velven.ai/mcp
Tool
publish
What it does
Publishes one HTML page (html) or a few files (files, up to 3 MB in all) as a new space or a new version (space). A private preview by default; prod: true goes live after the safety check. A velven.json, a dotfile, a dot-folder or node_modules among the files, in any folder, is left out and named, never published: velven.json's settings go in the tool's fields, or publish the folder with the CLI.
Sign-in
Optional
Tool
my_spaces
What it does
The account's spaces: slug, title, status, whether Velven hosts it, and its page.
Sign-in
Needed
Tool
versions
What it does
A hosted space's versions, newest first, with their status.
Sign-in
Needed
Tool
rollback
What it does
Puts a version that was live before live again.
Sign-in
Needed
Tool
search_docs
What it does
Searches these docs and answers the best sections, with links.
Sign-in
None

A tool that needs an account makes the app ask the creator to sign in to Velven and approve the connection in their browser. Without signing in, publish makes an unlisted page and answers its address, a claim token, a claim link and a Terms line: show every one of them, and tell the creator to keep the token, the only way to update the page or keep it past 7 days. Signed in later, publish with claim claims it.

Note: For a folder over 3 MB, or from a terminal, use the CLI.

Ask for a sign-in link

The rest of this page lists a space already live on a supported host, and reads and changes a listed space. The sign-in is the same device-code flow the CLI runs. Make one unauthenticated call. agent_name is optional, up to 60 characters, and is shown to the creator on the approval screen.

Terminal
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:

JSON
{  "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.

Note: A 429 rate_limited means this address asked for too many sign-in links in the last 15 minutes. Wait; do not retry in a loop.

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 are not signed in to Velven, the same screen signs them in with GitHub, Google or an email link, and asks for a handle.

Meanwhile, poll for the token every interval seconds, and give up after expires_in seconds:

Terminal
curl -s -X POST https://velven.ai/api/agent/token \  -H "content-type: application/json" \  -d '{"device_code":"<device_code>"}'
Status
200
Body
{ "access_token", "token_type": "bearer", "handle", "profile_url" }
What to do
Approved. Store the token: it is returned once and lasts 90 days.
Status
428
Body
{ "error": "authorization_pending", "interval": 5 }
What to do
Not decided yet. Wait interval seconds and poll again.
Status
403
Body
{ "error": "access_denied" }
What to do
The creator pressed Deny. Stop and say so.
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 was not { "device_code": "…" }. Fix the request.
Status
5xx
Body
{ "error": "server_error" }, or no answer
What to do
A problem on Velven's side. Keep polling until expires_in runs 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 and works only on the endpoints on this page and in the REST API. When a call answers 401, the token expired or was revoked: delete the file and sign in again.

To check who a token belongs to:

Terminal
curl -s https://velven.ai/api/agent/me -H "authorization: Bearer $VELVEN_TOKEN"
JSON
{ "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>, with the handle from the token response, then deploy again:

HTML
<meta name="velven" content="@handle">

Wait until the deploy is live before submitting; a submit that comes too early answers 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 detail for each host, including the framing header a page must allow.

Note: A ChatGPT site must be published with "Who has access" set to "Anyone on the Internet". A site only its owner can open answers 401 and cannot be verified.

Submit the space

Fill in the details from the project rather than asking the creator: the engine from package.json and the imports, the AI tools from yourself, the devices from the pointer and touch handling, the description from the README. Velven fetches the URL, checks the proof, and fills anything you leave out from the page: the title, the meta description, the engine its scripts name and the tool its markup names. What you send always wins.

Terminal
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.",  "space_type": "game",  "engine": "three.js",  "models": ["claude-opus-5"],  "ai_tools": ["claude-code"],  "devices": ["desktop", "mobile"],  "how_made": "One session with Claude Code. Three.js scene, hand-rolled physics, procedural debris field.",  "source_url": "https://github.com/mara/orbit-dodger"}JSON
Field
url
Required
Yes
Notes
The space's public http(s) URL. Stored after redirects.
Field
space_type
Required
Yes
Notes
One of the allowed values.
Field
devices
Required
Yes
Notes
An array of at least one allowed value.
Field
title
Required
No
Notes
Up to 80 characters. Defaults to the page's og:title or <title>.
Field
description
Required
No
Notes
One line, up to 160 characters. Defaults to the meta description.
Field
engine
Required
No
Notes
An allowed value. Left out, Velven reads it from the page's scripts; send null for 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. Left out, Velven reads it from the page's markup; send [] for none.
Field
how_made
Required
No
Notes
Up to 2000 characters: the prompt, or how it was made. Shown as Prompt on the page.
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 at once on the creator's handle but only the creator can see it while status is processing. It goes public, as published, once Velven has recorded its clip and thumbnail, a few minutes later.

JSON
{  "slug": "orbit-dodger",  "url": "https://velven.ai/mara/orbit-dodger",  "status": "processing",  "title": "Orbit Dodger"}

When the page carries a boards block, the answer also has boards, the keys Velven synced from it, or boards_error, why the block was not taken.

Status
401
Body
{ "error": "unauthorized" }
Meaning
The token is missing, revoked or unknown. Sign in again.
Status
403
Body
{ "error": "blocked" }
Meaning
This URL or account cannot list spaces.
Status
409
Body
{ "error": "unverified", "instruction", "snippet" }
Meaning
The proof is not on the live site yet. Make the change instruction names, deploy, wait for the host's cache, and call again. Do not retry more than a few times without changing something.
Status
409
Body
{ "error": "duplicate", "url" }
Meaning
Already on Velven; url is its page. If it shows as unclaimed, claim it.
Status
422
Body
{ "error": "invalid", "issues": [{ "path", "message" }] }
Meaning
Fix the fields named and retry. An issue on url can also mean Velven does not list that host.
Status
422
Body
{ "error": "unreachable", "message", "instruction" }
Meaning
The URL did not answer (a timeout, DNS, a refused connection), or is shut to visitors (a Vercel deployment behind Vercel Authentication, or a site shared with its owner only). Check the deployment is public and call again. instruction, saying how to open it, comes only with a shut page.
Status
422
Body
{ "error": "unframeable", "instruction", "snippet" }
Meaning
The site's headers refuse to let Velven frame it. Make the change instruction names, deploy, and call again.
Status
500
Body
{ "error": "server_error", "message" }
Meaning
A problem on Velven's side. Retry once after a moment; if it persists, tell the creator.

Reply to the creator

Reply with the url, and offer the badge for the README:

HTML
<a href="https://velven.ai/mara/orbit-dodger"><img src="https://velven.ai/badge/orbit-dodger" alt="On Velven"></a>

To list what this creator has already listed:

Terminal
curl -s "https://velven.ai/api/spaces?mine=1" -H "authorization: Bearer $VELVEN_TOKEN"
JSON
[{ "slug": "orbit-dodger", "title": "Orbit Dodger", "url": "https://orbit-dodger.netlify.app", "plays": 412, "upvotes": 18, "velven_url": "https://velven.ai/mara/orbit-dodger" }]

Keep the clip current

After listing, Velven plays the space in its own browser and records a 5-second clip; its first frame becomes the thumbnail. When the site is rebuilt, Velven's background check notices the change within 6 hours and records again. A space published on Velven gets its clip from the safety check of each version that goes live.

After a deploy that changes how the space looks, ask for a retake at once. 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 do not count.

Terminal
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
202
Body
{ "slug", "status" }
Meaning
Queued, or already queued or running; status says 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 was not { slug, note? }, or note is over 100 characters.
Status
503
Body
{ "error": "capture_off" }
Meaning
Recording is paused on Velven's side. Nothing to fix; try later.

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: write it from the code and send it with the listing, or change it later. Every field you send is the creator's, and 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), 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. For a world, a tool or a wonder, leave out how_to_play and controls if they do not apply.

Field
about
Limit
Up to 1500 characters
Notes
What the space is, 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" }] }. label is optional, up to 40 characters; input up to 40, such as W, Space or Mouse drag; action up to 80.
Field
features
Limit
Up to 6 items, 100 characters each
Notes
Short lines, 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
single single player, multi multiplayer.

In the body of POST /api/spaces, beside the other fields:

JSON
{  "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"}

If the listing is made but its page text could not be saved, the 201 response carries "page_not_written": true. The space stands without the text; send the same fields again with PATCH /api/spaces/{slug}.

To read the page as it stands, with who wrote each field in page_authors (model for Velven, creator for you), call GET /api/spaces/{slug}. To change it, send any of these fields, or any of the listing's own (title, description, space_type, engine, models, ai_tools, devices, how_made, source_url), to PATCH /api/spaces/{slug}. Only the fields you name change, and null clears one. Both answer the space's fields.

Terminal
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
200
Body
The space's fields, page_authors and velven_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 named. A field that cannot be changed is refused by name; to change url, move the space with POST /api/spaces/{slug}/move.
Status
500
Body
{ "error": "server_error", "message" }
Meaning
Saving failed. The message says whether the page's text was saved (page_written) and the other fields were not; send what did not land again.

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 stay. Velven checks the new page as it checks a listing, so first put the proof tag on the new page and allow framing there, deploy, then call:

Terminal
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 the space's fields as GET /api/spaces/{slug} does, with previous_url. Every refusal and its code is in the REST API.

  • 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 /api/spaces/{slug}/secret, which replaces the old one at once, so the old host stops posting.
  • Identity tokens name the new origin as their audience (aud) from the move on. A server that checks aud must 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.data saves belong to the space and move with it.

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/velven+json"> block beside the proof tag (or in velven.json for a space published on Velven), and 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.

HTML
<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", the default, takes posts only from the space's own server, with the space's secret and the player's token; a page's own post to it answers 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 choose.

A server board needs the secret, issued on the creator's token and shown once. Put it in the host's server environment, never in the page:

Terminal
curl -s -X POST https://velven.ai/api/spaces/<slug>/secret -H "authorization: Bearer $VELVEN_TOKEN"

Boards can also be declared or changed without a deploy, in the same shape as the block. Only the boards named are written, and the page's next sync overwrites a key it also names:

Terminal
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}]}'

Reading the boards is in the REST API, and deleting a submission or banning a player under Moderation.

Claim a space Velven listed

Velven lists some spaces itself. They show as unclaimed at /s/slug, and a duplicate answer on submit can point at one. With the proof 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.

Terminal
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
200
Body
{ "verified": true, "url" }
Meaning
Done. url is now the creator's page, /handle/slug.
Status
403
Body
{ "error": "blocked" }
Meaning
This account cannot claim spaces. Tell the creator and stop.
Status
404
Body
{ "error": "not_found" }
Meaning
No published space has that slug.
Status
409
Body
{ "error": "unverified", "instruction", "snippet" }
Meaning
The proof is not 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 tell Velven with Feedback on the space's page.
Status
422
Body
{ "error": "invalid", "issues" }
Meaning
The body was not { slug }.

Allowed values

space_type

  • game Game
  • world World
  • tool Tool
  • wonder Wonder

engine

  • three.js Three.js
  • r3f React Three Fiber
  • babylon.js Babylon.js
  • playcanvas PlayCanvas
  • a-frame A-Frame
  • godot Godot web
  • unity Unity web
  • phaser Phaser
  • p5.js p5.js
  • canvas Canvas / vanilla JS
  • webgl WebGL / shaders
  • webgpu WebGPU / shaders
  • wasm WebAssembly
  • dom HTML / DOM
  • marble Marble
  • spline Spline
  • other Other

models

  • claude-fable-5.1 Claude Fable 5.1
  • claude-opus-5.5 Claude Opus 5.5
  • claude-opus-5 Claude Opus 5
  • claude-sonnet-5 Claude Sonnet 5
  • gpt-6-astra GPT-6 Astra
  • gpt-6-sol GPT-6 Sol
  • gpt-6-luna GPT-6 Luna
  • gpt-5.6-sol GPT-5.6 Sol
  • gemini-3.8-flash Gemini 3.8 Flash
  • muse-spark-1.3 Muse Spark 1.3
  • grok-4.7 Grok 4.7
  • grok-4.6 Grok 4.6
  • kimi-k3 Kimi K3
  • glm-5.3 GLM-5.3
  • jev Jev
  • other Other

ai_tools

  • claude-code Claude Code
  • claude Claude
  • cursor Cursor
  • codex Codex
  • copilot GitHub Copilot
  • gemini Gemini
  • windsurf Windsurf
  • lovable Lovable
  • bolt Bolt
  • v0 v0
  • replit Replit
  • marble Marble (world model)
  • muse-code Muse Code
  • grok-build Grok Build
  • kimi-code Kimi Code CLI
  • opencode Opencode
  • devin Devin
  • other Other

devices

  • desktop Desktop
  • mobile Mobile
  • vr VR headset

In the browser

If you drive a browser instead, Velven's pages expose their actions as WebMCP tools on document.modelContext. Call those rather than reading the page: search_spaces on every page; 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: it lists the space at once when the proof is already on the site, and otherwise keeps the details, shows the proof steps and re-checks the site until it clears. Chrome ships WebMCP in an origin trial; other browsers see the same pages without the tools.

Note: Agents that use skills can install Velven's onboard skill, which carries this whole flow: Agent skills.