Skill: vladlat/vvvlink-site-builder (version 1.8.1)

--- name: vvvlink-site-builder description: > Build and publish websites on vvvlink.com. Creates HTML sites, uploads to VVVLink API, publishes with unique subdomain URLs. Triggers on: "create a site", "build a website", "landing page", "website", "portfolio", "publish site", "deploy site". config: path: ~/.vvvlink/config.json description: > Stores account UUID and API key. Created automatically on first use via POST /auth/create_new_user. API key is stored with chmod 600 permissions. persistence: - ~/.vvvlink/config.json (account credentials) - ~/.vvvlink/sites/<subdomain>/ (generated site files) api: base: https://publish.vvvlink.com auth: Bearer API key (stored in config.json) --- # VVVLink Sitebuilder Use this skill when the user wants to create, redesign, or publish a website, landing page, portfolio, or any HTML page that should be accessible online. <ANNOUNCE> Before ANY work, announce to the user: "Using vvvlink-site-builder to create and publish your site." </ANNOUNCE> ## Quick Reference - **API Base**: `https://publish.vvvlink.com` - **Site creation rules**: See [website-development-rules.md]( references/website-development-rules.md) - **Full API docs**: See [api-reference.md]( references/api-reference.md) ## Direct Invocation When the user invokes this skill directly (without a specific task), show an intro and check for existing sites. Use Telegram formatting. 1. Load API key from `~/.vvvlink/config.json` and call `GET /sites` (if config exists) 2. Split sites by status: "live" and non-live ("uploading", etc.) 3. Show intro message based on result: **If user has live sites:** ``` VVVLink Site Builder Your sites: 1. my-site.vvvlink.com 2. cool-page.vvvlink.com +2 drafts (uploading). Ask me to show or clean them up. I can update any of these or create a new one. What would you like to do? ``` Only show the drafts line if there are non-live sites. If the user asks to see or manage drafts, list them with subdomain, status, and offer to delete individual ones or all at once via `DELETE /sites/:siteId`. **If no sites or no API key:** ``` VVVLink Site Builder I build and publish websites on vvvlink.com in seconds. Describe what you need — a landing page, portfolio, business site — and I'll design, build, and publish it. Your site will be live with a unique URL right away. ``` When user picks an existing site to update, go to Step 10 (Updates) in the flow below. ## Flow (strict order, no skipping) ### Step 1: Announce and show progress <PROGRESS-CRITICAL> THIS IS THE MOST IMPORTANT RULE IN THIS SKILL. The user sees your text output as a stream. While you write text — they see it appearing live. When you make a tool call — they see nothing until the tool returns and you write more text. Therefore: WRITE TEXT BEFORE AND AFTER EVERY TOOL CALL. In practice, your response should look like this (the user sees each line appear as you stream it): ``` ⚡ Building your site with VVVLink Site Builder... 🔍 Searching for photos of [topic]... [tool call: curl image search] 📸 Found 5 great shots! 🎨 Designing layout with [palette] and [fonts]... 🏗️ Building hero section... [tool call: write first part of HTML] ✨ Adding content sections... [tool call: write rest of HTML] 📦 HTML ready! 🚀 Publishing to vvvlink.com... [tool call: create site] [tool call: upload file] [tool call: publish] [tool call: rename subdomain] ✅ Your site is live! 🌐 https://your-site.vvvlink.com ``` KEY RULES: - Write at least one emoji + text line BEFORE each tool call - The user must always see what you're doing RIGHT NOW - NEVER make 2+ tool calls in a row without text between them - If a tool call takes time, the preceding text is what the user will see while waiting — make it informative IMPORTANT: These progress rules apply ONLY when BUILDING a site (multi-step process: photos → HTML → upload → publish). For simple operations like listing sites or checking status — just do it quickly, no need for multiple progress messages. One short message before + result is enough. </PROGRESS-CRITICAL> ### Step 1.5: Ask for business details (if missing) If the site looks like a business page (cafe, shop, studio, agency, etc.) and the user did NOT provide key info, gently ask ONCE before building. Keep it short — one message: "📋 Before I start — want to add any of these to make it real? 📞 Phone / email / address 🕐 Opening hours 🔗 Social media links Or I can build it now and you'll add details later." If user says "just build it" or similar — proceed immediately. Do NOT block the build. This is optional, not a gate. Skip this step entirely for non-business sites (portfolios, personal pages, experiments). ### Step 2: Find photos for the site Before writing any code, search for relevant photos: ```bash # Search by keywords matching the site's topic curl -s "https://publish.vvvlink.com/images/search?q=KEYWORD&count=5&orientation=landscape" ``` Pick 3-6 keywords based on the site content (e.g., "office", "team", "technology"). Select the best photo for each section (hero, about, features, etc.). Use the `url` field from the response directly in `<img src>` tags. Add `&w=1600&h=900&fit=crop` to URLs for hero images, `&w=600&h=400&fit=crop` for cards. ### Step 3: Create the site Read and follow `references/website-development-rules.md`. Build a high-quality static site using the photos from Step 2. Output all files to `~/.vvvlink/sites/<subdomain>/`. ### Step 4: Get API key <API-KEY-RULES> **apiKey is SECRET.** Never show it to the user, never log it, never include in messages. It is only for Authorization headers. **UUID is public.** It identifies the account. You may show it to the user if asked. **Storage:** `~/.vvvlink/config.json` ```json { "uuid": "account-uuid", "apiKey": "SECRET-key-never-share" } ``` **Site files:** `~/.vvvlink/sites/<subdomain>/` **Rules:** 1. NEVER call `/auth/create_new_user` if `~/.vvvlink/config.json` exists. The apiKey is issued ONCE and cannot be recovered. 2. Only create a new account if config.json does not exist. 3. Protect `~/.vvvlink/config.json` with `chmod 600`. **Procedure:** 1. Read `~/.vvvlink/config.json` → use `.apiKey` 2. If file does not exist → `POST /auth/create_new_user {}` → save response to config.json </API-KEY-RULES> ```bash # Load or create API credentials VVVLINK_DIR="$HOME/.vvvlink" VVVLINK_CONFIG="$VVVLINK_DIR/config.json" if [ -f "$VVVLINK_CONFIG" ]; then VVVLINK_API_KEY=$(jq -r '.apiKey' "$VVVLINK_CONFIG") else mkdir -p "$VVVLINK_DIR" RESP=$(curl -s -X POST https://publish.vvvlink.com/auth/create_new_user \ -H "Content-Type: application/json" -d '{}') if echo "$RESP" | jq -e '.apiKey' > /dev/null 2>&1; then VVVLINK_API_KEY=$(echo "$RESP" | jq -r '.apiKey') echo "$RESP" | jq '{uuid: .uuid, [REDACTED_SECRET]' \ > "$VVVLINK_CONFIG" chmod 600 "$VVVLINK_CONFIG" else echo "Error: could not create account" >&2 exit 1 fi fi ``` ### Step 5: Create site and upload ```bash # Create site curl -s -X POST https://publish.vvvlink.com/sites \ -H "Authorization: Bearer $API_KEY" # Upload each file (from ~/.vvvlink/sites/<subdomain>/) curl -s -X PUT \ "https://publish.vvvlink.com/sites/$SITE_ID/files/index.html" \ -H "Authorization: Bearer $API_KEY" \ --data-binary @~/.vvvlink/sites/$SUBDOMAIN/index.html ``` Upload ALL files: HTML, CSS, JS, images, fonts. <SITE-LIMIT-HANDLING> If `POST /sites` returns HTTP 429 with `"Site limit reached"`, the user has hit their plan limit. Handle this gracefully: 1. Call the upgrade endpoint to get a checkout URL: ```bash curl -s -X POST https://publish.vvvlink.com/billing/upgrade \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"success_url": "https://vvvlink.com"}' ``` 2. Show the user an upgrade message with the payment link. In Telegram, use an inline link so the user can pay in the built-in web view: ``` ⚠️ *Site limit reached* You've used all available sites on the Free plan\. 🚀 [Upgrade to Pro](PAYMENT_URL) to unlock up to 100 sites\. _After upgrading, I'll publish your site right away\._ ``` 3. After the user pays and confirms, retry `POST /sites`. The limit will be updated automatically. If the upgrade endpoint returns `"Already on Pro plan"`, the user is already on Pro — this means they've genuinely used all 100 sites. Tell them to delete unused sites first. </SITE-LIMIT-HANDLING> ### Step 6: Publish ```bash curl -s -X POST \ "https://publish.vvvlink.com/sites/$SITE_ID/publish" \ -H "Authorization: Bearer $API_KEY" ``` ### Step 7: Choose subdomain BEFORE creating site Do NOT create a site with a random name. Pick a good one first: 1. Analyze the site content (`<title>`, `<h1>`, topic) 2. Generate 10 slug-friendly name candidates 3. Check availability via POST /subdomains/check-bulk 4. Pick the BEST available name 5. Pass it when creating the site: ```bash curl -s -X POST https://publish.vvvlink.com/sites \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"subdomain": "chosen-name"}' ``` The API will use your name if available, or fall back to a random one if taken. Check the response `subdomain` field to confirm which name was assigned. 6. Show the user the live URL and other available options: ``` 🚀 Your site is live! 🌐 https://chosen-name.vvvlink.com Want a different name? 1. alternative-one 2. alternative-two 3. alternative-three Reply with a number or type your own. ``` If user picks a different name, rename via: ```bash curl -s -X PUT \ "https://publish.vvvlink.com/sites/$SITE_ID/subdomain" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "new-name"}' ``` ### Step 8: Show URL and STOP When showing the final URL, add `?v=VERSION` to bust cache. Use the version number from the publish response. Use markdown link to hide the param: ``` [your-site.vvvlink.com](https://your-site.vvvlink.com?v=2) ``` The user sees clean URL but clicks through to the latest version. After showing the live URL and rename options — STOP. Do NOT automatically start improving, searching more photos, or making changes. Wait for the user to respond. You may add ONE short line like: "Want me to tweak anything — colors, content, add a section?" But do NOT take any action until the user asks. ### Step 9: Updates To update an existing site: ```bash curl -s -X POST \ "https://publish.vvvlink.com/sites/$SITE_ID/new-version" \ -H "Authorization: Bearer $API_KEY" # Upload new files, then publish again ``` ## Rules - Publish immediately without asking for confirmation - For updates to existing sites, mention that the user can roll back to a previous version or view version history - Always offer rename after first publish - Store API key — don't re-init every time - Upload ALL site files (HTML, CSS, JS, images, fonts) - Max 10MB per site for free users <ERROR-HANDLING> NEVER let a script error silently kill the build. - Do NOT use `set -e` in shell scripts - ALWAYS check API response before parsing with jq: ```bash RESP=$(curl -s -X POST .../sites -H "Authorization: Bearer $KEY") SITE_ID=$(echo "$RESP" | jq -r '.siteId // empty') if [ -z "$SITE_ID" ]; then # Show error to user, don't silently fail echo "Error creating site: $RESP" # Try to recover or explain fi ``` - If ANY step fails — tell the user what happened and what to do next. NEVER end with a raw error or empty response. - Show the user a human-readable message, not jq errors or raw JSON. - Use ONLY bash, curl, and jq. NEVER use python, python3, node, ruby, or any other interpreter. There is NO python on the server. If you try to use python — IT WILL FAIL. Everything can be done with bash + curl + jq. <ABSOLUTE-BAN> NEVER EVER put HTML inside bash. This is the #1 cause of failures and MUST be followed without exception. BANNED — all of these WILL break: - heredoc: cat > file << EOF ... EOF - echo: echo '<html>...' > file - printf: printf '%s' '<html>...' - inline: curl -d '<html>...' - any bash command containing HTML REQUIRED — the ONLY correct way: 1. Use your Write/file tool to create the HTML file 2. Then upload it with curl (separate tool call): ```bash curl -s -X PUT \ "https://publish.vvvlink.com/sites/$SITE_ID/files/index.html" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: text/html" \ --data-binary @~/.vvvlink/sites/my-site/index.html ``` Two separate tool calls. NEVER combine them. If you put HTML in bash — it WILL fail. Every time. </ABSOLUTE-BAN> - If you get HTTP 403 or 1010 — retry the request once. Cloudflare may temporarily block requests. If retry fails, tell the user: "Temporary access issue, try again in a moment." </ERROR-HANDLING> ## Message Formatting Messages to the user MUST be visually polished. Format for the platform you are running on (Telegram, CLI, etc). Do NOT hardcode escape characters — let your platform handle formatting. **Progress updates — use status indicators:** ``` Searching for photos... Building your site... Uploading files (3/3)... Publishing... Done! ``` **Final result — clean, no technical details:** ``` 🚀 Your site is live! yoursite.vvvlink.com Want a different name? Here are some options: ``` Do NOT show file names, file counts, photo counts, version numbers, or any other technical details in the final message. The user only cares about the URL. **Alternative names — simple numbered list, no "available":** ``` Want a different name? 1. cool-startup 2. my-project 3. awesome-landing Reply with a number or type your own. ``` **Key formatting rules:** - Keep messages compact — no walls of text - Clickable links for the published URL (always) - Never dump raw JSON, curl output, or API responses <COMPLETION-CRITERIA> THE TASK IS NOT DONE UNTIL THE USER SEES A LIVE URL. If you showed progress messages ("Building...", "Searching...") but did NOT finish with a live URL — YOU FAILED. Go back and complete all remaining steps. Checklist — ALL must be true: 1. Site HTML was created and written to file 2. Site was uploaded via API (create + upload + publish) 3. User received a CLICKABLE live URL (https://*.vvvlink.com) 4. Rename was offered If ANY tool call fails or times out — retry or tell the user what happened. NEVER silently stop in the middle. NEVER end your response after a progress message. The last message the user sees MUST contain either: - A live URL (success), OR - An error explanation with next steps (failure) </COMPLETION-CRITERIA>

path: references/website-development-rules.md
content: # Website Development Rules

## Visual Design (MOST IMPORTANT)

Sites MUST look distinctive and professionally designed.
Generic, template-like output is a failure.

### Typography
- Use real fonts from Google Fonts — never rely on system defaults
- Pick a display font for headings + a readable font for body
- Font sizes: hero 3-5rem, headings 1.5-2.5rem, body 1-1.125rem
- Use font-weight variation (300, 400, 600, 800) for hierarchy
- Letter-spacing on uppercase text: 0.05-0.1em

### Color
- Define a cohesive palette: 1 primary, 1 accent, 1-2 neutrals
- Store as CSS custom properties (--color-primary, etc.)
- Use gradients for backgrounds and CTAs — not flat solid colors
- AVOID: purple-on-white, all-gray, generic blue (#007bff)
- Dark sections + light sections = visual rhythm
- Accent color should appear in at least 3 places

### Layout & Depth
- Generous whitespace — padding 4-8rem between sections
- Asymmetric layouts are more interesting than centered grids
- Add depth: subtle shadows, overlapping elements, layered bg
- Use CSS Grid for complex layouts, not just centered columns
- Hero sections should be full-viewport and bold
- Break the grid occasionally — offset images, angled dividers

### Motion & Interaction
- 1-2 strong CSS animations: fade-in on scroll, hover transforms
- Smooth transitions on all interactive elements (0.2-0.3s ease)
- Subtle hover effects on cards/buttons (scale, shadow, color)
- Consider a gradient animation or floating element for hero
- Do NOT overdo — motion should enhance, not distract

### Photography (Unsplash via proxy)

Every site MUST include real, topically relevant photos.

**How to get images:**
Search for relevant photos via the image proxy (no auth needed):

```bash
# Search for photos by keyword
curl -s "https://publish.vvvlink.com/images/search?q=KEYWORD&count=5"

# With orientation filter
curl -s "https://publish.vvvlink.com/images/search?q=office&count=3&orientation=landscape"
```

Response:
```json
{
  "photos": [
    {
      "id": "abc123",
      "url": "https://images.unsplash.com/photo-...",
      "thumb": "https://images.unsplash.com/photo-...&w=400",
      "alt": "description of the photo",
      "credit": "Photographer Name",
      "credit_url": "https://unsplash.com/@photographer",
      "width": 5472,
      "height": 3648
    }
  ]
}
```

**CRITICAL: Response field is `photos`, NOT `results`.**
Parse with: `response.photos[0].url` — not `response.results`.

**Workflow:**
1. Analyze site content — pick 3-6 keywords (hero, about, features)
2. Search each keyword via the proxy
3. Parse response: access `.photos[]` array (NOT `.results[]`)
4. Use the `.url` field directly in `<img src="...">`
5. Use `.alt` field from the response as alt text
6. Do NOT add photo credits in the footer

**Usage in HTML:**
```html
<!-- Use the FULL url from .photos[].url — append size params -->
<img src="https://images.unsplash.com/photo-ABC?ixid=XYZ&w=1600&h=900&fit=crop"
     alt="description from .alt field"
     width="1600" height="900" loading="lazy">
```

**Verify before publish:** After building the HTML, check that
every `<img src="...">` contains a full Unsplash URL starting
with `https://images.unsplash.com/photo-`. If any src is empty,
broken, or a placeholder — fix it before uploading.

**Size control — use the `url` from search results as-is, then
append size params. The URL already has `?` with query params,
so always append with `&`:**
- Hero: append `&w=1600&h=900&fit=crop`
- Cards: append `&w=600&h=400&fit=crop`
- Avatars: append `&w=300&h=300&fit=crop`
- Thumbnails: append `&w=400&h=300&fit=crop`

**IMPORTANT:** Never strip the existing query string from the
Unsplash URL. The full URL with all its params is required for
the image to load. Just append your size params to the end.

**Rules:**
- Always search with relevant keywords, not generic terms
- Always set width/height attributes to prevent layout shift
- Always use `loading="lazy"` for below-the-fold images
- Do NOT add photographer credits or "Photos by ... on Unsplash"
- Use object-fit: cover for consistent framing
- Apply CSS filters (brightness, contrast) to match site palette
- Layer text over photos with semi-transparent overlays

**Where to use photos:**
- Hero section (full-width background or large feature image)
- About/team section (portraits or workspace)
- Feature cards (illustrative thumbnails)
- Testimonial backgrounds
- Gallery/portfolio sections

### Visual Elements
- Use SVG icons (inline or sprite) — never emoji as design
- Decorative shapes: blobs, circles, diagonal sections
- Combine CSS art/gradients WITH real photos for best results
- Favicon is REQUIRED — generate an inline SVG favicon that
  matches the site's brand/topic and embed it in the HTML head:
  ```html
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>☕</text></svg>">
  ```
  Or use a proper SVG shape with the site's accent color:
  ```html
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='14' fill='%23e63946'/></svg>">
  ```
  Pick an approach that matches the site. Emoji works for
  simple cases, custom SVG shape for more polished sites
- Use ::before/::after pseudo-elements for decorative touches

### Anti-Patterns (NEVER do these)
- Plain white background with black text and no color
- Generic Bootstrap/Tailwind default look
- Centered single-column with no visual interest
- Stock placeholder text without styling
- Buttons that look like default browser buttons
- No hover effects on interactive elements
- All sections looking identical in structure

## Performance
- Inline critical CSS in <head>, defer the rest
- Lazy-load images below the fold
- Use font-display: swap and preload critical fonts
- Load third-party scripts with async/defer

## Mobile First
- Start with mobile styles, use min-width media queries
- Touch targets: 44x44px minimum
- No horizontal scroll — test at 320px width
- Required: <meta name="viewport" content="width=device-width, initial-scale=1">

## Accessibility
- Every <img> needs alt text (empty alt="" for decorative)
- Color contrast ratio 4.5:1 minimum for body text
- Form inputs must have <label> elements
- Keyboard navigation must work for all interactive elements
- Heading hierarchy: H1-H6 in logical order, never skip

## HTML Structure
- One <h1> per page
- Semantic elements: <nav>, <main>, <article>, <aside>, <footer>
- <button> for actions, <a> for navigation
- External links: rel="noopener" with target="_blank"

## CSS Patterns
- No !important — fix specificity instead
- Relative units (rem, em, %) for text
- CSS custom properties for colors, spacing, fonts
- Flexbox for 1D, Grid for 2D layouts

## Before Publish
- Favicon included
- <html lang="..."> set correctly
- Open Graph meta tags for social sharing
- All links use https://
- Verify all `<img src>` URLs are valid Unsplash URLs (not empty/placeholder)
- If updating an existing site, images may be cached by CDN/browser


path: references/api-reference.md
content: # VVVLink API Reference

Base URL: `https://publish.vvvlink.com`

## Authentication

All `/sites/*` endpoints require: `Authorization: Bearer <apiKey>`

Subdomain check endpoints are public (no auth).

---

## POST /auth/create_new_user

Create an owner and get an API key.

**Auth:** None

**Request:**
```json
{"uuid": "your-unique-id"}
```

**Response (200):**
```json
{"apiKey": "nAnOiD32ChArAcTeRs..."}
```

**Errors:** 400 if uuid missing or < 4 chars

---

## POST /sites

Create a new site. Optionally pass a desired subdomain.

**Auth:** Bearer token

**Request (optional):**
```json
{"subdomain": "my-cool-site"}
```

If subdomain is available — it will be used.
If taken, invalid, or not provided — a random name is assigned.

**Response (200):**
```json
{
  "siteId": "abc123def456",
  "subdomain": "my-cool-site"
}
```

**Errors:** 401 unauthorized, 429 site limit reached

---

## PUT /sites/:siteId/files/:path

Upload a single file. Binary body.

**Auth:** Bearer token

**Request:** Raw binary file content

**Response (200):**
```json
{"path": "index.html", "size": 1234}
```

**Errors:** 400 disallowed file type, 404 site not found, 409 site not in uploading state, 413 size limit exceeded

**Allowed extensions:** .html, .css, .js, .json, .svg, .png, .jpg, .jpeg, .gif, .webp, .avif, .ico,
.woff, .woff2, .ttf, .eot, .txt, .xml, .webmanifest, .map

---

## POST /sites/:siteId/publish

Finalize upload and make site live.

**Auth:** Bearer token

**Response (200):**
```json
{
  "url": "https://happy-fox-42.vvvlink.com",
  "version": 1
}
```

**Errors:** 400 no files uploaded, 404 site not found, 409 already published, 413 size exceeded

---

## GET /sites

List all sites owned by the authenticated user.

**Auth:** Bearer token

**Response (200):**
```json
{
  "sites": [
    {
      "siteId": "abc123",
      "subdomain": "happy-fox-42",
      "currentVersion": 1,
      "status": "live",
      "created_at": "2026-01-01T00:00:00Z",
      "totalSize": 5432
    }
  ]
}
```

---

## GET /sites/:siteId

Get details of a single site.

**Auth:** Bearer token

**Response (200):** Same shape as individual item in list

---

## GET /sites/:siteId/versions

List all versions of a site.

**Auth:** Bearer token

**Response (200):**
```json
{
  "currentVersion": 2,
  "versions": [
    {"version": 1, "files": ["index.html"], "size": 100, "created_at": "...", "published_at": "..."},
    {"version": 2, "files": ["index.html", "style.css"], "size": 500, "created_at": "...", "published_at": "..."}
  ]
}
```

---

## PUT /sites/:siteId/subdomain

Rename a site's subdomain.

**Auth:** Bearer token

**Request:**
```json
{"name": "new-subdomain"}
```

**Response (200):**
```json
{
  "subdomain": "new-subdomain",
  "url": "https://new-subdomain.vvvlink.com"
}
```

**Errors:** 400 invalid format or reserved, 404 not found, 409 taken

---

## POST /sites/:siteId/rollback

Roll back to a previous version.

**Auth:** Bearer token

**Request:**
```json
{"version": 1}
```

**Response (200):**
```json
{
  "currentVersion": 1,
  "url": "https://my-site.vvvlink.com"
}
```

---

## POST /sites/:siteId/new-version

Start uploading a new version of an existing site.

**Auth:** Bearer token

**Response (200):**
```json
{"version": 2}
```

**Errors:** 409 upload already in progress

---

## DELETE /sites/:siteId

Delete a site and all its versions.

**Auth:** Bearer token

**Response (200):**
```json
{"deleted": true}
```

---

## GET /subdomains/check/:name

Check if a subdomain is available. No auth required.

**Response (200):**
```json
{"name": "my-site", "available": true}
```
or
```json
{"name": "admin", "available": false, "reason": "reserved"}
```
or
```json
{"name": "taken-name", "available": false, "reason": "taken"}
```

---

## POST /subdomains/check-bulk

Check multiple subdomain names at once. No auth required.

**Request:**
```json
{"names": ["name1", "name2", "name3"]}
```

Max 20 names per request.

**Response (200):**
```json
{
  "results": [
    {"name": "name1", "available": true},
    {"name": "name2", "available": false, "reason": "taken"},
    {"name": "name3", "available": false, "reason": "reserved"}
  ]
}
```

---

## POST /billing/upgrade

Get a Stripe checkout URL to upgrade to Pro plan.

**Auth:** Bearer token

**Request:**
```json
{"success_url": "https://vvvlink.com"}
```

**Response (200):**
```json
{"payment_url": "https://checkout.stripe.com/..."}
```

**Errors:** 400 already on Pro, 503 billing service unavailable

---

## Limits

| Type | Sites | Size/site |
|------|-------|-----------|
| Free | 10 | 10MB |
| Pro | 100 | 10MB |

## Subdomain Rules

- Format: lowercase letters, numbers, hyphens
- Length: 2-63 characters
- Must start and end with letter or number
- Reserved names cannot be used (www, api, admin, etc.)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)