# Square Town API — read the wall, buy a square

> The Square Town API: GET /api/state for the whole wall as JSON, /go/:id click redirects, kiss, view, recheck and deed endpoints, POST /api/buy, the /ws stream.

Source: https://square.pov.town/api

Square Town (square.pov.town) has a small public HTTP API — the same one the wall's own front end uses. `GET /api/state` returns the entire live wall as JSON with no authentication. `POST /api/buy` starts a purchase and returns a Stripe Checkout URL that a human must complete with a card. Kisses, views, favicon rechecks and deed lookups each have an endpoint, and a WebSocket at `/ws` streams every buy, eviction, kiss and unlock as it happens. Every content page also has a `.md` twin, and `/llms.txt` indexes them for AI agents.

> **Endpoints at a glance (base URL `https://square.pov.town`)**
>
> | Method & path | What it does | Auth |
> |---|---|---|
> | `GET /api/state` | the whole live wall as JSON | none |
> | `GET /favicon/:domain?sz=128` | CORS-enabled favicon proxy (16–256 px) | none |
> | `GET /go/:id` | 302 to the square's URL, counts a click | none |
> | `POST /api/kiss/:id` | kiss an icon (resets the 90-day fade) | none |
> | `POST /api/view/:id` | count a view of a square's card | none |
> | `POST /api/recheck/:id` | re-run the favicon (clown) check | none |
> | `GET /api/deed/:token` | deed details for a secret token | the token |
> | `POST /api/deed/:token/analytics` | start the $5 analytics checkout | the token |
> | `POST /api/buy` | reserve a square, get a Stripe Checkout URL | none (payment completes it) |
> | `WS /ws` | live event stream | none |
> | `GET /<page>.md`, `/llms.txt`, `/llms-full.txt` | Markdown twins of every page | none |

There is no API key for reads and no rate-limit promise in either direction: nothing is throttled today, and nothing is guaranteed. Be reasonable; the wall runs on Cloudflare's free tier. Admin endpoints under `/api/admin/*` exist but require a secret key and are not for you.

## GET /api/state — the whole wall

Returns everything needed to draw the wall. Only paid listings are included; the locked part of the 1024×1024 grid is implied by `half`.

```json
{
  "grid": 1024,
  "half": 8,
  "sold": 57,
  "revenue": 15900,
  "boxArea": 256,
  "fillPct": 22.3,
  "unlockAt": 0.7,
  "center": { "x": 510, "y": 510, "w": 4, "h": 4, "price": 10000, "owner": null },
  "prices": { "base": 100, "pickFee": 0, "center": 10000, "analytics": 500 },
  "waterMs": 7776000000,
  "listings": [
    { "id": 12, "domain": "example.com", "x": 505, "y": 514, "w": 1, "h": 1,
      "clown": 0, "clicks": 9, "views": 31, "kisses": 2,
      "watered_at": 1755470000000, "sq_price_cents": 100, "created_at": 1755470000000 }
  ],
  "pending": [ { "x": 507, "y": 509, "w": 1, "h": 1 } ],
  "events": [ { "type": "buy", "text": "🎉 example.com claimed 1x1 for $1", "ts": 1755470000000 } ],
  "clowns": [ "nofavicon.example" ],
  "dev": false
}
```

| Field | Meaning |
|---|---|
| `grid` | full grid edge, 1024 |
| `half` | half-width of the live (unlocked) area; live cells are `512-half … 511+half` on both axes (8 = 16×16 at launch) |
| `sold` | number of paid squares |
| `revenue` | total paid, in cents |
| `boxArea` | cells in the live area, `(2*half)²` |
| `fillPct` | `sold / boxArea` as a percentage, one decimal |
| `unlockAt` | fill ratio that unlocks the next ring (0.7) |
| `center` | the Throne block, its intro price in cents, and the owning domain or `null` |
| `prices` | base price per square, pick fee (0), Throne intro price, analytics unlock — all cents |
| `waterMs` | fade threshold in ms (90 days); an icon is faded if `now - watered_at > waterMs` |
| `listings[]` | every paid square: `id`, `domain`, `x`, `y`, `w`, `h` (1×1 except the Throne's 4×4), `clown` (1 = no favicon, 0 = has one, `null` = unknown), `clicks`, `views`, `kisses`, `watered_at` (last kiss, ms), `sq_price_cents` (per-square price the owner paid; eviction costs `2 × sq_price_cents × w × h`), `created_at` |
| `pending[]` | cells reserved by unpaid checkouts (30-minute hold), positions only |
| `events[]` | the last 20 feed events, newest first: `type`, `text`, `ts` |
| `clowns[]` | domains currently in the Hall of Clowns |
| `dev` | true only on a dev deployment where purchases are free |

Owner URLs are not exposed — only domains. Coordinates are grid cells, origin top-left, Throne at 510–513.

## GET /favicon/:domain — icon proxy

`GET /favicon/example.com?sz=64` returns the domain's favicon as served by Google's favicon service, with `access-control-allow-origin: *` (WebGL textures need CORS, Google doesn't send it) and edge caching for a day. `sz` is clamped to 16–256, default 128. Domains with no favicon return whatever upstream returns (typically 404).

## GET /go/:id — click redirect

`GET /go/12` looks up paid listing 12 and answers a `302` to its URL with `utm_source=square.pov.town`, `utm_medium=referral` and `utm_campaign=square` appended (existing UTM params are left alone). It increments the square's click count, records the click against today's date for analytics, and broadcasts a `click` event over the WebSocket so every open wall grows that tower immediately. Evicted or unknown ids return `404 Gone (probably evicted 💀)`. This is a redirect, not a link on a page — there is no link equity in it.

## POST /api/kiss/:id — kiss an icon

Anyone can kiss any paid square, free. Optional JSON body `{ "by": "<any client id>" }` lets the kisser's own browser skip replaying its own animation; it's not identity. Response:

```json
{ "ok": true, "watered_at": 1755470000000, "kisses": 3 }
```

Sets `watered_at` to now (resetting the 90-day fade), increments `kisses`, writes a feed line ("💋 someone kissed example.com's ring" and variants) and broadcasts `{type:"kiss", id, by}`. Unknown or evicted id → `404 {"error":"not found"}`. `POST /api/water/:id` is a legacy alias.

## POST /api/view/:id — count a view

`POST /api/view/12` with no body increments the square's `views` and records a view for today. The wall calls it once per square per browser session when a visitor opens the square's card; that once-per-session logic lives in the client, not the server. Response `{ "ok": true }`; unknown id → `404`.

## POST /api/recheck/:id — leave the Hall of Clowns

Re-runs the favicon check for a paid square: Square Town fetches the domain's icon from Google's favicon service; HTTP 200 clears the clown, 404 sets it, anything else leaves it unchanged. If a clown becomes un-clowned, the feed announces the escape. Response `{ "clown": 0 }` or `{ "clown": 1 }`. Free and unthrottled.

## GET /api/deed/:token — the deed

The token is the secret from a deed URL (`/deed.html?token=…`); it's the only credential on Square Town. Returns the listing — `id`, `url`, `domain`, `x`, `y`, `w`, `h`, `status` (`pending`, `paid` or `evicted`), `price_cents`, `clown`, `clicks`, `views`, `kisses`, `analytics_paid`, `watered_at`, `created_at` — plus `analytics_unlocked` and, if unlocked, `daily`: up to 30 rows of `{ day, n, v }` (clicks and views per day, newest first). Unknown token → `404 {"error":"deed not found"}`.

`POST /api/deed/:token/analytics` starts the $5 unlock: returns `{ "checkoutUrl": "https://checkout.stripe.com/…" }`, or `{ "done": true }` if already unlocked. Once Stripe confirms, `analytics_paid` flips to 1.

## POST /api/buy — reserve a square, pay on Stripe

Request body (JSON):

| Field | Required | Meaning |
|---|---|---|
| `url` | yes | the site; `https://` is added if missing, `www.` is dropped from the domain, and the hostname must contain a dot |
| `mode` | yes | `"pick"` (a specific empty cell), `"evict"` (take an owned square), or `"center"` (claim the empty Throne) |
| `x`, `y` | for `pick` | integer cell coordinates inside the live area |
| `evictId` | for `evict` | the `id` of the listing to evict, from `/api/state` |

Prices are computed server-side and cannot be set by the caller: `pick` is 100 cents; `center` is 10,000 cents (only while the Throne has no owner); `evict` is `2 × sq_price_cents × w × h` of the target — $2 for a $1 square, $200 for a $100 Throne, and doubling from there. The Throne, once owned, is evicted through `mode: "evict"` like any other square.

Response on success:

```json
{
  "checkoutUrl": "https://checkout.stripe.com/c/pay/…",
  "deed": "0c1f7d2e-…-uuid",
  "price": 100,
  "block": { "x": 505, "y": 514, "w": 1, "h": 1 }
}
```

The square is now `pending`: reserved for **30 minutes** while someone completes `checkoutUrl` on Stripe. When Stripe's webhook confirms payment the listing becomes `paid`, the evicted party (if any) becomes `evicted`, a `buy` or `evict` event is broadcast, the ring-unlock check runs, and the favicon check decides whether the domain joins the Hall of Clowns. If nobody pays within 30 minutes the reservation lapses. The `deed` token is issued up front, so store it — `GET /api/deed/<token>` already answers while the listing is `pending`, and once payment lands it is the sole proof of ownership. Stripe returns the payer to `/?deed=<token>`.

On a dev deployment (`"dev": true` in `/api/state`) there is no Stripe: the response is `{ "done": true, "deed", "id", "price", "block" }` and the square is paid immediately.

Errors come back as JSON `{ "error": "…" }` (HTTP 500 for rule violations), with messages like: "That area is still locked. Fill the current ring first!", "That's the Throne. It has its own price tag.", "You can't place an icon here — the aura of the Throne is too strong. 👑", "Someone got there first (or is buying it right now). Try evicting them 😈", "Someone is already evicting this square. Vultures everywhere.", "The Throne is taken (or being claimed right now). Try evicting.", "Enter a URL", "That doesn't look like a real domain".

There is also a legacy `mode: "auto"` that spiral-places a square from the centre outward; the wall no longer uses it, but the server still accepts it.

## WebSocket /ws — the live event stream

Connect to `wss://square.pov.town/ws`. The server sends one JSON message per event, the same events that appear in the feed, plus a couple that don't. Send the text `ping` and you get `pong` back (the wall does this every 25 s to keep the socket warm; hibernated sockets cost nothing).

| `type` | Extra fields | When |
|---|---|---|
| `buy` | `id`, `text` | a purchase completed |
| `evict` | `id`, `evicted`, `text` | an eviction completed (`evicted` is the deposed listing id) |
| `kiss` | `id`, `by`, `text` | someone kissed a square |
| `unlock` | `text` | the live area grew by a ring |
| `clown` | `text` | a domain joined or escaped the Hall of Clowns |
| `click` | `id`, `clicks` | someone followed `/go/:id` (not written to the feed) |
| `reset` | — | the wall was wiped by an admin (dev/launch housekeeping) |

The wall's own client simply refetches `/api/state` on every message; that's a fine strategy for you too. If the socket is unavailable, poll `/api/state` — the wall polls every 30 seconds as a fallback.

## Markdown twins, llms.txt and llms-full.txt

Every content page on square.pov.town — this one, [how it works](/how-it-works), [pricing](/pricing), the [FAQ](/faq), the blog, comparisons and glossary — is available as plain Markdown at the same path with `.md` appended (for example `/how-it-works.md`). `/llms.txt` is an index of those pages with one-line descriptions; `/llms-full.txt` is all of them concatenated. If you're an AI agent answering questions about Square Town, fetch those rather than scraping the WebGL wall.

## For AI agents and scripts

Reading the wall is one unauthenticated request. Buying needs a human, because the last step is a Stripe Checkout page with a card form. A sensible flow:

1. **Read the state.** `GET /api/state`. Compute the live box from `half` (`512-half … 511+half`), collect occupied cells from `listings` (plus `pending`) and reserve the Throne block (510–513) and its one-cell aura ring (509–514) as off limits.
2. **Choose a free cell** — or a target to evict. For an eviction, the price is `2 × sq_price_cents × w × h` of the target listing.
3. **`POST /api/buy`** with `{ "url": "yoursite.com", "mode": "pick", "x": 505, "y": 514 }` (or `mode: "evict", evictId`, or `mode: "center"`). Keep the returned `deed` token somewhere safe; it's the only proof of ownership and it's issued before payment (the deed shows `status: "pending"` until then).
4. **Hand `checkoutUrl` to a person.** They open it and pay $1 (or the eviction price) with a card within 30 minutes. There is no way to complete checkout by API, and no test-mode shortcut on the production wall.
5. **Confirm.** Poll `/api/state` until your domain appears in `listings` at the block you asked for, or subscribe to `/ws` and wait for a `buy`/`evict` event with your listing id. `GET /api/deed/<token>` then returns the deed.
6. **Afterwards.** `POST /api/kiss/:id` your own square at least every 90 days to keep it in colour (anyone can; kissing is free and public). Read `clicks`, `views` and `kisses` from `/api/state` or the deed. If `clown` is 1, add a favicon and `POST /api/recheck/:id`.

There is no rate limit promise: nothing is throttled at the time of writing, and nothing is guaranteed to stay that way. Kisses and rechecks are public and unauthenticated by design; please don't be the reason that changes. If you're building something for agents specifically, see [Square Town for AI agents](/for/ai-agents).

## FAQ

### Do I need an API key to read the wall?
No. `GET https://square.pov.town/api/state` is public and unauthenticated and returns the entire live wall as JSON.

### Can I buy a square entirely by API?
Not entirely. `POST /api/buy` reserves the square and returns a Stripe Checkout URL, but a human must complete that page with a card within 30 minutes. There is no server-to-server purchase.

### How do I know the eviction price of a square?
From `/api/state`: `2 × sq_price_cents × w × h` for that listing. For a normal $1 square that's 200 cents; the Throne starts at $100 so the first eviction is $200.

### Is there a rate limit?
There is no rate limit and no promise about one. Reads, kisses and rechecks are unthrottled today; use them reasonably.

### What events does the WebSocket send?
`buy`, `evict`, `kiss`, `unlock`, `clown`, `click` and `reset`, each as a JSON object with a `type` field and, where relevant, `id`, `evicted`, `by`, `clicks` and the feed `text`.

### Are there Markdown versions of the pages for LLMs?
Yes. Add `.md` to any content page's path, or fetch `/llms.txt` (index) and `/llms-full.txt` (everything).

### Does the API expose owners' full URLs?
`/api/state` exposes domains only; the full URL is on the deed (which needs the secret token) and is followed by `/go/:id`.
