Skip to content
Open
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
95 changes: 95 additions & 0 deletions workers/agentpay-landing/deploy-routing-fix.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail

# Deploys the routing fix: known routes unchanged, /awesome-free-dev-tools/buy
# redirects to Stripe again, unknown paths 404 instead of serving the landing
# page with HTTP 200. Records the current 100%-traffic version first and rolls
# back automatically if public verification fails.

EXPECTED_CONFIRMATION='agentpay.so/routing-404'
if [[ "${CONFIRM_DEPLOY_ROUTING:-}" != "$EXPECTED_CONFIRMATION" ]]; then
echo "Refusing production deploy. Set CONFIRM_DEPLOY_ROUTING=$EXPECTED_CONFIRMATION after action-time approval." >&2
exit 2
fi

if [[ -z "${CLOUDFLARE_API_TOKEN:-}" ]]; then
echo 'CLOUDFLARE_API_TOKEN is required.' >&2
exit 2
fi

# wrangler 4.x requires Node >= 22; this repo's default runtime is 20.18.0.
NODE_MAJOR=$(node -p 'process.versions.node.split(".")[0]')
if (( NODE_MAJOR < 22 )); then
echo "Node >=22 required for wrangler (found $(node --version)). Run: nvm use 22" >&2
exit 2
fi

ROOT=$(cd "$(dirname "$0")" && pwd)
RECEIPT_DIR=${RECEIPT_DIR:-/Users/brain/Documents/memorybrain/Shared-Brain/Agent-Claude/deploy-receipts}
WRANGLER_VERSION=${WRANGLER_VERSION:-4.103.0}
WORKER_NAME=${WORKER_NAME:-agentpay-landing-production}
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p "$RECEIPT_DIR"

cd "$ROOT"
node --test worker.test.mjs
node --check worker.js
npx --yes "wrangler@$WRANGLER_VERSION" deploy --strict --dry-run --outdir "/tmp/agentpay-landing-$STAMP"
npx --yes "wrangler@$WRANGLER_VERSION" versions list --name "$WORKER_NAME" --json > "$RECEIPT_DIR/versions-before-$STAMP.json"
npx --yes "wrangler@$WRANGLER_VERSION" deployments list --name "$WORKER_NAME" --json > "$RECEIPT_DIR/deployments-before-$STAMP.json"

PREVIOUS_VERSION=$(python3 - "$RECEIPT_DIR/deployments-before-$STAMP.json" <<'PY'
import json, sys
rows = json.load(open(sys.argv[1]))
if not rows:
raise SystemExit('No previous deployment available for rollback')
latest = max(rows, key=lambda row: row['created_on'])
versions = [item for item in latest['versions'] if item['percentage'] == 100]
if len(versions) != 1:
raise SystemExit('Expected one previous version at 100% traffic')
print(versions[0]['version_id'])
PY
)
echo "Rollback target: $PREVIOUS_VERSION"

npx --yes "wrangler@$WRANGLER_VERSION" versions upload worker-entry.mjs \
--name "$WORKER_NAME" \
--compatibility-date 2026-06-21 \
--keep-vars \
--message "Routing: 404 unknown paths, restore /awesome-free-dev-tools/buy redirect"
npx --yes "wrangler@$WRANGLER_VERSION" versions list --name "$WORKER_NAME" --json > "$RECEIPT_DIR/versions-after-upload-$STAMP.json"

NEW_VERSION=$(python3 - \
"$RECEIPT_DIR/versions-before-$STAMP.json" \
"$RECEIPT_DIR/versions-after-upload-$STAMP.json" <<'PY'
import json, sys
before = {row['id'] for row in json.load(open(sys.argv[1]))}
after = [row['id'] for row in json.load(open(sys.argv[2])) if row['id'] not in before]
if len(after) != 1:
raise SystemExit(f'Expected one uploaded version, found {after}')
print(after[0])
PY
)
echo "Uploaded version: $NEW_VERSION"

npx --yes "wrangler@$WRANGLER_VERSION" versions deploy \
--name "$WORKER_NAME" \
--version-id "$NEW_VERSION" \
--percentage 100 \
--message "Routing fix: real 404s, working paid-product redirect" \
--yes

if ! node verify-routing.mjs > "$RECEIPT_DIR/routing-verification-$STAMP.json"; then
npx --yes "wrangler@$WRANGLER_VERSION" versions deploy \
--name "$WORKER_NAME" \
--version-id "$PREVIOUS_VERSION" \
--percentage 100 \
--message "Automatic rollback after routing verification failure" \
--yes
echo "Public verification failed; rolled back to $PREVIOUS_VERSION." >&2
echo "Receipt: $RECEIPT_DIR/routing-verification-$STAMP.json" >&2
exit 1
fi

npx --yes "wrangler@$WRANGLER_VERSION" deployments list --name "$WORKER_NAME" --json > "$RECEIPT_DIR/deployments-after-$STAMP.json"
echo "Deployment $NEW_VERSION verified. Rollback target was $PREVIOUS_VERSION. Receipts: $RECEIPT_DIR/*-$STAMP.json"
73 changes: 73 additions & 0 deletions workers/agentpay-landing/verify-routing.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env node

// Public verification gate for the routing fix: every known route must keep
// working, the paid product route must reach Stripe, and unknown paths must
// 404. The old catch-all returned 200 for everything, which made a dead route
// indistinguishable from a live one — that is exactly what this asserts against.

const base = (process.argv[2] || 'https://agentpay.so').replace(/\/$/, '')
const results = []

function record(url, ok, detail) {
results.push({ url, ok, detail })
}

async function expectOk(path, required = []) {
const url = `${base}${path}`
const response = await fetch(url, { redirect: 'follow' })
const body = await response.text()
const missing = required.filter(text => !body.includes(text))
record(url, response.status === 200 && missing.length === 0, {
status: response.status,
missing,
})
}

// Unknown paths must 404 and must not be a copy of the landing page.
async function expectNotFound(path) {
const url = `${base}${path}`
const response = await fetch(url, { redirect: 'manual' })
const body = await response.text()
const looksLikeLanding = body.includes('AgentPay Labs is shipping small paid tools')
record(url, response.status === 404 && !looksLikeLanding, {
status: response.status,
looksLikeLanding,
})
}

// The paid route must redirect to Stripe, not fall through to the landing page.
async function expectStripeRedirect(path) {
const url = `${base}${path}`
const response = await fetch(url, { redirect: 'manual' })
const location = response.headers.get('location') ?? ''
record(url, response.status === 302 && location.startsWith('https://buy.stripe.com/'), {
status: response.status,
location,
})
}

await expectOk('/', ['AgentPay', 'Privacy', 'Terms'])
await expectOk('/index.html', ['AgentPay'])
await expectOk('/awesome-free-dev-tools', ['Awesome Free Dev Tools'])
await expectOk('/terms', ['AgentPay Terms of Service'])
await expectOk('/privacy', ['AgentPay Privacy Policy'])
await expectOk('/postizzz', ['Postizzz'])
await expectOk('/rank/agentpay-demo', ['AgentRank'])
await expectOk('/awesome-free-dev-tools/status.json', ['payment_link'])

await expectStripeRedirect('/awesome-free-dev-tools/buy')

await expectNotFound('/nonexistent-route-xyz123')
await expectNotFound('/awesome-free-dev-tools/bogus')
await expectNotFound('/terms/extra')

const health = await fetch(`${base}/health`)
const healthBody = await health.json().catch(() => null)
record(`${base}/health`, health.ok && healthBody?.status === 'ok', {
status: health.status,
reported: healthBody?.status ?? null,
})

const failed = results.filter(result => !result.ok)
console.log(JSON.stringify({ base, ok: failed.length === 0, results }, null, 2))
process.exit(failed.length === 0 ? 0 : 1)
32 changes: 22 additions & 10 deletions workers/agentpay-landing/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,23 @@ async function handleRequest(request) {
})
}

// Everything else gets the landing page
return new Response(LANDING_PAGE, {
// Landing page is served only at the site root. Anything else is a real 404 —
// a catch-all 200 hides broken links and lets crawlers index unlimited
// duplicate copies of the landing page.
if (url.pathname === '/' || url.pathname === '/index.html') {
return new Response(LANDING_PAGE, {
headers: mergeHeaders({
'Content-Type': 'text/html;charset=UTF-8',
'Cache-Control': 'public, max-age=3600'
})
})
}

return new Response(renderNotFoundPage('Page not found', 'That page does not exist on agentpay.so.'), {
status: 404,
headers: mergeHeaders({
'Content-Type': 'text/html;charset=UTF-8',
'Cache-Control': 'public, max-age=3600'
'Cache-Control': 'public, max-age=300'
})
})
}
Expand Down Expand Up @@ -380,12 +392,12 @@ function renderRankPage(agent, url) {
</html>`
}

function renderNotFoundPage(message) {
function renderNotFoundPage(message, hint = 'Check the agent identifier and try again.') {
return `<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Not found — AgentPay</title><meta name="robots" content="noindex">
<style>body{margin:0;background:#0B0F14;color:#F4F1EA;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.wrap{max-width:680px;margin:0 auto;padding:72px 24px}a{color:#FFB020;text-decoration:none}.eyebrow{color:#FFB020;font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}h1{font-size:44px;line-height:1.05;margin:10px 0 14px;letter-spacing:0}p{color:#C9CDD4}</style></head>
<body><main class="wrap"><p class="eyebrow">404</p><h1>${escapeHtml(message)}</h1><p>Check the agent identifier and try again.</p><p><a href="/">Return to AgentPay</a></p></main></body></html>`
<body><main class="wrap"><p class="eyebrow">404</p><h1>${escapeHtml(message)}</h1><p>${escapeHtml(hint)}</p><p><a href="/">Return to AgentPay</a></p></main></body></html>`
}

const LANDING_PAGE = `<!DOCTYPE html>
Expand Down Expand Up @@ -1024,15 +1036,15 @@ const BIDDESK_PAGE = `<!DOCTYPE html>
body { background:#0a0a0f; color:#F4F1EA; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; line-height:1.6; }
.container { max-width:860px; margin:0 auto; padding:0 24px; }
nav { padding:20px 0; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(255,255,255,0.06); }
.logo { font-size:18px; font-weight:700; letter-spacing:-0.5px; color:#F4F1EA; }
.logo { font-size:18px; font-weight:700; letter-spacing:0; color:#F4F1EA; }
.logo span { color:#FFB020; }
header { padding:72px 0 40px; }
h1 { font-size:38px; line-height:1.2; letter-spacing:-1px; max-width:640px; }
h1 { font-size:38px; line-height:1.2; letter-spacing:0; max-width:640px; }
h1 em { color:#FFB020; font-style:normal; }
.sub { color:rgba(255,255,255,0.6); margin-top:16px; max-width:560px; font-size:17px; }
.honest { margin-top:20px; padding:14px 18px; border-left:3px solid #FFB020; background:rgba(255,176,32,0.06); font-size:15px; color:rgba(255,255,255,0.8); max-width:560px; }
.tiers { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:16px; margin:48px 0; }
.tier { border:1px solid rgba(255,255,255,0.1); border-radius:12px; padding:24px; }
.tier { border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:24px; }
.tier.featured { border-color:#FFB020; }
.tier h3 { font-size:17px; }
.price { font-size:30px; font-weight:700; margin:10px 0; color:#FFB020; }
Expand All @@ -1052,8 +1064,8 @@ const BIDDESK_PAGE = `<!DOCTYPE html>
<div class="container">
<nav><div class="logo">Bid<span>Desk</span></div><div style="color:rgba(255,255,255,0.4);font-size:13px;">by AgentPay Labs</div></nav>
<header>
<h1>Win more cleaning &amp; FM contracts — <em>without hiring a bid writer</em></h1>
<p class="sub">You run the cleaning company. We run the paperwork. Compliance-checked SQ and ITT responses for UK soft-FM tenders AI-drafted, reviewed by a named human, delivered in 72 hours at a fixed fee.</p>
<h1>Cleaning &amp; FM tender responses <em>without hiring a bid writer</em></h1>
<p class="sub">You run the cleaning company. We run the paperwork. Compliance-checked SQ and ITT responses for UK soft-FM tenders, AI-drafted, reviewed by a named human, delivered in 72 hours at a fixed fee.</p>
<div class="honest"><strong>We never guarantee a win.</strong> Nobody honestly can. What you get: a complete, compliant, deadline-ready submission pack built on your real accreditations and your real experience.</div>
</header>
<div class="tiers">
Expand Down
28 changes: 28 additions & 0 deletions workers/agentpay-landing/worker.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ try {
assert.match(postizzzHtml, /href="\/privacy"/)
assert.match(postizzzHtml, /href="\/terms"/)

const bidDeskResponse = await handleRequest(new Request('https://agentpay.so/biddesk'))
assert.equal(bidDeskResponse.status, 200)
assert.match(bidDeskResponse.headers.get('content-type'), /text\/html/)
const bidDeskHtml = await bidDeskResponse.text()
assert.match(bidDeskHtml, /BidDesk/)
assert.match(bidDeskHtml, /Cleaning &amp; FM tender responses/)
assert.match(bidDeskHtml, /Compliance-checked SQ and ITT responses/)
assert.match(bidDeskHtml, /No win guarantees/)
assert.match(bidDeskHtml, /mailto:biddesk@agentpay\.so\?subject=Draft%20Desk/)
assert.doesNotMatch(bidDeskHtml, /Win more/)

const bidDeskTrailingSlashResponse = await handleRequest(new Request('https://agentpay.so/biddesk/'))
assert.equal(bidDeskTrailingSlashResponse.status, 200)

const productResponse = await handleRequest(new Request('https://agentpay.so/awesome-free-dev-tools'))
assert.equal(productResponse.status, 200)
const productHtml = await productResponse.text()
Expand Down Expand Up @@ -163,6 +177,20 @@ try {
assert.match(landingHtml, /Owner-authorized accounts/)
assert.match(landingHtml, /Fail-closed routing/)

const indexResponse = await handleRequest(new Request('https://agentpay.so/index.html'))
assert.equal(indexResponse.status, 200)

// Unknown paths must 404. A catch-all 200 hides broken links (it previously
// made /awesome-free-dev-tools/buy indistinguishable from a dead route) and
// lets crawlers index unlimited duplicate copies of the landing page.
for (const deadPath of ['/nonexistent-route-xyz123', '/awesome-free-dev-tools/bogus', '/terms/extra']) {
const missingResponse = await handleRequest(new Request(`https://agentpay.so${deadPath}`))
assert.equal(missingResponse.status, 404, `${deadPath} should 404`)
const missingHtml = await missingResponse.text()
assert.match(missingHtml, /name="robots" content="noindex"/)
assert.doesNotMatch(missingHtml, /AgentPay Labs is shipping small paid tools/)
}

const apiResponse = await handleRequest(new Request('https://agentpay.so/api/agentrank'))
assert.equal(apiResponse.status, 299)
assert.equal(fetchCalled, true)
Expand Down