# Velven for agents

Velven is the community leaderboard for spaces built with AI: games, toys, tools, simulations, art. Spaces stay where they are hosted; Velven lists them, counts plays and ranks them. This page is for coding agents that deployed a space and want to list it for the creator. The flow is a device-code login: no API key is ever copied by hand.

## 1. Ask for a login link

One unauthenticated call. `agent_name` is optional (max 60 characters) and is shown to the creator on the approval screen.

```bash
curl -s -X POST https://velven.ai/api/agent/login \
  -H "content-type: application/json" \
  -d '{"agent_name":"Claude Code"}'
```

Response, 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 the `verification_url` and the `user_code`. A 429 `rate_limited` means this address opened too many logins in the last quarter hour; wait, do not retry in a loop.

## 2. The creator approves

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 yet, the same screen signs them in with Google or a magic link and asks for a handle. Sign-up lives inside the approval.

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

```bash
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 |
| --- | --- | --- |
| 428 | `{ "error": "authorization_pending", "interval": 5 }` | Not decided yet. Wait `interval` seconds, poll again. |
| 200 | `{ "access_token", "token_type": "bearer", "handle", "profile_url" }` | Approved. Store the token; it is returned exactly once and lasts 90 days. A 401 later means it expired or the creator revoked it: log in again. |
| 403 | `{ "error": "access_denied" }` | The creator pressed Deny. Stop and say so. |
| 400 | `{ "error": "expired_token" }` | 15 minutes passed, or the code is unknown. Start over at step 1. |
| 400 | `{ "error": "invalid_grant" }` | This device code was already exchanged. Use the token you have, or start over. |

Store the token at `~/.config/velven/token` with mode 600 and reuse it for later spaces by the same creator. When any call answers 401, delete the file and log in again. Tokens are tied to one creator and only work on `/api/spaces` and `/api/agent/me`.

Check who a token belongs to:

```bash
curl -s https://velven.ai/api/agent/me -H "authorization: Bearer $VELVEN_TOKEN"
```

```json
{ "handle": "mara", "profile_url": "https://velven.ai/@mara" }
```

## 3. Put the proof on the site

Only a verified creator can list a space, so the proof goes on the site before you submit. It names the creator's `handle` from the token response. You can do this part yourself on Vercel, Netlify and ChatGPT sites; a Claude artifact needs the creator.

| Host | Proof |
| --- | --- |
| Vercel, Netlify, ChatGPT sites | `<meta name="velven" content="@handle">` inside the page's `<head>`. Deploy or publish again. |
| Claude artifacts | In Claude, Publish → Get embed code → Allowed domains: add `handle.velven.ai` next to `velven.ai`. Velven reads it from the artifact's `frame-ancestors` header. |

Wait for the deploy to be live before submitting. If you submit too early the answer is a 409 with the exact change still needed.

## 4. Submit the space

Fill provenance in from the project rather than asking the creator: the engine from `package.json` and imports, the AI tools from yourself, devices from pointer and touch handling, the description from the README. Velven fetches the URL, follows redirects, checks the proof, decides whether it can play in place, and falls back to the page title, meta description and `og:image` for anything you leave out.

```bash
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",
  "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",
  "screenshot_url": "https://orbit-dodger.netlify.app/og.png"
}
JSON
```

| Field | Required | Notes |
| --- | --- | --- |
| `url` | yes | Public http(s) URL of the space. Stored after redirects. |
| `title` | no | Max 80. Falls back to `og:title` or `<title>`. |
| `description` | no | One line, max 160. Falls back to the meta description. |
| `space_type` | yes | One of the values below. |
| `engine` | yes | One of the values below. |
| `ai_tools` | yes | Array, at least one value from below. |
| `devices` | yes | Array, at least one value from below. |
| `how_made` | no | Max 2000. Prompts, process, what was hard. |
| `source_url` | no | Repository or artifact link. |
| `screenshot_url` | no | Absolute image URL. Falls back to `og:image`, then an automatic capture. |

Response, status 201. The page is live on the creator's handle and on the leaderboard:

```json
{
  "slug": "orbit-dodger",
  "url": "https://velven.ai/@mara/orbit-dodger",
  "embed_mode": "iframe",
  "nudge": null,
  "title": "Orbit Dodger"
}
```

| Status | Body | Meaning |
| --- | --- | --- |
| 409 | `{ "error": "unverified", "instruction", "snippet" }` | The proof is not on the live site yet. Make the change `instruction` names, deploy, wait for the cache, call again. Do not retry more than a few times without changing something. |
| 401 | `{ "error": "unauthorized" }` | Missing, revoked or unknown token. Log in again. |
| 403 | `{ "error": "blocked" }` | The URL or the account cannot list spaces. |
| 409 | `{ "error": "duplicate", "url": "…" }` | Already on Velven. `url` is the existing page. If it shows as unclaimed, Velven listed it itself; see below. |
| 422 | `{ "error": "invalid", "issues": [{ "path", "message" }] }` | Fix the listed fields and retry. A `url` issue also means the host is not one Velven lists yet. |

## 5. Reply to the creator

Reply with the `url`. If `nudge` is set, pass it along verbatim; it explains why the space opens in a new tab and how to fix that. 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>
```

List what this creator has already listed:

```bash
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" }]
```

## Claiming a space Velven listed

Velven seeds the board with a few curated spaces of its own. Those show as unclaimed at `/s/slug`, and a duplicate error on submit points at one. With the same proof on the site, claim it for the creator; its plays stay and it moves to their handle.

```bash
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, "url": "…" }` | Done. `url` is now the creator's page, `/@handle/slug`. |
| 409 | `{ "error": "unverified", "instruction", "snippet" }` | The proof is not on the live site yet. Deploy, wait, call again. |
| 409 | `{ "error": "owned" }` | Someone else already verified it. Tell the creator; they can report it from the page. |
| 404 | `{ "error": "not_found" }` | No such slug. |
| 403 | `{ "error": "blocked" }` | This account cannot claim spaces. Tell the creator, stop. |
| 422 | `{ "error": "invalid", "issues" }` | The body was not `{ slug }`. |

## Allowed values

`space_type`

- `game` Game
- `toy` Toy
- `tool` Tool
- `simulation` Simulation
- `art` Art

`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
- `marble` Marble
- `spline` Spline
- `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)
- `other` Other

`devices`

- `desktop` Desktop
- `mobile` Mobile
- `vr` VR headset

## Hosting

Velven lists spaces hosted on Vercel, Netlify, ChatGPT sites and Claude artifacts for now; custom domains on Vercel and Netlify are recognised by their response headers. Other hosts are refused with 422. Vercel, Netlify and ChatGPT sites allow framing, so the space plays in place (`embed_mode: "iframe"`). A Claude artifact plays in place once the creator publishes it and adds the Velven domain under "Get embed code" → Allowed domains; otherwise it opens in a new tab (`embed_mode: "newtab"`) and plays still count.

Velven checks each host's framing headers at submit time and again in the background, so a space can move from new tab to in place later without resubmitting.

## Skill and plain-text docs

Agents that use skills.sh can install the Velven skill with `npx skills add velvenai/app`. The skill lives at `skills/velven/SKILL.md` in the Velven repository and teaches this whole flow.

This page as markdown: https://velven.ai/docs/agent.md. Index for crawlers and agents: https://velven.ai/llms.txt.
