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 claims a square — free, instant, no card — or, for an eviction or the Throne, returns a Stripe Checkout URL that a human must complete with a card. POST /api/bulldoze is the eviction that takes nothing: same price, the square just ends up empty. Kisses, views, favicon rechecks and whacks each have an endpoint — all anonymous, with an optional free token from POST /api/register for callers who want their own kiss budget — deed lookups use the deed token, and a WebSocket at /ws streams every claim, 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/statethe whole live wall as JSON none GET /api/historyevery square that ever stood, with its lifespan — the wall at any past moment none GET /api/eventsolder live-feed events, newest first, paged — the last 5 days none GET /api/drDomain Rating (by Ahrefs) for every rated domain ever on the wall none GET /api/categoriescategory slugs for every classified domain ever on the wall none GET /api/square/:keyone square in full (title, description, counters) — by id, slug or domain none GET /api/search?q=search the wall by domain, title or description none GET /favicon/:domain?sz=128CORS-enabled favicon proxy (16–256 px) none GET /go/:key302 to the square's URL; counts the click (one per square per address per hour) none GET /s/:slugthe square's public share page (per-square title and description for unfurls; humans are sent to the wall with the card open) none GET /badge/:slug.svga live "I own a square" badge to embed, linking back to /s/:slugnone POST /api/registerget a token (free, instant); optionally attach an email to verify none GET /api/methe account behind a token token POST /api/kiss/:idkiss an icon (free, public, counted) none; a token gives you your own kiss budget POST /api/view/:idcount a view of a square's card, returns the square in full none POST /api/recheck/:idre-run the favicon (clown) check none POST /api/site-check/:idcheck a homepage again; extinguish its fire on recovery none POST /api/whack/:idwhack a clown square off the wall (free claims past their 24 h grace only) none GET /api/deed/:tokendeed details for a secret token the token POST /api/deed/:token/analyticsstart the $5 analytics checkout the token POST /api/buyclaim a free square instantly, or reserve an eviction / the Throne and get a Stripe Checkout URL none (an email address is required; payment completes evictions) POST /api/bulldozeevict a square without taking it — pay the eviction price, the lot reopens at $0 none (no URL, no email — anonymous by design) WS /wslive event stream none GET /<page>.md,/llms.txt,/llms-full.txtMarkdown twins of every page none
There is no API key for anything. The town's verbs — kiss, view, recheck, whack, and having a /go/ click counted — are open to everyone, held to per-IP limits; an optional token (below) gives an API caller its own budget. Rate limits are per IP (30 writes a minute, 3 registrations a minute) and are not a promise in either direction. Be reasonable; the wall runs on Cloudflare's smallest paid plan. Admin endpoints under /api/admin/* exist but require a secret key and are not for you.
Auth
There isn't any, for the wall. Every write below works anonymously; a token is an optional extra for API callers.
Reads are anonymous. So are the town's verbs — POST /api/kiss, /api/view, /api/recheck, /api/whack, and the click count behind GET /go/:id — held honest per network (5 kisses per square per hour, one counted click per square per hour, the nightly sweep — the kiss endpoint below has the numbers). A token gives an API caller its own kiss budget instead of sharing the anonymous per-IP one, and credits the acts to an account. It's free and instant — no email, no mail, nothing to click:
curl -X POST https://square.pov.town/api/register
# → { "ok": true, "token": "sq_…", "email": null, "verified": false, "created_at": 1756800000000 }
Send it on writes as Authorization: Bearer <token> (an x-token header or an sq_token cookie also work); GET /api/me with the header echoes the account. An unknown token is simply anonymous — the write still goes through on the IP's budget; only /api/me answers 401.
curl -X POST https://square.pov.town/api/kiss/12 -H 'authorization: Bearer sq_…'
curl https://square.pov.town/api/me -H 'authorization: Bearer sq_…'
Email is optional. Pass {"email": "you@example.com"} when registering, or later with the token, to attach an address to the account. It's stored unverified and a verification mail goes out (at most 3 a day per address); clicking the link stamps the account verified, and verified in /api/me flips to true. Nothing is gated on it today — it's a fact Square Town may build on later (a hand-picked favicon for verified owners, say). Until verified the address is a label, not an identity: the same address can sit on several tokens, and registering it again never returns someone else's token. There is no "forgot my token" flow; a lost token is a POST /api/register away from a new one.
curl -X POST https://square.pov.town/api/register -H 'authorization: Bearer sq_…' \
-H 'content-type: application/json' -d '{"email":"you@example.com"}'
# → { "ok": true, "token": "sq_…", "email": "you@example.com", "verified": false, "verify_sent": true, … }
Bad address → 400; /api/register is limited to 3 calls per minute per IP (429). Keep the token to yourself: anyone holding it kisses as you.
GET /api/state — the whole wall
Returns everything needed to draw the wall. Only standing (status claimed) listings are included — a free claim and a paid eviction both count; the locked part of the 1024×1024 grid is implied by half.
{
"grid": 1024,
"half": 8,
"sold": 57,
"revenue": 15900,
"boxArea": 256,
"fillPct": 22.3,
"unlockAt": 0.75,
"center": { "x": 510, "y": 510, "w": 4, "h": 4, "price": 10000, "owner": null },
"prices": { "base": 0, "pickFee": 0, "center": 10000, "analytics": 500 },
"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": 0, "created_at": 1755470000000 }
],
"pending": [ { "x": 507, "y": 509, "w": 1, "h": 1 } ],
"events": [ { "type": "buy", "text": "🎉 example.com claimed 1x1 for free", "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 claimed squares (the name is historical — claims are free) |
revenue |
total paid, in cents — evictions, the Throne and analytics unlocks; free claims add nothing |
boxArea |
cells in the live area, (2*half)² |
water, land, aura, raised, purchasable |
of those cells: lakes (deterministic terrain, not for sale), land, the land cells of the Throne's aura ring (never for sale), cells the town raised into hills (off the market), and purchasable = land − aura − raised — what can actually be claimed (the Throne's 16 cells included, as one block) |
forSale |
free squares you could claim right now: purchasable − sold − pending, minus the Throne block while it is unowned (that's a separate $100 item) |
fillPct |
sold / purchasable as a percentage, one decimal |
nextRingAt |
cells sold at which the next ring unlocks (ceil(purchasable × 0.75)) |
unlockAt |
fill ratio of the purchasable cells that unlocks the next ring (0.75) |
center |
the Throne block, its intro price in cents, and the owning domain or null |
prices |
base price per square (0 — claims are free), pick fee (0), Throne intro price, analytics unlock — all cents |
listings[] |
every claimed 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 — 0 for a free claim; eviction costs max(100, 2 × sq_price_cents × w × h), i.e. $1 minimum), created_at. The site's title and description are not here — with thousands of squares they'd dominate the payload; fetch GET /api/square/:id for the one square you care about |
pending[] |
cells reserved by unpaid eviction / bulldoze / Throne checkouts (30-minute hold), positions only |
terrain[] |
hand-sculpted ground: [x, y, height] per raised cell (height in notches ≥ 1; flat cells aren't listed). The town's landscapers raise and lower these — raised cells are not purchasable, so a pick buy there is rejected |
events[] |
the last 20 feed events, newest first: type (buy, evict, kiss, click, clown, unlock, reset), text, ts. A click is a visitor following a square's /go/ link — it is announced live and the square pops on every open wall |
clowns[] |
domains currently in the Hall of Clowns |
fires[] |
ids of claimed squares whose homepages returned two confirmed HTTP 404s at least ten minutes apart; present in all three state formats. This is independent of favicon/clown status and is omitted from history |
dev |
true only on a dev deployment where evictions and the Throne are free too |
Owner URLs are not exposed — only domains. Coordinates are grid cells, origin top-left, Throne at 510–513.
GET /api/history — the wall at any past moment
Evicted squares are never deleted, so the whole record exists — this endpoint hands it over in one compact payload. It powers the wall's ⏳ rewind control (the topbar button, or T: scrub back to any day, or replay the whole town as a timelapse), and it's yours too:
{ "fmt": 1, "t0": 1787065924220, "now": 1788012345678, "iconsV": 1787896835775, "d0": 20683,
"rows": [ [12, 505, 514, 1787065924220],
[98, 511, 500, 1787066000000, 1787153000000, "evicted.example", 14] ],
"curves": { "98": [0, 9, 1, 5] } }
Each row is [id, x, y, created_at, evicted_at, domain, hgt, clown, w, h] with trailing defaults omitted (evicted_at 0 = still standing; domain 0 means the id is in the live /api/state, so read its domain there; hgt is clicks+kisses; w,h default to 1). The wall as it was at time t is every row with created_at <= t and (evicted_at = 0 or evicted_at > t) — if two rows pass on the same cell (a paid eviction has a short checkout window where both exist), the newer created_at wins. t0 is the first square's arrival; takedowns are excluded entirely (whacked squares are included — they really stood and really fell, so the rewind shows the clown era ending), and the counters are each row's own totals (an evicted square's froze at eviction).
curves is how tall a square stood when: for every square that was ever clicked or kissed, its day-by-day tally as flat [day, count, day, count, …] pairs, day counted from d0 (UTC days since the epoch — so day 0 is the day of the first square). Sum the counts up to a moment and that is the square's hgt then; the wall's rewind fills the running day in linearly, and spreads whatever the row's hgt holds beyond its curve (kisses from before the per-day tally existed) evenly over the row's lifespan, so a tower grows the way it grew instead of arriving at today's height. Cached for a minute at the edge.
GET /api/events — the live feed, page by page
GET /api/events?before=1788012345678&limit=20 returns feed events strictly older than before (ms epoch; default now), newest first — the same rows /api/state carries its latest 20 of, and the pages the wall's feed panel loads as you scroll. limit is clamped to 1–50, default 20. Town memory is five days: nothing older is served, and exhausted: true means there is no older page. Cached for a minute at the edge.
{ "events": [ { "type": "kiss", "text": "💋 someone kissed example.com's ring", "ts": 1788012000000 } ],
"exhausted": false }
GET /api/dr — every domain's rating
One [domain, rating] pair per rated domain that was ever on the wall (evicted ones included, so a rewound wall can be filtered too). It powers the wall's 🧭 DR filter (the topbar funnel, or R: drag and everything below the threshold leaves the map):
{ "fmt": 1, "license": "Domain Rating by Ahrefs — https://ahrefs.com/", "rows": [ ["example.com", 34.5], ["pov.town", 3.5] ] }
Ratings are Domain Rating by Ahrefs on their 100-point logarithmic scale, refreshed offline every few weeks — if you reuse them, their license asks for the same attribution. A domain that's absent simply has no rating on file (Ahrefs doesn't know it, or it hasn't been fetched yet) — absence is not a zero. Cached up to 6 h at the edge.
GET /api/categories — what every site is
One [domain, "slugs"] pair per classified domain that was ever on the wall (evicted ones included, so a rewound wall can be filtered too). Each domain carries 1–3 slugs from a closed eighteen-slug taxonomy, comma-separated with the primary first — the primary is the single best answer to "what is this site?":
{ "fmt": 1, "rows": [ ["example.com", "dev-tools,ai"], ["shop.example", "commerce"] ] }
The eighteen slugs: dev-tools, ai, saas, design, marketing, finance, commerce, media, social, games, personal, education, health, travel, services, utilities, community, other. other only ever appears on its own. They're assigned offline by Claude reading each landing page's own words (title, description, first visible text), refreshed every few months. A domain that's absent said nothing usable — unreachable, parked, or a bare splash screen — and gets no category rather than a guessed one; absence is not other. Cached up to 6 h at the edge.
Ids and slugs
Every listing has a numeric id — the key the wall's own calls use (/api/kiss/:id, /api/view/:id, the evictId in POST /api/buy) — and a slug: six random characters from a 31-letter alphabet (no 0, 1, i, l or o; never all digits) that says nothing about when the square arrived or how many there are. The slug is the square's public handle: it's what the share link (/s/:slug), the badge (/badge/:slug.svg), the deed and the Visit button use, and the town never prints a row id anywhere a visitor can see one. GET /api/square/:key, GET /go/:key, /s/:key and /badge/:key.svg all take a key in any of three forms: a bare number is the id (so links made before slugs keep working — /s/ answers those with a 301 to the slug form), a name containing a dot is a domain and resolves to that domain's standing square, anything else is a slug. The slug field rides along on GET /api/square, /api/search results, GET /api/deed, and the responses of POST /api/buy (free claims) and POST /api/claim. /api/state doesn't carry it — with thousands of squares it would be a third of the payload — so fetch one square when you want its link.
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/:key — click redirect
GET /go/k7m2xq (a slug; an id or a domain works too, see Ids and slugs) looks up the square 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). Everyone gets the redirect and the click counts — 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. One counted click per square per address per hour. 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 claimed square, free — no token needed (with one, the 5-per-hour cap is yours rather than your address's). Optional JSON body { "by": "<any client id>" } lets the kisser's own browser skip replaying its own animation; it's not identity. Response:
{ "ok": true, "watered_at": 1755470000000, "kisses": 3 }
Sets watered_at to now (the last-kissed timestamp), increments kisses (which also grows the tower a storey), 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"}. Limit: 5 kisses per square per hour, per account and per network (a /24 for IPv4, a /64 for IPv6 — one block is one kisser, however many addresses it walks); over that → 429 {"error":"kissed out","limit":5,"retry_in_min":n}. The nightly sweep then settles the day: a network keeps at most 10 kisses per square per day, and a network that kissed more than 40 times that day across the whole town keeps none of them. POST /api/water/:id is a legacy alias.
GET /api/square/:key — one square in full
GET /api/square/12 (or /api/square/k7m2xq by slug, or /api/square/example.com by domain) returns the full row for one claimed square: everything its /api/state entry has plus slug (its public handle, see Ids and slugs), title and description (the site's own <title> / og:title and meta description, fetched by the Worker after the claim and refreshed on recheck; null until fetched or if the site sends none) and dr, the domain's Domain Rating by Ahrefs (null if none on file — same number /api/dr serves in bulk), and categories, the domain's comma-separated category slugs, primary first (null if unclassified — same string /api/categories serves in bulk). This is where the card's text lives now that /api/state carries only what's needed to draw the wall. It also carries claimable: 1 when the square is free and has no owner email on file — a square Square Town seeded for a site rather than one someone claimed — so the card can offer the keys (see POST /api/claim/:id); the email itself is never in the row. Unknown or evicted id → 404.
POST /api/view/:id — count a view
POST /api/view/12 with no body increments the square's views, records a view for today and responds with the same full row as GET /api/square/12 (with the fresh view already counted). Needs a token (401 otherwise). The wall calls it once per square per browser session when a signed-in visitor opens the square's card — the same request fetches the card's details; signed-out visitors get the card from GET /api/square/:id and count nothing. That once-per-session logic lives in the client, not the server. Unknown id → 404.
GET /api/search?q= — search the wall
GET /api/search?q=example searches claimed squares by domain, title and description — every whitespace-separated term must appear in at least one of the three. Returns { "results": [...] }, at most 8, best match first (domain prefix beats domain substring beats title beats description; ties break on clicks), each with id, slug, domain, x, y, w, h, clown, clicks, title and description. Empty or missing q → { "results": [] }.
POST /api/recheck/:id — leave the Hall of Clowns
Re-runs the favicon check for a claimed 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. Needs a token — or the square's own deed token, as JSON body { "deed": "…" } or an x-deed-token header, so an owner can fix their square from the deed page without signing in.
POST /api/site-check/:id — check a burning homepage again
Checks the claimed square's domain homepage with GET, following up to three public HTTP(S) redirects. Anonymous and subject to the shared 30-writes-per-minute IP limit. All squares for the same domain share one health record and at most one check per minute; overlapping requests return the existing record with cached: true.
Two HTTP 404 responses at least ten minutes apart ignite a fire. A timeout, network error, bot challenge, 403, 429, server error or other inconclusive result cannot start a fire and resets a pending confirmation. Once lit, only a successful 2xx response extinguishes it. Favicon health, ownership, height and eviction prices are unaffected.
{ "fire": 0, "status": 200, "checked_at": 1789034400000, "cached": false }
status is the last HTTP status, or null for an inconclusive network/redirect/challenge result; checked_at is Unix milliseconds (0 before any completed check). An unknown or departed square returns 404. The same fields appear on GET /api/square/:id as fire, site_status and site_checked_at (the last two may be null before the first check).
A bounded background patrol runs every five minutes. Pending 404s become eligible after ten minutes, burning sites after thirty minutes, healthy sites after a day, and other results after six hours. These are minimum delays: the patrol checks up to twenty domains per run. The card's Check again button can request an earlier check, subject to the one-minute domain cooldown. Live fires follow map filters; rewind omits present-day health, and Zen hides the flames.
POST /api/whack/:id — whack a clown off the wall
Anyone — no deed, no payment, no token — can whack a clown square off the wall, but only if all of this holds: the square is a clown (clown = 1), it was a free claim (sq_price_cents = 0 — a paid clown bought immunity; only an eviction removes it), its 24-hour grace period since claiming has run out, and the town-wide mallet cooldown (10 seconds since the last landed whack) is over. Before the whack lands, Square Town re-runs the full favicon check, fresh — the mercy check. Three outcomes:
- The icon now resolves →
{ "escaped": true }. The whack fails and saves them: the clown is cleared, the icon stored, and the feed posts a 🎈 escape. Every whack attempt is a free favicon amnesty. - Still no favicon (Google's favicon service answers 404) →
{ "whacked": true, "next": <ms> }. The square's status becomeswhacked, the lot goes straight back on the market at $0 — with no reservation for the whacker — the feed posts a randomly chosen, emoji-free line such as "somebody whacked example.com off the wall.", andnextis when the town-wide cooldown ends. - The check can't be completed →
503and nothing happens. Unproven guilt walks.
Errors say why: 400 (not a clown), 403 (paid square, with error text), 404 (gone), 409 (still in grace, with grace_left_ms), 429 (cooldown, with next). The whacker gets nothing and is never named; the whacked domain may reclaim a square immediately — the door was never locked.
GET /api/deed/:key — the deed
The key is the secret from a deed URL — /deed/<slug>/<key>: the square's slug, then twelve random characters (31¹² ≈ 2⁵⁹ possibilities; lookups are rate-limited per IP). It's the only credential on Square Town. Deeds issued before September 2026 were mailed as /deed.html?token=<uuid>; that form and that token stay valid forever, and the first time such a deed is opened the row gets a key and the page moves its address bar to the new form. Returns the listing — id, slug, deed_key, deed_url (the deed's own address), url, domain, x, y, w, h, status (pending, claimed, evicted or whacked — claimed whether the square was free or paid for; the money is in price_cents), 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 — claim a square (free) or evict one (Stripe)
Request body (JSON):
| Field | Required | Meaning |
|---|---|---|
url |
yes | the site; https:// is added if missing and www. is dropped from the domain. It must be a door a visitor could open: an email address, a wildcard (*.acme.com), a port, an IP or a private hostname is refused, and the site has to answer when we knock — any HTTP status counts, including a 403 or 429 bot wall; only a domain nothing responds at is turned away |
email |
yes | an email address — not an account; the deed link is mailed there as soon as the square is yours, and an eviction warning goes there if notify is set |
notify |
no | false to opt out of the eviction mail; omitted or true means the notice goes to email (the wall always sends it) |
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 0 — a claim is free and completes immediately, no checkout; center is 10,000 cents (only while the Throne has no owner); evict is 2 × sq_price_cents × w × h of the target with a 100-cent floor — $1 for a free square, then $2, $4, $8 …; $200 for a $100 Throne, and doubling from there. The Throne, once owned, is evicted through mode: "evict" like any other square.
Response for a free pick claim — done on the spot:
{
"done": true,
"deed": "0c1f7d2e-…-uuid",
"id": 12,
"price": 0,
"block": { "x": 505, "y": 514, "w": 1, "h": 1 }
}
The square is yours immediately: a buy event is broadcast, the ring-unlock check runs, the favicon check decides whether the domain joins the Hall of Clowns, and the deed token is the sole proof of ownership (a mail with the deed link goes to the email you gave).
Response for evict and center — paid, via Stripe:
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/…",
"deed": "0c1f7d2e-…-uuid",
"price": 100,
"block": { "x": 505, "y": 514, "w": 1, "h": 1 }
}
The target is now pending: reserved for 30 minutes while someone completes checkoutUrl on Stripe. When Stripe's webhook confirms payment the listing becomes claimed, the evicted party becomes evicted (and is mailed, if they asked to be), the new owner is mailed their deed, an evict (or buy, for the Throne) event is broadcast, the ring-unlock check runs, and the favicon check runs. If nobody pays within 30 minutes the reservation lapses. The deed key is issued up front, so store it — GET /api/deed/<key> already answers while the listing is pending, and once payment lands it is the sole proof of ownership. The deed page is /deed/<slug>/<key>. Stripe returns the payer to /?deed=<key>.
On a dev deployment ("dev": true in /api/state) there is no Stripe: evictions and the Throne also answer { "done": true, … } and complete 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 evicting 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", "That's an email address, not a website. Claim acme.com itself.", "A wildcard isn't a website — claim a domain a visitor can actually open.", "A square is a whole domain — leave the port off.", "Nothing answered at acme.com. Check the spelling — and if the site is only down for the moment, come back and claim it once it's up.".
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.
POST /api/claim/:id — pick up the keys to a seeded square
Many squares are put in town by Square Town itself — a site's favicon on a free square with no owner email on file and a deed nobody holds. If it's your site, you don't have to evict your own favicon for $1 and pay to move back in: POST /api/claim/12 with { "email": "you@example.com", "notify": true } attaches the email to the square, hands you its deed and mails the deed link to that address. Free, no checkout, nobody is evicted, and the square's price doesn't change (evicting it still costs $1). There is no domain check — the email is the whole point (the eviction notice has somewhere to go), and only a square that is both free and email-less qualifies: a square somebody claimed or paid for is never up for grabs.
{ "done": true, "deed": "0c1f7d2e-…-uuid", "id": 12, "slug": "k7m2xq", "block": { "x": 505, "y": 514, "w": 1, "h": 1 } }
A buy event is broadcast ("🔑 example.com picked up the keys to its square"). Errors: 400 for a bad email, 404 for an unknown or evicted id, 409 {"error":"… already has an owner on file …"} when the square isn't claimable (or someone claimed it a moment earlier). GET /api/square/:id says claimable: 1 beforehand.
POST /api/bulldoze — evict without taking the square
The pure-demolition variant of an eviction: the target is evicted, nobody moves in, and the empty lot goes straight back on the market at $0 — where anyone, including the freshly evicted, can claim it for free. It costs exactly what an eviction costs (2× what the owner paid, $1 minimum) and asks for nothing else: no URL, no email, no name. Bulldozing is anonymous by design — the feed says "🚜 Somebody paid $4 to bulldoze example.com", and "somebody" is all anyone will ever know.
Request body (JSON):
| Field | Required | Meaning |
|---|---|---|
evictId |
yes | the id of the listing to bulldoze, from /api/state |
The response is always a Stripe Checkout URL (a bulldoze is never free — the floor is $1):
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/…",
"price": 400,
"block": { "x": 505, "y": 514, "w": 1, "h": 1 }
}
The target's block is reserved for 30 minutes while checkout completes, exactly like an eviction ("Someone is already evicting this square. Vultures everywhere." guards both directions). When Stripe confirms, the target becomes evicted, an evict event is broadcast ("🚜 Somebody paid $… to bulldoze …. Nobody moved in — the lot is free again."), the evicted owner is mailed if they asked to be, and the cell is claimable for free. Stripe returns the payer to /?bulldozed=<domain>. On a dev deployment it answers { "done": true, … } and demolishes immediately.
Since the lot reverts to a free claim, the doubling ladder resets: the square that cost $4 to bulldoze costs $1 to evict once someone new claims it. You're paying for the eviction, not the land.
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 claim (or Throne 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 |
fire, fire-out |
domain, text |
a homepage has confirmed repeated 404s, or a successful response extinguished its fire; refresh state to update every square for that domain |
whack |
id, next, text |
a visitor whacked a clown square off the wall (next is when the town-wide cooldown ends) |
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, pricing, the 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. Claiming a free square is one more — no card. Evicting needs a human, because the last step is a Stripe Checkout page with a card form. A sensible flow:
- Read the state.
GET /api/state. Compute the live box fromhalf(512-half … 511+half), collect occupied cells fromlistings(pluspending) and reserve the Throne block (510–513), its one-cell aura ring (509–514) and everyterraincell (raised ground) as off limits. - Choose a free cell — or a target to evict. For an eviction, the price is
2 × sq_price_cents × w × hof the target listing, $1 minimum. POST /api/buywith{ "url": "yoursite.com", "email": "you@example.com", "mode": "pick", "x": 505, "y": 514 }(ormode: "evict", evictId, ormode: "center"). Apickclaim returnsdone: trueand you're finished. Keep the returneddeedtoken somewhere safe; it's the only proof of ownership. For evictions it's issued before payment (the deed showsstatus: "pending"until then).- Evicting? Hand
checkoutUrlto a person. They open it and pay the eviction price ($1 minimum) with a card within 30 minutes. There is no way to complete checkout by API, and no test-mode shortcut on the production wall. - Confirm. Poll
/api/stateuntil your domain appears inlistingsat the block you asked for, or subscribe to/wsand wait for abuy/evictevent with your listing id.GET /api/deed/<token>then returns the deed. - Afterwards.
POST /api/kiss/:idany square you like (kissing is free, public and counted, and every kiss grows the tower a storey); a token fromPOST /api/registeris optional and gives you your own kiss budget. Readclicks,viewsandkissesfrom/api/stateor the deed. Ifclownis 1, add a favicon andPOST /api/recheck/:id— and don't sit on it: a free-claim clown is whackable by anyone (POST /api/whack/:id) once it's 24 hours old.
There is no rate limit promise beyond the per-IP limits above, and nothing is guaranteed to stay as it is. Kisses and clicks are settled per IP every night, so a loop buys a tower nothing but a correction. If you're building something for agents specifically, see Square Town 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.
Do I need a token to kiss, whack or count clicks?
No. Kiss, view, recheck, whack and /go/:id clicks all work anonymously, held to per-IP limits. A token (POST /api/register, free and instant, no email needed) is optional: it gives an API caller its own kiss budget and credits the acts to an account. Attaching an email is optional too, and verifying it gates nothing today.
Can I claim a square entirely by API?
Yes. POST /api/buy with mode: "pick", a URL and an email claims the cell instantly — it's free. If your site is already in town on a seeded square (claimable: 1 in GET /api/square/:id), POST /api/claim/:id with an email takes it over for free instead. Evictions and the Throne are different: the call returns a Stripe Checkout URL that a human must complete with a card within 30 minutes. There is no server-to-server payment.
How do I know the eviction price of a square?
From /api/state: 2 × sq_price_cents × w × h for that listing, with a 100-cent floor. For a free square (sq_price_cents: 0) that's 100 cents; once evicted once it's 200, then 400; the Throne starts at $100 so its 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.