For AI agents / code builders: read skill.md first — it has the full decision tree, per-language code patterns, and security checklist in Anthropic skill format. | GitHub: bolivian-peru/pool-starter
Traditional proxy resale means buying modem hardware, juggling SIM plans, running a farm, and paying a developer to build the customer-facing layer. Every supplier price hike eats your margin.
The Proxies.sx Pool Gateway takes care of the infrastructure — you get a single endpoint (gw.proxies.sx:7000) backed by real 4G/5G mobile and residential proxies in 6 countries (DE, PL, US, FR, ES, GB), wholesale pricing with volume tiers, and a per-customer sub-key system (pak_*).
This open-source toolkit takes care of the software — typed SDK, drop-in React component, full Next.js storefront, and a language-agnostic REST API. Zero paid dependencies beyond what you choose (SMTP provider, hosting). MIT licensed — fork it, rebrand it, ship it.
Match your stack to a path. The implementation differs significantly between them — don't mix.
| If you... | Use | Effort |
|---|---|---|
| Want a branded reseller storefront, starting fresh | PATH A — Clone the Next.js starter | ~10 min |
| Already have a React / Next.js app and want a drop-in dashboard | PATH B — <PoolPortal /> component | ~15 min |
| Have a non-React JS app (Express, Fastify, Hono, Vue+API, plain Node, Bun, Deno, Workers) | PATH C — SDK only | ~10 min |
| Backend is PHP / Python / Go / Ruby / Rust / Elixir (anything not JS) | PATH D — REST API directly | ~5 min |
Decision tree + step-by-step for each path also in skill.md.
You need one thing: a Proxies.sx reseller API key.
customers:writepsx_... value — server-side only, never expose to the browserYou'll also see a "reseller username" of the form psx_<id> in the dashboard. That value is safe to reference in proxy URLs (it's the public part of the proxy auth) — it's NOT the secret API key.
| Credential | Format | Where it lives | Sensitivity |
|---|---|---|---|
| Reseller API key | psx_<hash> | Your server env (.env, secrets manager) | SECRET — never to browser |
| Reseller username | psx_<id> | Embedded in proxy URLs, server-known | Public-ish (proxy basic-auth) |
| Customer Pool Access Key | pak_<hash> | Minted via API, stored in your DB, given to customer | SECRET — handle like a password |
Use when you want a complete branded reseller site (landing, pricing, magic-link login, Stripe checkout, customer dashboard) and are starting from scratch.
git clone https://github.com/bolivian-peru/pool-starter.git my-shop
cd my-shop/apps/starter
cp .env.example .env
# Edit .env: PROXIES_SX_API_KEY, PROXIES_SX_USERNAME, STRIPE_*, AUTH_SECRET, DATABASE_URL
pnpm install
docker compose up -d db # local Postgres on :5432
pnpm db:migrate # idempotent schema bootstrap
pnpm dev # → http://localhost:3000
In another terminal:
stripe listen --forward-to localhost:3000/api/stripe/webhook
| Route | Purpose |
|---|---|
/ | Landing + pricing tiers (configured in src/config.ts) |
/login | NextAuth (Auth.js v5) magic-link auth (in dev, link prints to console — no SMTP needed) |
/dashboard | <PoolPortal /> showing customer's pak_, country selector, copy-to-clipboard proxy URLs |
/api/stripe/checkout | Stripe checkout session per pricing tier |
/api/stripe/webhook | Mints pak_ on payment success (signature-verified, idempotent) |
/api/pool/[...path] | Server-side SDK calls (keeps psx_ off the client) |
Edit one file: apps/starter/src/config.ts
export const config = {
brand: { name: 'AcmeProxies', primaryColor: '#7c3aed', supportEmail: '...' },
pricing: [
{ id: 'starter', displayName: 'Starter', gb: 5, priceUsd: 35 },
{ id: 'pro', displayName: 'Pro', gb: 25, priceUsd: 150 },
{ id: 'scale', displayName: 'Scale', gb: 100, priceUsd: 500 },
],
countries: ['us', 'de', 'pl', 'fr', 'es', 'gb'],
};
Deploy on a VPS with Caddy/nginx in front of localhost:3000 for TLS. Full task-by-task guide for AI agents: apps/starter/CLAUDE.md.
Use when you already have auth + billing + a UI shell, and just want a proxy dashboard on a page.
npm install @proxies-sx/pool-portal-react @proxies-sx/pool-sdk
app/api/pool/[...path]/route.ts:
import { createPoolApiHandlers } from '@proxies-sx/pool-portal-react/server';
import { auth } from '@/lib/auth';
const handlers = createPoolApiHandlers({
apiKey: process.env.PROXIES_SX_API_KEY!,
proxyUsername: process.env.PROXIES_SX_USERNAME!,
// CRITICAL: scope by authenticated user — without this,
// customer A can read customer B's keys.
resolveCustomerContext: async () => {
const session = await auth();
if (!session?.user?.id) throw new Response('Unauthorized', { status: 401 });
return { customerId: session.user.id };
},
});
export const GET = handlers.GET;
export const POST = handlers.POST;
export const PATCH = handlers.PATCH;
export const DELETE = handlers.DELETE;
'use client';
import { PoolPortal } from '@proxies-sx/pool-portal-react';
import '@proxies-sx/pool-portal-react/styles.css';
export default function Dashboard() {
return (
<PoolPortal
apiRoute="/api/pool"
branding={{ name: 'AcmeProxies', primaryColor: '#7c3aed' }}
/>
);
}
If <PoolPortal /> doesn't fit your design, use the hooks directly:
import {
usePoolKey, usePoolStock, useIncidents, useCopyToClipboard,
} from '@proxies-sx/pool-portal-react';
const { key, isLoading, regenerate } = usePoolKey({ apiRoute: '/api/pool' });
const { stock } = usePoolStock({ apiRoute: '/api/pool' });
For non-Next.js React stacks (CRA, Vite, Remix), implement the same handlers in your own backend framework. The hooks make POST/GET/PATCH/DELETE calls to {apiRoute}/keys and {apiRoute}/stock.
Use when you have a non-React frontend (Vue, Svelte, plain HTML) but a JS backend (Express, Fastify, Hono, Bun, Cloudflare Workers, Deno).
npm install @proxies-sx/pool-sdk
import { ProxiesClient } from '@proxies-sx/pool-sdk';
// Server-side ONLY. Never bundle PROXIES_SX_API_KEY into a browser build.
const proxies = new ProxiesClient({
apiKey: process.env.PROXIES_SX_API_KEY!,
proxyUsername: process.env.PROXIES_SX_USERNAME!,
});
// Mint a key for a customer who just paid
const key = await proxies.poolKeys.create({
label: `customer:${customerId}`,
trafficCapGB: 10, // null/omit = unlimited within reseller's pool
});
// Store key.id (for management) and key.key (the pak_ secret)
await db.update(customerId, { pakKeyId: key.id, pakKey: key.key });
// Build the proxy URL the customer uses in their HTTP client
const proxyUrl = proxies.buildProxyUrl(key.key, {
country: 'us',
sid: customerId, // sticky session — same customer = same exit IP
rotation: 'sticky',
});
// → "http://psx_abc-mbl-us-sid-123-rot-sticky:pak_xyz@gw.proxies.sx:7000"
| Method | Description |
|---|---|
poolKeys.list() | List all keys with usage |
poolKeys.update(keyId, { label, enabled, trafficCapGB }) | Update a key |
poolKeys.regenerate(keyId) | Rotate the secret (old pak_ stops working immediately) |
poolKeys.delete(keyId) | Permanent delete |
pool.getStock() | Live endpoint count by country |
pool.getIncidents() | Active pool incidents |
Runtime support: Node 18.17+, Bun, Deno (with npm: specifier), Vercel Edge, Cloudflare Workers. Zero runtime deps. Pass fetch in config if your runtime lacks global fetch.
Use when your backend is not JavaScript. The SDK is a thin wrapper around a public REST API — anyone with an HTTP client can integrate.
Auth: X-API-Key: psx_... on every request.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/reseller/pool-keys | Mint a pak_ key. Accepts optional expiresAt (ISO datetime) for time-bounded credits. |
| GET | /v1/reseller/pool-keys | List keys + usage. Returns expiresAt + server-computed isExpired flag. |
| PATCH | /v1/reseller/pool-keys/{keyId} | Update label / enabled / trafficCapGB / expiresAt. Pass expiresAt: null to remove an existing expiry. |
| POST | /v1/reseller/pool-keys/{keyId}/regenerate | Rotate secret (old value invalidated immediately) |
| DELETE | /v1/reseller/pool-keys/{keyId} | Permanent delete |
Base URL: https://api.proxies.sx/v1
Interactive docs: api.proxies.sx/docs/api · OpenAPI 3.0 spec: api.proxies.sx/docs/api-json
curl -X POST https://api.proxies.sx/v1/reseller/pool-keys \
-H "X-API-Key: psx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"customer:alice@example.com","trafficCapGB":10}'
# Response:
# {
# "id": "65f...",
# "key": "pak_a1b2c3...",
# "label": "...",
# "trafficCapGB": 10,
# "trafficUsedGB": 0,
# "enabled": true,
# "createdAt": "..."
# }
requests)import requests
resp = requests.post(
"https://api.proxies.sx/v1/reseller/pool-keys",
headers={"X-API-Key": "psx_YOUR_API_KEY"},
json={"label": "customer:alice", "trafficCapGB": 10},
)
key = resp.json()["key"] # "pak_..."
# Use it as a proxy
proxies = {
"http": f"http://psx_RESELLER-mbl-us-sid-alice-rot-sticky:{key}@gw.proxies.sx:7000",
"https": f"http://psx_RESELLER-mbl-us-sid-alice-rot-sticky:{key}@gw.proxies.sx:7000",
}
r = requests.get("https://api.ipify.org", proxies=proxies)
$ch = curl_init('https://api.proxies.sx/v1/reseller/pool-keys');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: psx_YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'label' => 'customer:alice',
'trafficCapGB' => 10,
]),
]);
$key = json_decode(curl_exec($ch), true)['key']; // pak_...
body := strings.NewReader(`{"label":"customer:alice","trafficCapGB":10}`)
req, _ := http.NewRequest("POST",
"https://api.proxies.sx/v1/reseller/pool-keys", body)
req.Header.Set("X-API-Key", "psx_YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
require 'net/http'; require 'json'
uri = URI('https://api.proxies.sx/v1/reseller/pool-keys')
req = Net::HTTP::Post.new(uri,
'X-API-Key' => 'psx_YOUR_API_KEY',
'Content-Type' => 'application/json')
req.body = { label: 'customer:alice', trafficCapGB: 10 }.to_json
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
The customer's HTTP/SOCKS5 client connects to:
{protocol}://{username}:{pakKey}@gw.proxies.sx:{port}
| Field | Value |
|---|---|
protocol | http or socks5 |
port | 7000 for HTTP, 7001 for SOCKS5 |
username | psx_RESELLER_USERNAME + optional --separated tokens |
pakKey | The pak_* secret minted via the API |
All optional, in any order, separated by -:
| Token | Example | Meaning |
|---|---|---|
| Pool | mbl, peer | mbl = ProxySmart mobile modems (default), peer = residential peer devices |
| Country | us, de, pl, fr, es, gb | ISO 3166-1 alpha-2 |
sid-{id} | sid-alice | Sticky session — same sid keeps the same exit IP |
rot-{mode} | rot-sticky, rot-auto10, rot-auto30, rot-hard, rot-none | IP rotation policy |
city-{name} | city-nyc | City filter (when supported) |
carrier-{name} | carrier-att | Carrier filter |
http://psx_acme-mbl-us-sid-customer123-rot-sticky:pak_a1b2c3@gw.proxies.sx:7000
This says: route customer123's traffic through US mobile modems, keep the same exit IP for the session.
The SDK's buildProxyUrl(pakKey, opts) generates this. In other languages, build the string manually:
# Python
def build_proxy_url(reseller, pak_key, country='us', sid=None, rotation='sticky'):
parts = [reseller, 'mbl', country]
if sid: parts.append(f'sid-{sid}')
if rotation: parts.append(f'rot-{rotation}')
return f"http://{'-'.join(parts)}:{pak_key}@gw.proxies.sx:7000"
async function onStripeCheckoutCompleted(event) {
const session = event.data.object;
const customerId = session.client_reference_id;
const gbPurchased = Number(session.metadata.gb);
const key = await proxies.poolKeys.create({
label: `customer:${customerId}`,
trafficCapGB: gbPurchased,
});
await db.update(customerId, {
pakKeyId: key.id,
pakKey: key.key,
});
}
async function rotateForCustomer(customerId) {
const customer = await db.get(customerId);
const { id, key } = await proxies.poolKeys.regenerate(customer.pakKeyId);
await db.update(customerId, { pakKey: key });
return key; // hand to UI
}
const keys = await proxies.poolKeys.list();
const ours = keys.find(k => k.id === customer.pakKeyId);
console.log(`${ours.trafficUsedGB} / ${ours.trafficCapGB ?? '∞'} GB used`);
await proxies.poolKeys.update(customer.pakKeyId, {
trafficCapGB: customer.trafficCapGB + additionalGB,
});
Pass expiresAt (ISO datetime or Date) on create / update. Past the expiry, the gateway rejects the key immediately — no waiting for a cron. The platform's daily cron (03:30 UTC) flips enabled=false on past-expiry keys for tidier admin queries.
// Mint with a 60-day expiry
const key = await proxies.poolKeys.create({
label: 'customer:alice',
trafficCapGB: 10,
expiresAt: new Date(Date.now() + 60 * 86_400_000).toISOString(),
});
// On top-up, bump cap AND push expiry forward in one call
await proxies.poolKeys.update(key.id, {
trafficCapGB: 25,
expiresAt: new Date(Date.now() + 60 * 86_400_000).toISOString(),
});
// Remove the expiry (perpetual key)
await proxies.poolKeys.update(key.id, { expiresAt: null });
// Helpers
import { isPoolKeyExpired, daysUntilPoolKeyExpiry } from '@proxies-sx/pool-sdk';
isPoolKeyExpired(key); // boolean — true if past expiry
daysUntilPoolKeyExpiry(key); // number | null — days remaining
SDK ≥ 0.2.0 required. The <PoolPortal /> component shows an amber banner at <7 days remaining and a red one once expired (just include expiresAt + isExpired in your /api/pool/me response).
| Status | Meaning | Action |
|---|---|---|
200 / 201 | Success | Use the response body |
400 | Validation error | Show error details to the user, don't retry |
401 | API key invalid or revoked | Re-mint key from client.proxies.sx/account |
403 | Scope insufficient | Add customers:write to the key |
404 | Key doesn't exist | Stop — don't loop |
429 | Rate-limited | Back off (exponential, start at 1s) |
500–599 | Server error | Retry up to 3× with exponential backoff |
The SDK ships these as typed errors:
import { ProxiesApiError, ProxiesTimeoutError } from '@proxies-sx/pool-sdk';
try {
await proxies.poolKeys.create({ label: 'x' });
} catch (err) {
if (err instanceof ProxiesApiError) {
if (err.isAuth) { /* 401/403 */ }
if (err.isRateLimited) { /* 429 */ }
if (err.isServer) { /* 5xx */ }
} else if (err instanceof ProxiesTimeoutError) {
/* request exceeded timeout */
}
}
| Rule | Why it matters |
|---|---|
PROXIES_SX_API_KEY is server-only | Never inline in next.config.js, never NEXT_PUBLIC_*, never ship to browser bundle. Trust boundary lives at your backend. |
| Scope every request by authenticated user | In PATH B, resolveCustomerContext MUST read the session. Without it, customer A reads/regenerates customer B's keys. |
| Use parameterized SQL if storing keys | The starter app uses $1, $2 placeholders, never string interpolation. |
| Verify Stripe webhook signatures | The starter does this. If you adapt it, do not comment out the check "to test". |
Rotate leaked pak_ immediately | Call regenerate(keyId) — the old value is invalidated within ~1 second. |
Store psx_ in a secrets manager | Production: 1Password / Doppler / AWS Secrets Manager — not .env in git. |
Wholesale rates from Proxies.sx have volume tiers. Do not hardcode dollar amounts in your app — they are configured by the platform and can change.
| To get current rates... | Endpoint |
|---|---|
| Programmatically | GET /v1/x402/pricing (public, no auth) |
| Via dashboard | client.proxies.sx |
You set your retail price (whatever you charge your own customers) — that lives in your own app config. The wholesale rate affects your margin, not your customer-facing pricing UI.
| Resource | URL |
|---|---|
| Main repo | github.com/bolivian-peru/pool-starter |
| SKILL.md (AI agent integration guide) | SKILL.md |
| SDK README (full API surface) | packages/sdk/README.md |
| React component README | packages/react/README.md |
| Starter app README | apps/starter/README.md |
| Repo CLAUDE.md (agents working on the SDK) | CLAUDE.md |
| Starter CLAUDE.md (agents customizing the storefront) | apps/starter/CLAUDE.md |
| Package | Install |
|---|---|
@proxies-sx/pool-sdk | npm i @proxies-sx/pool-sdk |
@proxies-sx/pool-portal-react | npm i @proxies-sx/pool-portal-react |
| Resource | URL |
|---|---|
| Swagger UI (interactive) | api.proxies.sx/docs/api |
| OpenAPI 3.0 spec (JSON) | api.proxies.sx/docs/api-json |
| Reseller LLM-friendly reference | api.proxies.sx/v1/reseller/docs/llm |
| Live pricing | api.proxies.sx/v1/x402/pricing |
| Live pool stock | api.proxies.sx/v1/gateway/pool/availability |
| File | URL | Purpose |
|---|---|---|
| This page's skill | /build/skill.md | Anthropic skill format — drop into Claude Code, Cursor, etc. |
| Master Proxies.sx skill | /skill.md | Full infrastructure reference |
| Marketplace skill | /marketplace/skill.md | x402 service catalog |
| Peer skill | /peer/skill.md | Bandwidth-sharing reference |
| Channel | For |
|---|---|
| Telegram @proxyforai | Quick questions, onboarding |
| Twitter/X @sxproxies | Updates, support |
| agents@proxies.sx | Reseller upgrade requests |
| GitHub issues | Bug reports, feature requests |