Agent Hosting (PureVPS Cross-Sell)
PrivateRouter sells AI inference + chat. PureVPS sells the persistent
compute environments to run an agent. The /agent-hosting surface
glues the two together so customers can buy both from one place — even
though the actual VPS provisioning lives on PureVPS, not here.
What's in the product
Public surface
/agent-hosting— index page with hero, FAQ, and a 3-col grid of allstatus='active'products/agent-hosting/[slug]— per-product detail page; the primary CTA links topurevps_product_url(opens in a new tab)- Homepage band — shows the 3
is_featured=trueproducts - Footer link — present on every page
Member surface
/dashboard— a dismissable upsell card sits between the balance card and the activity chart/billing/success— after a topup completes, a dismissable banner invites the user to view VPS plans
Both dismissals are local-only (localStorage); we don't track the
state server-side because it's purely a UX nudge.
Admin surface
/admin/agent-hosting— full CRUD, status filter, audit-logged- Sidebar link sits between "Billing" and "GPU Nodes"
Backend
Schema — agent_hosting_products
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
slug | varchar(80) UNIQUE | URL-safe identifier |
name | varchar(120) | display name |
tagline | varchar(200) | one-line pitch |
description | text | long-form copy |
image_url | varchar(500) NULL | optional |
monthly_price_usd | numeric(10,2) | dollars |
included_credits_usd | numeric(10,2) NULL | NULL = no credits bundled |
purevps_product_url | varchar(500) | deep-link to PureVPS checkout |
status | varchar(20) | active | draft | archived |
is_featured | boolean | shows on homepage band |
sort_order | integer | lower = earlier |
created_at / updated_at | timestamptz |
Sort precedence for public listing:
is_featured DESC, sort_order ASC, name ASC
Endpoints
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/agent-hosting/products | public | active-only |
| GET | /api/admin/agent-hosting/products | admin | includes drafts |
| POST | /api/admin/agent-hosting/products | admin | 409 on slug clash |
| GET | /api/admin/agent-hosting/products/{id} | admin | |
| PATCH | /api/admin/agent-hosting/products/{id} | admin | partial update |
| DELETE | /api/admin/agent-hosting/products/{id} | admin | hard delete |
All mutating admin endpoints write an admin_audit_logs row.
Decimal serialisation
The API serialises Decimal columns as JSON strings ("49.00"), not
numbers — match this on the frontend types.
Seeded catalog (idempotent on container boot)
| Slug | Name | Monthly | Credits | Featured |
|---|---|---|---|---|
hermes-vps | Hermes Agent VPS | $49 | $10 | ✓ |
openclaw-vps | OpenClaw VPS | $59 | $15 | ✓ |
openhands-vps | OpenHands VPS | $59 | $15 | ✓ |
coding-agent-vps | Coding Agent VPS | $39 | $5 | |
browser-agent-vps | Browser Agent VPS | $49 | $10 |
Seeding lives in apps/api/scripts/seed.py — UPSERT by slug, runs as
part of the api container's boot command.
purevps_product_url values are placeholders
(https://purevps.com/order?product=<slug>). When PureVPS confirms the
real deep-link shape, update via the admin UI or edit the seeder.
Adding a new product
Through the admin UI (recommended)
- Visit
/admin/agent-hosting - Click New product
- Fill in slug (lowercase, hyphens, must be unique), name, tagline, description, monthly price, PureVPS URL
- Pick status:
activeto publish immediately,draftto stage - Save — the row appears in
/agent-hostingwithin ~5 minutes (the public page revalidates every 300s)
Through the seeder (for catalog defaults)
Add an entry to the seed list in apps/api/scripts/seed.py. The
seeder upserts by slug so re-running is idempotent. Restart the api
container to apply.
What's not integrated yet (M7 explicit non-goals)
- Real PureVPS provisioning API. The CTA is a deep-link, not a programmatic provisioning call. When a customer clicks "Deploy on PureVPS", they leave PrivateRouter and complete the order on PureVPS's own checkout.
- Bundle billing. The page copy may imply a discount when buying API credits + VPS together, but there's no enforcement. Adding a real bundle discount means either a discount-code flow on PrivateRouter's Stripe topup or shared cart with PureVPS — both are separate projects.
- Provisioning templates. "Preconfigured for Hermes / OpenClaw / OpenHands" is a marketing claim; the actual VPS templates live in the PureVPS catalog, not in this repo.
- Cross-product identity. A user who buys a Hermes VPS through PureVPS doesn't auto-sign-in to PrivateRouter. They get an API key the normal way.
These are all M7+ follow-ups. The current scope is "make the cross-sell discoverable and the catalog editable".
Smoke test
./infra/scripts/smoke-m7.sh --keep
15 checks, runs in seconds. Covers public listing, public page render, per-product render + 404, full admin CRUD round-trip including audit log writes, 403 for non-admin, homepage band, dashboard/success render.
Open ops questions
- Real PureVPS deep-link URL shape — currently
https://purevps.com/order?product=<slug>. Confirm with PureVPS. - Should the dismissal state move server-side? localStorage is fine for MVP but means the upsell re-appears if a user clears their browser. Low priority.
- Bundle pricing strategy — if we want "save 15% when bundled" to be real, decide which side runs the discount.