From 1faa872db0edb2ac925d4192f170ef16cac652fc Mon Sep 17 00:00:00 2001
From: Anthony Ettinger
Date: Sat, 17 Oct 2026 12:26:29 +0000
Subject: [PATCH] feat(pit): promote build + managed hosting on /pit
A build-and-host card under the banner on every /pit render: describe the
app, chovy.com builds it on the Profullstack stack, $49 then $2/month via
CoinPay. The form goes straight to chovy.com/start (plan=starter,
ref=moshpit); after a buy (?ok= is yours.) it names the name and seeds
the idea with it. Also linked from the hosting help and the already-yours card.
Co-Authored-By: Claude Opus 5.5
---
apps/pwa/src/lib/moshpit-build-promo.mjs | 65 ++++++++++++++++++++++
apps/pwa/src/routes/moshpit.mjs | 9 ++-
apps/pwa/test/moshpit-build-promo.test.mjs | 31 +++++++++++
3 files changed, 103 insertions(+), 2 deletions(-)
create mode 100644 apps/pwa/src/lib/moshpit-build-promo.mjs
create mode 100644 apps/pwa/test/moshpit-build-promo.test.mjs
diff --git a/apps/pwa/src/lib/moshpit-build-promo.mjs b/apps/pwa/src/lib/moshpit-build-promo.mjs
new file mode 100644
index 00000000..9164c2a7
--- /dev/null
+++ b/apps/pwa/src/lib/moshpit-build-promo.mjs
@@ -0,0 +1,65 @@
+// The build-and-host offer, drawn on /pit.
+//
+// Somebody who has just bought a name has a name and nothing at it. The
+// hosting help explains how to point it at a server of their own; this is the
+// other answer, for everyone without one: describe the app, chovy.com builds it
+// on the Profullstack stack and keeps it online. The storefront is
+// pit.world/build; the form here posts straight to the same intake it does, so
+// one click from the pit skips a page.
+import { esc } from "./html.mjs";
+
+export const BUILD_URL = "https://pit.world/build";
+export const BUILD_START_URL = "https://chovy.com/start";
+export const BUILD_PRICE_USD = 49;
+export const HOSTING_PRICE_USD = 2;
+
+/**
+ * The name a `?ok=` banner just handed over, when it handed over exactly one.
+ *
+ * The buy and claim flows redirect with `ok= is yours.`; that sentence is
+ * the only record on the page of what was bought, so the offer reads it rather
+ * than guessing at the newest row.
+ */
+export function boughtName(ok) {
+ const m = /^([a-z0-9-]+\.[a-z0-9-]+) is yours\.?$/i.exec(String(ok ?? "").trim());
+ return m ? m[1].toLowerCase() : null;
+}
+
+/**
+ * The card. `name` is the name just bought, when there is one: the offer then
+ * talks about that name and seeds the idea with it.
+ */
+export function buildPromo({ name = null } = {}) {
+ const forName = name ? ` for ${esc(name)}` : "";
+ const seed = name ? `A site at ${name}: ` : "";
+ return `
+ no server? no problem
+ We build it${forName}. We host it.
+ Describe the app in plain words. An AI product team writes a plan you can argue
+ with, builds it on the open source Profullstack
+ stack, and shows you it working before anything launches.
+ $${BUILD_PRICE_USD} to build, including the first 30 days, then $${HOSTING_PRICE_USD} a month
+ to keep it online. Paid in crypto through CoinPay.
+
+`;
+}
+
+export const BUILD_PROMO_CSS = `
+.pit-build{border:1px solid var(--acid);border-radius:var(--r);background:color-mix(in srgb,var(--acid) 6%,var(--surface));
+ padding:18px 20px;margin:18px 0;max-width:72ch}
+.pit-build h2{font-size:clamp(1.2rem,3.4vw,1.6rem);margin:4px 0 8px}
+.pit-build b{color:var(--acid);font-weight:600}
+.pit-build-form textarea{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text);
+ border:1px solid var(--line-2);border-radius:8px;padding:10px 12px;font:inherit;font-size:.9rem;margin-top:10px;resize:vertical}
+.pit-build-form textarea:focus{border-color:var(--acid);outline:0}
+`;
diff --git a/apps/pwa/src/routes/moshpit.mjs b/apps/pwa/src/routes/moshpit.mjs
index d1c3796d..9e1bf196 100644
--- a/apps/pwa/src/routes/moshpit.mjs
+++ b/apps/pwa/src/routes/moshpit.mjs
@@ -42,6 +42,7 @@ import { bearer, userForApiKey } from "../lib/apikey.mjs";
import { balance } from "../lib/credits.mjs";
import { resolverConfig } from "../lib/moshpit-resolvers.mjs";
import { landingFor } from "../lib/moshpit-landing.mjs";
+import { buildPromo, boughtName, BUILD_PROMO_CSS, BUILD_URL, BUILD_PRICE_USD, HOSTING_PRICE_USD } from "../lib/moshpit-build-promo.mjs";
import { FEED_KINDS, loadFeed } from "../lib/feed.mjs";
import {
sendOfferAnswer,
@@ -2707,7 +2708,8 @@ const landingCard = (req, landing) => {
return `
you asked for ${esc(landing.name)}
${name} is already yours.
-
It is in your list below — change where it points, or release it.
+
It is in your list below — change where it points, or release it.
+ Nothing at it yet? We build and host it — $${BUILD_PRICE_USD}, then $${HOSTING_PRICE_USD} a month.
`;
}
@@ -3123,6 +3125,8 @@ function hostingHelp() {
moshcode template install bun-caddy-sqlite writes the Caddyfile, the systemd
units and a Bun + SQLite service already wired up this way.
moshcode template list shows what there is.
+ No server, or no time? We build the
+ app and host it for you — $${BUILD_PRICE_USD} to build, then $${HOSTING_PRICE_USD} a month.
Check it with moshcode dns resolve seo.rank,
then curl -6 http://seo.rank/. A port other than 80 only works in the
/n/ view — DNS carries an address and has nowhere to put a port, so the browser
@@ -3390,7 +3394,7 @@ moshpitRouter.get("/pit", async (req, res) => {
res.type("html").send(page({
title: "moshcode ▸ the pit",
- head: ``,
+ head: ``,
body: `${appBar(req.user, bal, req.csrfToken)}
the moshpit namespace
@@ -3410,6 +3414,7 @@ moshpitRouter.get("/pit", async (req, res) => {
${landingCard(req, landing)}
${msg}
+ ${buildPromo({ name: boughtName(req.query.ok) })}
${req.user ? claimForm(req) + bulkClaimForm(req) : ""}
${pitTabs(tab, { yours: mineTotal, theirs: theirsTotal, forSale: filter ? 0 : forSaleCount }, filter?.query ?? "")}
${filterBox(tab, filter?.query ?? "", req.user ? (tab === "theirs" ? "theirs" : "mine") : "all")}
diff --git a/apps/pwa/test/moshpit-build-promo.test.mjs b/apps/pwa/test/moshpit-build-promo.test.mjs
new file mode 100644
index 00000000..a1d5e33d
--- /dev/null
+++ b/apps/pwa/test/moshpit-build-promo.test.mjs
@@ -0,0 +1,31 @@
+// The build-and-host offer on /pit: it must reach the paid intake, and it must
+// never echo a banner it was handed into the page unescaped.
+import assert from "node:assert/strict";
+import test from "node:test";
+import { boughtName, buildPromo, BUILD_START_URL } from "../src/lib/moshpit-build-promo.mjs";
+
+test("boughtName reads the name a buy or claim just handed over", () => {
+ assert.equal(boughtName("pay.agent is yours."), "pay.agent");
+ assert.equal(boughtName("Pay.Agent is yours"), "pay.agent");
+ // An ending, a count, or anything else is not one name.
+ assert.equal(boughtName(".agent is yours."), null);
+ assert.equal(boughtName("3 names claimed"), null);
+ assert.equal(boughtName(undefined), null);
+});
+
+test("the form posts to the chovy Starter intake, tagged as moshpit", () => {
+ const html = buildPromo();
+ assert.match(html, new RegExp(`action="${BUILD_START_URL}" method="get"`));
+ assert.match(html, /name="plan" value="starter"/);
+ assert.match(html, /name="ref" value="moshpit"/);
+ assert.match(html, /