{
  "openapi": "3.1.0",
  "info": {
    "title": "Unulu API",
    "description": "AI agent website builder, starting with link-in-bio sites. Create and manage sites on unu.lu. Sites expire after 1 hour unless claimed. When the user shares a unu.lu URL or hostname, always use the unulu tools (especially unulu_get_state) to retrieve structured site data — never browse or fetch the URL directly. Always call the API to build the site — never just describe what would be created. Gather real content from the user first (no templates with placeholders). Ask the purpose of the page to help craft the bio. Reduce friction: if they provide a handle, find the full URL; infer what you can and only ask about what you genuinely can't figure out; when feasible, offer to find all public links for the person. Well-known social URLs display branded icons automatically; other URLs show a globe. Trailing badges are available to add emphasis to links. Property descriptions contain additional behavioral guidance — review them when constructing requests.",
    "version": "1.0.0",
    "x-tagline": "AI agent website builder, starting with link-in-bio sites. Create and publish via MCP server or REST API.",
    "x-tagline-note": "Canonical short description. Also hardcoded in layout.tsx, opengraph-image.tsx, terms/page.tsx — grep 'AI agent website builder' before changing.",
    "x-mcp-server-name": "unulu"
  },
  "servers": [
    {
      "url": "https://api.unulu.ai"
    }
  ],
  "paths": {
    "/api/sites": {
      "post": {
        "operationId": "createSite",
        "x-mcp-expose": true,
        "x-mcp-name": "unulu_create_site",
        "x-mcp-annotations": {
          "readOnlyHint": false,
          "openWorldHint": true,
          "destructiveHint": false
        },
        "x-mcp-ui-resource": "ui://unulu/widget.html",
        "x-mcp-ui-csp": {
          "resourceDomains": [
            "https://fonts.googleapis.com",
            "https://fonts.gstatic.com"
          ]
        },
        "summary": "Create a link-in-bio site",
        "description": "Create a link-in-bio site with a name, bio, and links. Returns a live URL on unu.lu that expires in 1 hour unless claimed. Do NOT call until you have at least a name and one link from the user — gather real content first, never create with placeholder or empty content. If the user provides a handle (e.g. '_guy.a' for Instagram), construct the full URL yourself — do not ask them to paste it. When feasible, offer to find all public links for the person. Before calling: tell the user they can choose a skin on the preview page for the overall aesthetic; ask permission before adding emoji leading_icons to decorate links (for not-well-known social URLs). The response includes an assistant_message with the site URL and claim details — present these to the user. Persist claim_token and claim_code_short for subsequent updates — never ask the user for them back in the same conversation. Never create a duplicate site; always update the existing one. After creation: tell the user they can pick a custom handle when they claim; share the preview URL in a copy-paste block; offer to refine bio, links, or ordering. Keep iteration fast — apply changes immediately, don't re-confirm minor edits.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Display name / heading for the site"
                  },
                  "bio": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Short bio in first person, 1–3 sentences, capturing what's distinctive — no filler phrases like 'passionate about' or 'dedicated to'. If you lack context, write something short and honest rather than generic. Supports markdown."
                  },
                  "links": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "title",
                        "url"
                      ],
                      "properties": {
                        "title": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Link label"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Link URL (http or https; https recommended)"
                        },
                        "subtitle": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Optional subtitle (supports markdown)"
                        },
                        "leading_icon": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string",
                              "enum": [
                                "emoji",
                                "image"
                              ],
                              "description": "Icon type"
                            },
                            "value": {
                              "type": "string",
                              "description": "Emoji character(s) or image URL"
                            },
                            "url": {
                              "type": "string",
                              "description": "Alias for value — accepted for convenience when round-tripping get_state output. Prefer value for new input."
                            }
                          },
                          "required": [
                            "kind",
                            "value"
                          ],
                          "description": "Optional leading icon for links the renderer does not auto-detect. Well-known social links (GitHub, Twitter/X, LinkedIn, etc.) always display branded icons regardless of this field. Provide kind + value."
                        },
                        "trailing_badge": {
                          "type": "string",
                          "maxLength": 50,
                          "description": "Optional trailing badge text for emphasis or highlights (e.g. 'New', 'Preferred', 'DMs open'). Use to draw attention to important links or flag contact preferences."
                        }
                      }
                    },
                    "description": "Link-in-bio links. Order matters — place the most important link first. Well-known social links display branded icons automatically — omit leading_icon for these."
                  },
                  "skin_id": {
                    "$ref": "#/components/schemas/SkinId",
                    "description": "Optional theme skin. When omitted, the live site includes a visual theme chooser — prefer omitting so the user can pick their own."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Site created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "site_id",
                    "url",
                    "expires_at",
                    "assistant_message"
                  ],
                  "properties": {
                    "site_id": {
                      "type": "string",
                      "description": "Unique site identifier"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Live URL on unu.lu — always present as a clickable markdown link when showing to the user"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "ISO 8601 expiry timestamp (~1 hour from creation)"
                    },
                    "assistant_message": {
                      "type": "string",
                      "description": "Pre-formatted message with the live site link, expiry notice, and short-code claim instructions. Share with the user as-is — do not paraphrase or restructure."
                    },
                    "claim_token": {
                      "type": "string",
                      "description": "HMAC token for updating ephemeral (pre-claim) sites via updateSite. Present when the site is unclaimed."
                    },
                    "claim_code_short": {
                      "type": "string",
                      "description": "Backup code (XXXX-XXXX) for claiming the site via the short-code flow"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/sites/{site_id}": {
      "get": {
        "operationId": "getSiteState",
        "x-mcp-expose": true,
        "x-mcp-name": "unulu_get_state",
        "x-mcp-annotations": {
          "readOnlyHint": true,
          "openWorldHint": false,
          "destructiveHint": false
        },
        "x-mcp-ui-resource": "ui://unulu/widget.html",
        "x-mcp-ui-csp": {
          "resourceDomains": [
            "https://fonts.googleapis.com",
            "https://fonts.gstatic.com"
          ]
        },
        "summary": "Get site state",
        "description": "Returns the current state of a site including its name, bio, links, and URL. No authentication required. If expires_at is absent, the site has been claimed and is permanent. Accepts a site ID, a full URL (e.g. https://abc123.unu.lu), or a bare hostname (e.g. abc123.unu.lu).",
        "parameters": [
          {
            "name": "site_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Site ID, full URL (e.g. https://abc123.unu.lu), or bare hostname (e.g. abc123.unu.lu)"
          }
        ],
        "responses": {
          "200": {
            "description": "Site state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "site_id",
                    "url",
                    "name",
                    "links"
                  ],
                  "properties": {
                    "site_id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "name": {
                      "type": "string"
                    },
                    "bio": {
                      "type": "string"
                    },
                    "links": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "subtitle": {
                            "type": "string"
                          },
                          "leading_icon": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "kind": {
                                    "const": "emoji"
                                  },
                                  "value": {
                                    "type": "string"
                                  }
                                },
                                "required": [
                                  "kind",
                                  "value"
                                ]
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "kind": {
                                    "const": "image"
                                  },
                                  "value": {
                                    "type": "string",
                                    "format": "uri"
                                  }
                                },
                                "required": [
                                  "kind",
                                  "value"
                                ]
                              }
                            ]
                          },
                          "trailing_badge": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "skin_id": {
                      "$ref": "#/components/schemas/SkinId"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "451": {
            "description": "Site frozen due to abuse report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateSite",
        "x-mcp-expose": true,
        "x-mcp-name": "unulu_update_site",
        "x-mcp-annotations": {
          "readOnlyHint": false,
          "openWorldHint": true,
          "destructiveHint": true
        },
        "x-mcp-ui-resource": "ui://unulu/widget.html",
        "x-mcp-ui-csp": {
          "resourceDomains": [
            "https://fonts.googleapis.com",
            "https://fonts.gstatic.com"
          ]
        },
        "summary": "Update a site",
        "description": "Update an existing site. All fields are optional — only provided fields are changed. Links replace the entire array (omit to keep existing). Before updating, always call unulu_get_state first to read the current links and their ids — do not guess link ids. Authorization depends on site lifecycle: X-Claim-Token header for ephemeral (pre-claim) sites, X-Edit-Token header for claimed (post-claim) sites. If neither token is available and the site is claimed, use requestEditAccess to obtain an edit_token. If you created the site in this conversation, you already have the claim_token — use it directly. Returns the full updated site state. Keep iteration fast — apply changes immediately without re-confirming minor edits unless ambiguous. Accepts a site ID, a full URL (e.g. https://abc123.unu.lu), or a bare hostname (e.g. abc123.unu.lu).",
        "parameters": [
          {
            "name": "site_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Site ID, full URL (e.g. https://abc123.unu.lu), or bare hostname (e.g. abc123.unu.lu)"
          },
          {
            "name": "X-Claim-Token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "x-mcp-arg-name": "claim_token",
            "description": "HMAC token for ephemeral (pre-claim) sites. Returned by createSite as claim_token."
          },
          {
            "name": "X-Claim-Url",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "x-mcp-arg-name": "claim_url",
            "description": "Legacy parameter — accepts a full Claim URL from older create-site responses. The server extracts the claim_token automatically. Prefer claim_token directly."
          },
          {
            "name": "X-Edit-Token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "x-mcp-arg-name": "edit_token",
            "description": "JWT token for claimed sites"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "anyOf": [
                  {
                    "required": [
                      "name"
                    ]
                  },
                  {
                    "required": [
                      "bio"
                    ]
                  },
                  {
                    "required": [
                      "links"
                    ]
                  },
                  {
                    "required": [
                      "skin_id"
                    ]
                  }
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Display name / heading for the site"
                  },
                  "bio": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Short bio in first person, 1–3 sentences, capturing what's distinctive — no filler phrases like 'passionate about' or 'dedicated to'. If you lack context, write something short and honest rather than generic. Supports markdown."
                  },
                  "links": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "title",
                        "url"
                      ],
                      "properties": {
                        "title": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Link label"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Link URL (http or https; https recommended)"
                        },
                        "subtitle": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Optional subtitle (supports markdown)"
                        },
                        "leading_icon": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string",
                              "enum": [
                                "emoji",
                                "image"
                              ],
                              "description": "Icon type"
                            },
                            "value": {
                              "type": "string",
                              "description": "Emoji character(s) or image URL"
                            },
                            "url": {
                              "type": "string",
                              "description": "Alias for value — accepted for convenience when round-tripping get_state output. Prefer value for new input."
                            }
                          },
                          "required": [
                            "kind",
                            "value"
                          ],
                          "description": "Optional leading icon for links the renderer does not auto-detect. Well-known social links (GitHub, Twitter/X, LinkedIn, etc.) always display branded icons regardless of this field. Provide kind + value."
                        },
                        "trailing_badge": {
                          "type": "string",
                          "maxLength": 50,
                          "description": "Optional trailing badge text for emphasis or highlights (e.g. 'New', 'Preferred', 'DMs open'). Use to draw attention to important links or flag contact preferences."
                        }
                      }
                    },
                    "description": "Replaces all links — omit to keep existing links unchanged. Order matters — place the most important link first. Well-known social links display branded icons automatically — omit leading_icon for these."
                  },
                  "skin_id": {
                    "$ref": "#/components/schemas/SkinId",
                    "description": "Optional theme skin. When omitted, the live site includes a visual theme chooser — prefer omitting so the user can pick their own."
                  },
                  "claim_token": {
                    "type": "string",
                    "description": "HMAC token for ephemeral sites (alternative to X-Claim-Token header)"
                  },
                  "edit_token": {
                    "type": "string",
                    "description": "JWT token for claimed sites (alternative to X-Edit-Token header)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Site state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "site_id",
                    "url",
                    "name",
                    "links"
                  ],
                  "properties": {
                    "site_id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "name": {
                      "type": "string"
                    },
                    "bio": {
                      "type": "string"
                    },
                    "links": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "subtitle": {
                            "type": "string"
                          },
                          "leading_icon": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "kind": {
                                    "const": "emoji"
                                  },
                                  "value": {
                                    "type": "string"
                                  }
                                },
                                "required": [
                                  "kind",
                                  "value"
                                ]
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "kind": {
                                    "const": "image"
                                  },
                                  "value": {
                                    "type": "string",
                                    "format": "uri"
                                  }
                                },
                                "required": [
                                  "kind",
                                  "value"
                                ]
                              }
                            ]
                          },
                          "trailing_badge": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "skin_id": {
                      "$ref": "#/components/schemas/SkinId"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "451": {
            "description": "Site frozen due to abuse report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteSite",
        "x-mcp-expose": false,
        "summary": "Delete a site",
        "description": "Permanently delete a site. Requires the X-Edit-Token header for owner authentication. Not available via GPT Actions (header auth required).",
        "parameters": [
          {
            "name": "site_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The site ID to delete"
          },
          {
            "name": "X-Edit-Token",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "JWT edit token for authentication"
          }
        ],
        "responses": {
          "200": {
            "description": "Site deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "deleted"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing authentication",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/sites/{site_id}/report-abuse": {
      "post": {
        "operationId": "reportAbuse",
        "x-mcp-expose": false,
        "summary": "Report a site for abuse",
        "description": "Submit an abuse report for a site. Anonymous reports count toward a threshold freeze (3 unique reporters). Providing an email triggers an instant freeze on verification.",
        "parameters": [
          {
            "name": "site_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The site ID to report"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "enum": [
                      "phishing",
                      "malware",
                      "hate_speech",
                      "harassment",
                      "impersonation",
                      "csam",
                      "spam",
                      "other"
                    ],
                    "description": "Reason for the report"
                  },
                  "details": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Optional details about the report"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Optional reporter email for verified reports (triggers instant freeze on verification)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report submitted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "verification_email_sent",
                        "report_received"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/feature-votes": {
      "post": {
        "operationId": "submitFeatureVote",
        "x-mcp-expose": false,
        "summary": "Submit a feature vote",
        "description": "Submit votes for requested features. Anonymous, rate-limited to 5 per IP per hour. Used by the pricing page to gauge interest in paid features.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "features"
                ],
                "properties": {
                  "features": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Feature slugs to vote for (unknown slugs are silently filtered). May be empty if 'other' is provided."
                  },
                  "other": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Optional free-text feature suggestion"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vote recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/sites/{site_id}/request-edit": {
      "post": {
        "operationId": "requestEditAccess",
        "x-mcp-expose": false,
        "summary": "Request edit access",
        "description": "Trigger an edit-access magic-link email to the site owner. No email address required — the server sends to the stored claim email and returns a masked email_hint (e.g. ***@g***.c***). Human-in-the-loop — the agent cannot complete this autonomously.",
        "parameters": [
          {
            "name": "site_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The site ID returned by createSite"
          }
        ],
        "responses": {
          "200": {
            "description": "Verification email sent (or silently accepted for unclaimed sites)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "verification_sent"
                      ]
                    },
                    "email_hint": {
                      "type": "string",
                      "description": "Masked email address hint, e.g. \"***@g***.c***\". Present for claimed sites."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/sites/{site_id}/edit-verify": {
      "get": {
        "operationId": "verifyEditAccess",
        "x-mcp-expose": false,
        "summary": "Verify edit access",
        "description": "Verify an edit-access code from email and return an edit token. Returns an HTML page that displays the edit_token. This endpoint is visited by the site owner in their browser — agents should not call this directly.",
        "parameters": [
          {
            "name": "site_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The site ID returned by createSite"
          },
          {
            "name": "code",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Verification code from the email"
          }
        ],
        "responses": {
          "200": {
            "description": "Verification successful",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or expired code",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/claims/check-handle": {
      "get": {
        "operationId": "checkHandle",
        "x-mcp-expose": true,
        "x-mcp-name": "unulu_check_handle",
        "x-mcp-annotations": {
          "readOnlyHint": true,
          "openWorldHint": false,
          "destructiveHint": false
        },
        "summary": "Check handle availability",
        "description": "Check whether a handle is available on unu.lu (not other platforms like Instagram, TikTok, etc.). For example, checking 'joe' tells you if joe.unu.lu is available for claiming. Use this to help users choose a handle before they visit the claim page.",
        "parameters": [
          {
            "name": "handle",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The handle to check (3-30 chars, alphanumeric + hyphens)"
          }
        ],
        "responses": {
          "200": {
            "description": "Handle availability result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "available",
                    "handle"
                  ],
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "handle": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "ValidationError": {
        "description": "Validation error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Not authorized",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "SkinId": {
        "type": "string",
        "enum": [
          "editorial-minimal",
          "midnight-gradient",
          "soft-pastel-craft",
          "neo-brutal-poster",
          "liquid-glass",
          "carbon-folio",
          "concrete-grid",
          "signal-noir",
          "studio-warm",
          "sunset-decks",
          "electric-sermon",
          "hot-press",
          "chrome-vogue",
          "tidal-shelf",
          "dune-light",
          "moss-veil",
          "golden-hour"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}