Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pitworld",
"version": "0.11.0",
"version": "0.11.1",
"mcpName": "world.pit/pit",
"description": "pit.world MCP server: private web search and Moshpit name lookups for AI agents (stdio, no API key). Also a web app, API, CLI and TUI.",
"license": "MIT",
Expand Down Expand Up @@ -34,6 +34,7 @@
"@profullstack/affiliates": "0.2.0",
"@profullstack/bridges": "0.2.0",
"@profullstack/footer": "0.4.0",
"@profullstack/form-guard": "0.3.0",
"@profullstack/hqtui": "^0.11.0",
"@profullstack/openprofile": "0.2.0",
"@profullstack/redeem-credits": "0.1.0",
Expand Down
30 changes: 19 additions & 11 deletions src/build-invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* for $2 that CoinPay repeats monthly from day 31. We keep nothing; the order
* goes to the builder by email and the buyer goes to their $49 invoice.
*/
import { scoreSubmission, substance } from '@profullstack/form-guard'
import type { Mailer } from './newsletter.js'

export const BUILD_USD = 49
Expand Down Expand Up @@ -40,20 +41,27 @@ export function readOrder(form: Record<string, unknown>): BuildOrder {
return { email, name: str('name', 80), idea }
}

/** Selling to us, not buying: no idea that mentions these is an app. */
const SELLING = /\b(guest post|backlinks?|seo services|rank (your|on) (website|google))\b/i
/** Stock enquiry sentences form-guard does not know (it knows the pricing/availability/process one). */
const CANNED = [/interested in (scheduling|booking) a (brief|quick|short) (consultation|call)/i]

/**
* Contact-form spam: the stock "questions about pricing" notes that are sent to
* every form on the web. They describe no app, so they get no invoice.
* Contact-form spam: the stock "questions about pricing, availability, and the
* general process of working with you" note sent to every form on the web.
* It is spam only when nothing is left once its stock sentences and
* pleasantries are taken out (form-guard's substance()), so a real buyer who
* opens with the same line and then describes an app still gets an invoice.
*/
const SPAM_IDEAS = [
/questions? about (your )?(pricing|prices|availability)/i,
/general process of working with you/i,
/let me know what (details|information) you need/i,
/interested in (scheduling|booking) a (brief|quick|short) (consultation|call)/i,
/\b(guest post|backlinks?|seo services|rank (your|on) (website|google))\b/i,
]

export function looksLikeSpam(order: BuildOrder): boolean {
return SPAM_IDEAS.some((re) => re.test(order.idea))
if (SELLING.test(order.idea)) return true
const canned = scoreSubmission({ message: order.idea }).signals.includes('contentless_request') || CANNED.some((re) => re.test(order.idea))
if (!canned) return false
const rest = substance(order.idea)
.split(/(?<=[.!?])\s+/)
.filter((s) => !CANNED.some((re) => re.test(s)))
.join(' ')
return rest.length < 20
}

export async function orderByInvoice(cfg: BuildInvoicing, order: BuildOrder, now = new Date()): Promise<{ build: Invoice; hosting: Invoice }> {
Expand Down
29 changes: 29 additions & 0 deletions test/build-spam.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from 'bun:test'
import { looksLikeSpam } from '../src/build-invoice.js'

const order = (idea: string) => ({ email: 'pat@example.com', name: 'Pat', idea })
const template =
'Hello, I have a few questions about pricing, availability, and the general process of working with you. Please let me know what details you need from me to proceed. Thanks!'

describe('looksLikeSpam (build by invoice)', () => {
test('the stock pricing/availability/process note is spam (INV-043/044, 2026-10-28)', () => {
expect(looksLikeSpam(order(template))).toBe(true)
})

test('the stock consultation note is spam', () => {
expect(looksLikeSpam(order("Hello, I'm interested in scheduling a brief consultation. Thanks!"))).toBe(true)
})

test('selling SEO is spam', () => {
expect(looksLikeSpam(order('We sell backlinks and guest post placements'))).toBe(true)
})

test('a real buyer who opens with the stock line still gets an invoice', () => {
expect(looksLikeSpam(order(`${template}\nI want a booking page for my barbershop with SMS reminders.`))).toBe(false)
})

test('a short real idea is not spam', () => {
expect(looksLikeSpam(order('A todo app for my team'))).toBe(false)
expect(looksLikeSpam(order('A booking page for my barbershop\nwith reminders'))).toBe(false)
})
})
3 changes: 3 additions & 0 deletions test/pit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,9 @@ describe('/build', () => {
expect(spam.headers.get('location')).toBe('/build')
expect(sent).toHaveLength(0)
expect(mails).toHaveLength(0)
const seo = await post({ idea: 'We offer guest post and backlinks packages for your site', email: 'a@b.example', name: 'S' }, '203.0.113.11')
expect(seo.headers.get('location')).toBe('/build')
expect(sent).toHaveLength(0)

const ok = await post({ idea: 'A booking page for my barbershop\nwith reminders', email: 'Pat@Example.com', name: 'Pat' })
expect(ok.status).toBe(302)
Expand Down
Loading