Offers and leases on a parked name (onto main) - #444
Merged
Conversation
Every parked page ended the conversation. A name somebody held said "claimed but does not point anywhere yet"; a name under an ending with no price said ".eggs is not for sale". Both are true, both land at the moment a visitor wants the name most, and both left the holder never hearing that anyone asked. An offer is the missing half. A visitor says what a name is worth to them and the holder accepts, refuses, or names a different number. No account needed to ask. Requiring one means asking a stranger to sign up before they may say what they would pay, on the page whose whole job is converting that stranger. The address is confirmed by mail instead, and that step is load-bearing: an offer is recorded `unverified` and the holder is told nothing until the offerer clicks the link — without it the form is a way to write to every holder in the registry from our own domain. Twenty offers per address per day, one standing offer per address per name, behind that. Private, not an auction. Only the holder sees an amount; a public board tells every later bidder what the last one offered and shows the holder's next buyer their floor. A counter is held beside the original rather than replacing it — agreedTerms() is the one place that decides which number is operative. Accepting moves nothing. It opens a CoinPay checkout beside the existing name and ending purchases, claimed with a conditional accepted→settling update because a webhook gets redelivered. Everything is re-checked at settlement: a name that changed hands in between becomes refund_due rather than a silent loss, and a sale closes every other live offer on that name. A sale leaves nothing of the seller's behind — contact and its forwarding alias, pins, records, twin and target all go, for the reason releaseName gives. Leases are real. The holder keeps the name; the tenant points it, publishes under it and presents keys until the term ends, then it reverts on its own. Paid once upfront for the whole term — a monthly rate needs subscription billing, a grace period and a story for a failed payment against a live site, and a term paid in full before it starts cannot lapse halfway through. ownedName stays the holder check and controlledName also accepts a tenant, so what outlives the lease (release, sale, twin, contact) stays with the holder. A lease ends by the clock, not by a sweep: leased_until is read-time, so a former tenant loses control the moment it passes and resolveMoshpitName stops serving their target. endExpiredLeases() does the part a reader cannot — taking the target, records and keys back off — hourly and at boot. Their published content is left alone deliberately; a lapsed lease should unlink work, not destroy it. leased_to/leased_until are denormalised onto moshpit_names because every lookup in the pit goes through resolveMoshpitName, and a second SELECT there to answer a question that is null for almost every name is not worth it. Endings can be bought but not leased: a name minted during an ending's lease would outlive it, so a six-month tenancy would permanently carve up a namespace its holder never sold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QSp1aAoQQB7jFV7hwt4jJ
| const OFFERS_PER_NAME = 1; | ||
|
|
||
| export async function getOffer(id) { | ||
| return get(`SELECT ${OFFER_COLS} FROM moshpit_offers WHERE id = ?`, [String(id ?? "")]); |
| export async function offerByVerifyToken(token) { | ||
| const raw = String(token ?? ""); | ||
| if (!raw) return null; | ||
| return get(`SELECT ${OFFER_COLS} FROM moshpit_offers WHERE verify_token = ?`, [raw]); |
| } | ||
|
|
||
| export async function getLease(tld, label) { | ||
| return get(`SELECT ${LEASE_COLS} FROM moshpit_leases WHERE tld = ? AND label = ?`, [tld, label]); |
| } | ||
|
|
||
| export async function listLeasesForUser(userId) { | ||
| return all(`SELECT ${LEASE_COLS} FROM moshpit_leases WHERE lessee_user_id = ? ORDER BY expires_at DESC`, [userId]); |
| } | ||
|
|
||
| const standing = await all( | ||
| `SELECT ${OFFER_COLS} FROM moshpit_offers |
|
|
||
| export async function listOffersForHolder(userId, { limit = 200 } = {}) { | ||
| return all( | ||
| `SELECT ${OFFER_COLS} FROM moshpit_offers |
|
|
||
| export async function listOffersForEmail(email, { limit = 200 } = {}) { | ||
| return all( | ||
| `SELECT ${OFFER_COLS} FROM moshpit_offers WHERE offerer_email = ? ORDER BY created_at DESC LIMIT ?`, |
| /** Live offers on one name, for the holder's own page. Never shown to a visitor. */ | ||
| export async function listOffersForName(tld, label = "", { now = Date.now() } = {}) { | ||
| const rows = await all( | ||
| `SELECT ${OFFER_COLS} FROM moshpit_offers WHERE tld = ? AND label = ? ORDER BY created_at DESC`, [tld, label]); |
| */ | ||
| export async function settleOfferPurchase(paymentId, now = Date.now()) { | ||
| const offer = await get( | ||
| `SELECT ${OFFER_COLS} FROM moshpit_offers WHERE payment_id = ? AND status = 'accepted'`, [paymentId]); |
| */ | ||
| export async function endExpiredLeases(now = Date.now(), limit = 200) { | ||
| const due = await all( | ||
| `SELECT ${LEASE_COLS} FROM moshpit_leases WHERE reverted_at IS NULL AND expires_at <= ? LIMIT ?`, |
ThreatCrush Security Scan40 finding(s) in the 9 file(s) this pull request changes. MEDIUM: 40
41 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 5 | **MEDIUM**: 27 | **LOW**: 9Not introduced by this pull request. The full set is in the Security tab.
…and 21 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Same work as #443, retargeted at
main.#443 was merged into its stacked base
moshpit-contact-guardrather than intomain, so the offers work never reached main — only the contact PR (#442) did. This is that commit, rebased onto current main.Why a rebase rather than merging the contact branch.
moshpit-contact-guardwas cut before #441 (the session arrow pad) landed, so a diff of that branch against main shows #441's files as deletions. Rebasing instead replays only the offers commit: git dropped the contact commit as already applied via the #442 squash, andgit diff main..HEADis now exactly the nine offers files, with #441's017_session_features.sql,sessions-keys.test.mjsandmirror-keys.test.mjsuntouched.Full suite on the rebased branch: 2793 passing, 0 failing (4 skipped). The count is higher than #443 reported because #441's tests are now in scope.
The parked page now takes an offer to buy or lease instead of ending the conversation — replacing both dead ends ("claimed but does not point anywhere yet", and ".eggs is not for sale" on an unpriced ending).
unverifiedand the holder is told nothing until the link is clicked. Without that the form is a way to mail every holder in the registry from our own domain. 20/address/day, one standing offer per address per name.accepted → settlingupdate against webhook redelivery. A name that changed hands in between becomesrefund_due. A completed sale closes every other live offer and strips the seller's contact, alias, pins, records, twin and target.leased_untilis read at resolution time, so a former tenant loses control the moment it passes; the hourly sweep does the part a reader can't.Full reasoning in
docs/offers-and-leases.md.🤖 Generated with Claude Code
https://claude.ai/code/session_017QSp1aAoQQB7jFV7hwt4jJ