| name: | val-tool-playground |
|---|---|
| description: | Build vals on Val Town — tools, utilities, playgrounds, dashboards, apps, and agents with consistent patterns. Use this skill whenever creating any Val Town HTTP val. Trigger when the user says "create a val", "build a tool", "make a playground", "build a dashboard", or wants to build anything that runs as an HTTP endpoint on Val Town. |
A skill for building vals on Val Town. These range from single-purpose developer tools to interactive playgrounds, dashboards, apps, and agent interfaces. They share a common philosophy: composability over complexity, URLs over abstractions.
- Developer tools — jwt-decoder, dns-lookup, headers-inspector, cron-tester, hashEncode, qr-generator, cache-purge, og-previewer, sitemap-viewer, userAgentParser, logo-fetcher
- Playgrounds — sprite-playground, ts-playground, podcast-search, search-api-playground, aiPlayground
- Dashboards & admin — token-pool-dashboard, sqlite-usage-tracker, github-ratelimits, sqliteExplorerReact, status
- Apps & services — golinks, living-stories-self-registration, valLauncher, flag-duel, share-prompts, dispatch, company
- Agents — agent-chat-app, agentfs-bash
- Infrastructure — token-pool, axiom-logger, vt-sync
Different categories have different needs, but the philosophy below applies to all.
The most useful things on the internet are the ones you can compose. A val that only works through its web UI is a dead end. A val that returns structured output from a URL is a building block.
Design every val so that:
- A human can open it in a browser and use it interactively
- A script can
curlit and pipe the output somewhere - An LLM can fetch it as context for a prompt
- Another val can call it as a dependency
This last point is increasingly important. Vals should be agent-friendly — their output should be parseable and useful when an LLM fetches the URL directly. Think of every val as a potential node in an agentic workflow.
Not every val needs a full API (a game probably doesn't), but always ask: "could something useful be extracted from a programmatic request to this URL?"
When the val processes a URL as input, make it a path parameter:
https://defuddle.val.run/https://example.com/article
This is more composable than query params because you can construct it by concatenation. Query params are for modifiers and flags (?json=1, ?selector=.content), not primary input.
This pattern applies to proxy-style tools. For tools with other input types (text, config, files), use POST with a JSON body or query params — whatever maps most naturally to how the tool would be called programmatically.
When a val has an API mode, return the most useful raw format — plain text, markdown, CSV — not JSON. JSON is for metadata.
Why: A tool that returns markdown can be piped directly into an LLM context window. A tool that returns JSON requires parsing first. Optimize for the common case.
Provide ?json=1 for when callers need structured metadata alongside the content.
Each val does one thing. Don't build a Swiss Army knife. Build a screwdriver that works perfectly and can be composed with other screwdrivers.
Composition happens at the URL level: chain tools together by passing one tool's output URL to another. This only works if each tool has a clear, singular purpose.
For tool-style vals, serve from a single HTTP endpoint with routing based on the request:
GET / → HTML page (the UI)
GET /path-or-params → raw output (the API)
GET /path-or-params?json=1 → structured JSON with metadata
POST / with JSON body → process input and return JSON
The UI is a convenience layer. The API is the product.
For app-style vals (dashboards, games, registration forms), the UI is the product — but even then, consider whether read endpoints (status checks, data exports, health checks) would be useful.
export default async function (req: Request): Promise<Response> {
const url = new URL(req.url);
// 1. Handle OPTIONS for CORS preflight
// 2. Handle POST (for paste/upload input)
// 3. Check for target input in path or query params
// → Return raw output or JSON based on ?json param
// 4. Fall through to HTML UI
}
Simple vals (single concern, short HTML):
main.ts # HTTP handler + inline HTML renderer
README.md
Complex vals (multiple features, rich UI):
main.ts # HTTP handler (routing only)
lib/
renderer.ts # HTML page generation
parser.ts # Core logic
types.ts # TypeScript interfaces
README.md
Keep main.ts thin — just routing. Push logic into lib/.
Always add CORS headers on API responses so tools are callable from anywhere:
const cors = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
Return proper HTTP status codes with error messages in both modes:
// API consumers get JSON errors
if (wantJson) return Response.json({ error: e.message }, { status: 502, headers: cors });
// Raw consumers get plain text errors
return new Response(`Error: ${e.message}`, { status: 502, headers: cors });
In the UI, show errors inline — never use alert().
We use Pico CSS v2 as the styling foundation. It provides semantic HTML styling with zero classes, system theme support, and consistent form/button/card components.
Core rules:
- System theme only — never force light or dark
- No external images or favicons — use emoji or inline SVG
- No frameworks (React, etc.) for simple tools — vanilla HTML/CSS/JS
- Prefer Pico's native components over custom CSS (accordions, tooltips, buttons, switches, dropdowns are all built in)
- Use ARIA attributes for state (
aria-busyfor loading,aria-invalidfor validation,aria-selectedfor tabs)
When to reach for React/Hono: If the val has complex client-side state (chat interfaces, real-time dashboards, multi-step forms with validation), a framework is justified. Use Hono for server-side routing when you have more than ~3 route patterns.
For the base HTML template, component patterns, tabs implementation, and the full list of native Pico components, see references/pico-ui-patterns.md.
Pin npm versions. Use npm: prefix. Fall back to https://esm.sh/ if sandboxed:
import { Window } from "npm:happy-dom@17.4.4";
export default async function (req: Request): Promise<Response> { ... }
- No filesystem (
fs,Deno.readFile) — use SQLite/blob for persistence - No subprocess (
Deno.Command,child_process) - 30s execution timeout
- Don't use
Response.redirect— broken on Val Town
Keep brief. Lead with what it does and how to use the API (if it has one). The UI is self-explanatory.
# Tool Name
One-line description.
**Live:** https://tool.val.run
## API
\`\`\`
https://tool.val.run/https://example.com
\`\`\`
### Options
| Param | Description |
|-------|-------------|
| `?json=1` | Full JSON with metadata |
## Limitations
- Known constraints
For app-style vals without an API, skip the API section — just describe what it does and link to the live URL.
- Val name: kebab-case (
jwt-decoder,dns-lookup) - Description: terse, starts with verb or noun
- Emoji in H1 only, not in val name or description
- Don't forget the API — if the val processes input, it should work without the UI
- Don't over-abstract — single
main.tsis fine for simple vals - Don't force a color theme — respect system preference
- Don't reach for React/Hono by default — only when complexity demands it
- Don't build multi-purpose vals — split concerns into separate vals that compose