-
Notifications
You must be signed in to change notification settings - Fork 0
Pak Key Lifecycle
A pak_<32 hex> is the customer-facing credential you mint on behalf of an end-user. It has its own state machine separate from your billing system, and getting the lifecycle right is what separates a kit deployment that runs itself from one that paginates support tickets.
This page covers all six lifecycle states: mint → in-use → top-up → rotate → suspend → revoke.
You call:
const key = await proxies.poolKeys.create({
label: 'customer:alice@example.com',
trafficCapGB: 10,
expiresAt: new Date(Date.now() + 60 * 86_400_000).toISOString(),
idempotencyKey: stripeEvent.id,
});You get back:
{
id: 'pakkey_xxx', // stable, use this for lookups
key: 'pak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
// the SECRET — shown once, never again
label: 'customer:alice@example.com',
trafficCapGB: 10,
trafficUsedMB: 0,
expiresAt: '2026-07-27T00:00:00.000Z',
enabled: true,
isExpired: false,
createdAt: '2026-05-28T...',
}The key field is shown exactly once. Store it in your DB immediately — there is no API to retrieve it later (only audit-logged reveal for the masked form). If you lose it, your only option is regenerate() which mints a fresh secret.
The idempotencyKey is non-negotiable for production webhook handlers:
- Stripe will retry your webhook on any 5xx. Without idempotency, every retry mints another pak.
- The platform dedupes by
idempotencyKeywithin a 24-hour window — same key in that window returns the same pak (with secret) from cache. - Bind the idempotency key to a stable domain object: Stripe event ID, your invoice ID, your purchase row ID. Never
randomUUID()at retry time — that defeats the purpose.
After 24h, the dedupe expires. If you somehow process the same event after 24h (rare with most queues), you'll double-mint. Persist tx/event → pak mappings in your own DB if your event lag can exceed 24h.
The customer's HTTP/SOCKS5 client embeds the pak in the proxy URL:
http://psx_yourUsername-mbl-us-sid-jobX-rot-sticky:pak_xxx@gw.proxies.sx:7000
Every byte through that URL increments trafficUsedMB on the pak. Updates flush every ~5 seconds from the gateway to the central DB. There's no real-time read — trafficUsedMB is eventually consistent within ~10s.
Pre-flight rejection conditions (the gateway returns 407 E_AUTH_INVALID before any bytes flow):
enabled === false-
isExpired === true(expiresAtpassed) - Reseller's wholesale balance < 0 (your
psx_account empty)
Mid-flight enforcement (gateway cuts the connection):
-
trafficUsedMB / 1024 >= trafficCapGB(auto-suspend, see below)
When the customer pays again, don't mint a new pak. Top up the existing one:
await proxies.poolKeys.topUp(key.id, {
addTrafficGB: 10,
extendDays: 30,
idempotencyKey: stripeEvent.id,
});This is a single atomic server-side write:
trafficCapGB += addTrafficGBexpiresAt = max(now, current_expiresAt) + extendDays
The max(now, ...) is important — if the pak already expired, top-up extends from now, not from the past expiry. Customer doesn't lose the days they bought.
Why not mint? Because:
- Customer keeps the same
pak_string — no swap-credentials downtime - Their
sidsticky sessions stay pinned to the same modems - Your billing system stays simple (one pak per customer, not N)
- Auto-suspend (below) doesn't fire mid-payment-flow
If you're tempted to mint a parallel pak for "topping up," resist. Top-up is what the API is shaped for.
If a customer leaks their pak (committed to GitHub, in a screenshot, on Twitter), rotate immediately:
const refreshed = await proxies.poolKeys.regenerate(key.id, {
idempotencyKey: `rotate-${key.id}-${Date.now()}`,
});
// refreshed.key is the NEW pak_ secret — old one is invalidated IMMEDIATELYOld secret stops working mid-connection. Any TCP sessions using the old pak get an RST and a fresh 407 on retry. This is intentional and instant — no "5 minute grace period" for security.
Have the customer update their proxy URL with the new secret and reconnect. Their sid sticky pinning is preserved — they'll be reassigned to the same modem on the next request with the same sid.
When trafficUsedMB / 1024 >= trafficCapGB, the platform automatically flips enabled = false and writes an audit log entry auto_suspended_cap_exceeded. This is a defense against leaked credentials being used to drain your wholesale balance.
Auto-suspend does NOT auto-recover — even after top-up. You must explicitly:
await proxies.poolKeys.update(key.id, { enabled: true });This is intentional: if a pak got suspended because the cap was hit by an attacker, you don't want the next top-up to silently re-enable an already-compromised key. Have your top-up flow re-enable explicitly and audit-log it.
Also auto-suspends:
-
auto_suspended_expired— whenexpiresAtpasses and the nightly cron runs (03:30 UTC tidy-up). The pak rejects traffic immediately at expiry regardless; this just flips the boolean for cleanliness.
To pause a customer (failed payment, abuse investigation) without deleting:
await proxies.poolKeys.update(key.id, { enabled: false });Their proxy URL starts returning E_AUTH_INVALID immediately. Their pak_ and trafficCapGB and history are preserved. Flip enabled: true to reactivate.
await proxies.poolKeys.delete(key.id);Pak is gone — secret invalidated, future requests rejected. You cannot undelete. If you delete by accident, you have to mint a new pak with a new secret and the customer must update their config.
Use delete for: customer churn (definitely won't come back), GDPR / data-deletion requests, decommissioning test paks.
Use enabled: false for: maybe-coming-back, billing disputes, suspicious-but-not-confirmed-abuse.
Every mutation writes an audit log entry. The platform exposes:
// All audit entries across all your paks
const events = await proxies.poolKeys.audit({
action: 'auto_suspended_cap_exceeded', // optional filter
limit: 100,
});
// Scoped to one pak
const events = await proxies.poolKeys.auditForKey(key.id, { limit: 100 });Recorded actions:
-
create/update/topup/regenerate/delete -
reveal(audit-logged unmask via API) -
gateway_auth_success/gateway_auth_failure -
auto_suspended_cap_exceeded/auto_suspended_expired
Each entry records the ip, userAgent, requestId, authMethod (apiKey for SDK calls, jwt for dashboard, gateway for byte-flow events). Use these for incident forensics.
Retention: 90 days in the platform DB. For long-term retention, stream them into your own SIEM via the webhook events (POST /v1/reseller/webhooks).
[mint] ──────────────────► enabled=true, isExpired=false, trafficUsed=0
│ │
│ │ (use)
│ ▼
│ ┌─────── trafficUsed climbs ──┐
│ │ │
│ ▼ ▼
│ (cap reached) │ (expiresAt passes)
│ auto_suspended_ │ auto_suspended_
│ cap_exceeded │ expired
│ │ │
│ ▼ ▼
│ enabled=false ◄──────┐ enabled=false
│ │ │ │
│ [topUp] │ │ │
│ cap +=, expiry +│ [update │ [update │
│ │ enabled:true]│ enabled:true]
│ │ (manual) │ + extend? │
│ ▼ │ │
└─────────► enabled=true, larger cap ◄───────┘ │
│
[regenerate] ─────────────────────────► same pak_id, new secret │
│
[delete] ──────────────────────────────► gone, permanent ───────┘
// Use the Stripe event id as the idempotency key.
const key = await proxies.poolKeys.create({
label: `customer:${customer.email}`,
trafficCapGB: plan.gb,
expiresAt: addDays(new Date(), plan.days).toISOString(),
idempotencyKey: event.id,
});
// Store key.id + key.key in your DB. Don't fetch the secret again — you can't.const existingKeyId = await db.getPakIdForCustomer(customer.id);
await proxies.poolKeys.topUp(existingKeyId, {
addTrafficGB: plan.gb,
extendDays: plan.days,
idempotencyKey: event.id,
});
// Re-enable if it auto-suspended.
await proxies.poolKeys.update(existingKeyId, { enabled: true });const refreshed = await proxies.poolKeys.regenerate(existingKeyId, {
idempotencyKey: `rotate-${existingKeyId}-${Date.now()}`,
});
await db.updatePakSecret(customer.id, refreshed.key);
await sendEmail(customer.email, 'Your proxy credentials were rotated…');const key = await proxies.poolKeys.get(existingKeyId);
const usedGB = key.trafficUsedMB / 1024;
const remainingGB = key.trafficCapGB - usedGB;
const daysLeft = Math.max(0, Math.ceil((new Date(key.expiresAt) - Date.now()) / 86_400_000));- Sticky-Sessions-and-Rotation — how sid pinning interacts with regenerate
- Troubleshooting — pak-related error responses
- x402-and-Wallet-Setup — pak lifecycle under USDC payments
Wiki for bolivian-peru/proxy-reseller-kit · Built by Proxies.sx · Edit on GitHub