kink.ong
A safe-for-work directory of adult-oriented AI tools — what each one does, what it costs, and what it does with what you type into it.
kink.ong is a plain-spoken, PG-13 reference for a corner of the AI market that is usually written about in either breathless ad copy or nothing at all: tools built for adults. Every entry says what the tool does, what it costs, what hardware it needs, what it does badly, and — the reason the site exists — what happens to the things a person types into it. Nothing explicit is hosted, and where the answer to a privacy question has not been found in writing, the page says "unknown" rather than guessing. Under it: one Go binary with the templates, fonts, icons, legal documents and catalogue seed all embedded, SQLite with FTS5 for search, and passkeys as the only way in — there is no password field anywhere in the codebase, and no password hash in the database to steal. A scheduler pulls curated feeds into a private queue every two hours; nothing reaches the public side of the site without two separate decisions by a person.
- Go
- SQLite (FTS5)
- WebAuthn / passkeys
- goldmark
- gofeed
- Caddy
- systemd
- Postmark
kink.ong is a directory for grown-ups written at a PG-13 volume. The subject is adult-oriented AI tooling; the treatment is the same one a careful buyer's guide would give to anything else — what a tool is for, what it charges, what it needs to run, where it falls down, and what it keeps. Nothing explicit is hosted on the site, and the editorial line is that a plain answer is more useful to an adult than a coy one.
Three rules the catalogue keeps
- Unknown is an answer. Where no clear statement from the vendor could be found, the stance is recorded as unknown and the page says so. A guess dressed as a fact is the exact failure this directory exists to avoid, and a test enforces it.
- A missing figure is not a "no". The hardware filter scores each tool fits, tight, no or not known, and keeps the last of those in the results, labelled — while still failing anything that genuinely needs a graphics card the reader doesn't have.
- Every tool lists what it does badly, with a source where one exists. An entry with an empty limitations list fails the seed test and never ships.
Nothing publishes itself
Curated feeds are pulled into a private queue every two hours. Getting out of that queue takes two separate human decisions — write it up, then publish — and the background jobs can do neither: every writing function takes the id of the person who asked, and a scheduled job has nobody to name. A test scans the jobs package to keep it that way. Where a model drafts a summary, the post says so, from a column set at the moment the draft was made, and every model call is written to a ledger with what it cost so a hard spend cap can be enforced by summing the table rather than trusting a counter in memory.
Passkeys only
Sign-in is WebAuthn and nothing else: invites, step-up confirmation for destructive actions, recovery, roles and an audit trail, with no password field in the codebase and no password hash in the database. API keys live in an encrypted vault whose master key is written once, on the box, from a shell a person owns — never in the repository, the environment, or a log. Taking a page down or deleting a record asks for a passkey again.
One binary, its own box
The whole site — templates, CSS, fonts, icons, favicons, the legal documents it publishes about itself and the catalogue seed files — is embedded in a single Go binary, served behind Caddy under a nonce-based content-security policy, with rate limits and one outbound HTTP client that guards the addresses it is allowed to reach. It runs on a small dedicated server of its own, and the home page prints that machine's live processor, memory and uptime figures rather than claiming anything it can't show.
Slugs are permanent
Renaming a record keeps the old slug and answers a 301 for ever. A directory that is worth linking to has to be safe to link to; the first broken permalink is the moment people stop bothering.
Straight from the source
The project's own README.
Rendered in place — every link, image, and code block carried over from the repo. The page below is what a contributor would see opening the project for the first time.
kink.ong
A safe-for-work directory of adult-oriented AI tools. One Go binary with everything embedded, SQLite for storage, Caddy in front. Passkeys only — there is no password anywhere in this codebase.
Layout
cmd/kinkong: the binary.serve(default),migrate,admin invite,vault init|status|rekey,seed,reindex,version;routes.gois the one route table.internal/:config,db(+ numbered migrations),auth(passkeys, sessions, roles, invites, step-up, recovery, audit),vault(sealed secrets),mailer(Postmark, with a send governor),content(the changelog, roadmap and legal markdown),catalog(the tool directory: tools, categories, models, hardware profiles, the FTS5 search index and the permanent-slug rules),jobs(the freshness engine: the scheduler, eight jobs and the admin notices they raise),fetch(the one outbound HTTP client, with the address guard),news(the feeds, the inbox they fill, the posts a person writes out of it and the AI spend ledger),middleware(nonce CSP, real IP, rate limits, request log),admin,web,view,stats,version.templates/{public,admin,email},static/{css,js,fonts,icons},root/(favicons, robots.txt, og.png …) — all embedded byembed.go, along with the documents the site publishes about itself (docs/CHANGELOG.md,docs/ROADMAP.md,docs/legal/,data/changelog.json) and the catalog seed (data/seed/*.json, and the news sources indata/seed/news/feeds.json).deploy/: systemd unit (sandboxed, statickinkonguser), Caddyfile, deploy script.scripts/: the gates — headers, Lighthouse, repo scrub, and the end-to-end passkey test.docs/:PLAN.md(the build plan),ROADMAP.md,CHANGELOG.md,handoff-*.md.
Run locally
just dev # http://localhost:8080, database in ./data
Then mint an invite for yourself and open the printed link:
go run ./cmd/kinkong admin invite --email [email protected] --role superadmin \
--data-dir ./data --site-url http://localhost:8080
The catalog
Day to day the directory is edited in the admin panel — /admin/tools, and the same shape for
/admin/categories, /admin/models and /admin/hardware. A new tool saves as a draft and is
invisible until someone presses Publish: absent from the listings, from search, from the sitemap and
from the far end of another tool's relation. Taking a page down or deleting one asks for a passkey.
A tool's lists — modalities, limitations, links, relations — are textareas, one item per line,
value | note | source, which is the same shape as the seed files below, so an entry can be pasted
either way.
From cold, the directory is seeded from JSON files that ship inside the binary:
kinkong seed --dry-run # parse and check the files, write nothing
kinkong seed # create or update; safe to re-run, it matches on slug
kinkong reindex # rebuild the search index from the tables
Records match on slug, so editing data/seed/*.json and running seed again updates what is there
rather than duplicating it, and a tool that is already published keeps the moment it went live. Nothing
is ever deleted by a seed run. On the live box: sudo -u kinkong kinkong seed.
Three rules the catalog keeps, each with a test behind it:
- Unknown is an answer. Where we have not found a clear statement, the stance is
unknownand the page says so. A guess dressed as a fact is the failure this directory exists to avoid. - A missing figure is not a "no". The hardware filter scores fits / tight / no / not known, and keeps the last of those in the list, labelled. A machine with no graphics card is a real constraint, though, so a tool that needs one fails it.
- Every tool lists what it does badly, with a source where there is one. An entry with an empty limitations list fails the seed test.
Slugs are permanent: renaming records the old one, and the old URL answers a 301 for ever.
News
Curated feeds are pulled every two hours into a queue at /admin/news. Nothing in that queue is public,
and nothing leaves it without two separate decisions by a person: Write it up, which copies the item
into a draft post, and Publish, which is the only thing anywhere in the pipeline that makes something
public. The jobs package cannot do either — every writing function in internal/news takes the id of the
person who asked, and a job has nobody to name. A test scans internal/jobs to keep it that way.
Sources are managed at /admin/feeds and ship in data/seed/news/feeds.json; every URL there was
fetched and confirmed to be a feed before it was added. A source marked Summarise has its items
drafted by a model — a title, two or three sentences, one line on why it matters, some tags — written
into the item's own columns, next to the source's own words rather than over them. An item with less
than 200 characters of text is never sent: there is nothing to summarise and the answer would be
invention. A post whose first draft came from a model says so on its page and in the feed, from a column
set when the draft was made.
Every model call is written to ai_ledger with what it cost, successful or not, and the $9 cap it
shares with the art generator is enforced by summing that table before each call. There is no counter to
drift and nothing in memory for a restart to forget.
Secrets
API keys live in the encrypted vault, never in the repo, the environment or a log. Once, on the box — from a shell you own, because it prints a secret and an assistant or a recorded session would keep it:
sudo kinkong vault init # writes the master key, prints it once, points systemd at it
Then paste keys into /admin/vault and press Test on each. sudo -u kinkong kinkong vault status lists
what is set from a terminal; it needs no key and must not be run as root.
Deploy
just ship # check, back up, deploy, verify headers, push
sudo ./deploy/deploy.sh
Gates
just gates # vet, tests, admin-guard, scrub, end-to-end passkeys, headers, Lighthouse
Endpoints
/the home page;/tools,/tools/{slug},/categories,/categories/{slug},/models,/models/{slug},/search?q=;/sitemap.xml(generated from the database)/news,/news/{slug},/news.atom/updatesand/updates.atom;/roadmap;/terms,/privacy,/content-policy,/dmca,/disclosure,/contact/login,/invite,/recover,/account/passkeys,/admin/…/api/statslive server stats,/api/versionthe build stamp,/healthzliveness/api/auth/{login,invite,stepup,recover,passkeys}/{begin,finish}the passkey ceremonies
Regenerate icons
rsvg-convert -w 512 -h 512 root/favicon.svg -o root/icon-512.png # etc.
Build something like this
Want a tool like this for your shop?
We've shipped this kind of thing before. Twenty-minute intro call, no slides.