Kiến trúc một website bán lại proxy: từ landing page tới provider API
Bộ khung chung mình rút ra sau nhiều dự án website bán proxy: landing page cho SEO và affiliate, pipeline mua hàng theo strategy, thanh toán và webhook an toàn, tầng provider chống cấp trùng, cron và API cho reseller.
- Architecture
- Backend
- Frontend
Mình đã làm cho khá nhiều website bán proxy, cả frontend lẫn backend: từ landing page cần SEO tốt, trang bảng giá có configurator, tới backend gọi API của các nhà cung cấp IP để cấp proxy cho khách. Sau vài dự án, mình nhận ra chúng có chung một bộ khung. Bài này ghi lại bộ khung đó để ai cần xây một website reseller proxy có thể bắt đầu nhanh hơn và tránh những lỗi mình đã gặp.
Reseller proxy là gì#
Phần lớn website bán proxy không sở hữu IP. Họ mua lại từ các nhà cung cấp (upstream provider) qua API reseller, đóng gói thành gói cước của mình, rồi bán cho khách. Việc của hệ thống là:
- Thu hút và thuyết phục khách kỹ thuật, những người luôn so sánh nhiều nhà cung cấp cùng lúc.
- Nhận tiền qua nhiều kênh.
- Gọi API provider để cấp proxy (tạo user, cấp bandwidth, cấp IP) đúng thứ khách đã trả.
- Quản lý vòng đời: đồng bộ dung lượng đã dùng, gia hạn, tự động renew, thông báo hết hạn.
Mỗi loại proxy tính tiền theo một kiểu khác nhau, và đây là nguồn phức tạp lớn nhất:
| Loại | Cách tính | Ví dụ |
|---|---|---|
| Residential | Theo GB bandwidth | 10 GB, hết hạn sau 30 ngày |
| Unlimited residential | Theo tốc độ và thời gian | 100 Mbps trong 7 ngày |
| Datacenter | Số IP × bandwidth, cộng tuỳ chọn thêm | 50 IP, chọn quốc gia |
| ISP | Theo từng IP, giá bậc thang theo số lượng | 1–9 IP, 10–49 IP… |
| Mobile, IPv6 | Bandwidth hoặc không giới hạn, tuỳ gói |
Bức tranh tổng thể#
┌─────────────────────┐ ┌──────────────────────┐
│ Landing (Next.js) │ │ Dashboard khách │
│ SEO, pricing, tools│ │ mua, quản lý proxy │
└──────────┬──────────┘ └──────────┬───────────┘
│ đăng ký (giữ mã affiliate) │ REST
▼ ▼
┌──────────────────────────────────────────────────┐
│ Backend API (NestJS) │
│ auth · catalog · purchase · subscription │
│ public API (API key) · reseller API │
└───┬──────────────┬──────────────┬────────────────┘
│ │ │
PostgreSQL Redis + Bull Webhook ◀── Stripe / Crypto gateway
(Prisma) (queue, cache,
rate limit)
│ │
▼ ▼
┌──────────────────────────────────────────────────┐
│ Provider adapters: A · B · C · D · E · F │
│ (mỗi nhà cung cấp IP một client riêng) │
└──────────────────────────────────────────────────┘
▲
Cron: đồng bộ bandwidth · auto-renew · dọn whitelist
· thông báo hết hạn · dọn đơn chưa thanh toánFrontend: landing page là cỗ máy bán hàng#
Khách mua proxy là dân kỹ thuật và luôn mở vài tab so sánh. Landing page phải vừa rộng (để bắt từ khoá dài trên Google) vừa hữu ích trước khi mua.
- Trang riêng cho mọi thứ. Mỗi loại proxy một trang sản phẩm và một trang giá; mỗi quốc gia một trang (với Lightning Proxies là 49 trang quốc gia); mỗi trình duyệt anti-detect một trang hướng dẫn tích hợp. Từ khoá như "residential proxies japan" hay "octobrowser proxy setup" mang lại khách có ý định mua cao.
- Sinh tĩnh mọi route động. Trang quốc gia, trang tích hợp đều prerender lúc build. Sitemap, robots, structured data (Organization, Service, Breadcrumb) tự sinh.
- Công cụ miễn phí trên cùng domain: proxy checker, IPv6 checker, WHOIS, speed test, kiểm tra blacklist, trang trạng thái mạng. Chúng kéo traffic kỹ thuật vào, trang sản phẩm chuyển đổi thành đơn. Mỗi công cụ là client mỏng gọi qua route server, nơi giữ API key và validate đầu vào. Trình duyệt không bao giờ thấy credential.
- Configurator giá theo từng sản phẩm. Mỗi sản phẩm tính tiền khác nhau, nên đừng ép tất cả vào ba thẻ "Basic / Pro / Enterprise". Với Clean Proxies, mình gom năm configurator vào một route có tab: chuyển tab tức thì phía client, URL vẫn phản ánh lựa chọn nhưng không làm dài lịch sử trình duyệt.
- Affiliate attribution không được làm hỏng trang. Khách đến từ link đối tác phải vẫn được ghi nhận khi đăng ký vài phút sau, trên một subdomain khác. Mình lưu mã đối tác, gắn vào link đăng ký khi khách bấm, và báo về tracking API kèm token Cloudflare Turnstile để chống bot. Nguyên tắc quan trọng nhất: fail open. Nếu bất kỳ bước nào lỗi, khách vẫn thấy trang bình thường.
- Script bên thứ ba không chặn render. Live chat chỉ tải khi khách tương tác lần đầu, video là ảnh tĩnh cho tới khi bấm play, tracking tải ở mức ưu tiên thấp nhất.
Backend: một pipeline, nhiều strategy#
Bản đầu tiên mình tiếp nhận (viết bằng Express) có khoảng 85% code trùng lặp: cùng các bước validate, tính coupon, thanh toán được copy cho từng loại proxy. Khi viết lại bằng NestJS và Prisma, quyết định quan trọng nhất là tách phần chung khỏi phần riêng của từng sản phẩm.
PurchaseOrchestrator (chung cho mọi sản phẩm)
1. validate request, user, plan, phương thức thanh toán
2. strategy.validateProductInput() ← riêng: quốc gia, location, số IP…
3. strategy.computeBasePrice() ← riêng: tuỳ chọn thêm, pro-rata khi nâng cấp
4. áp coupon → phí → thuế → giá cuối
5. tạo subscription NOT_PAID
6. khởi tạo thanh toán (balance / thẻ / crypto)
7. strategy.provision() ← riêng: gọi API provider, CHỈ sau khi có tiềnMỗi sản phẩm là một class implement PurchaseStrategy. Thêm một sản phẩm mới là thêm một strategy, không đụng vào pipeline. Nhờ vậy, phần lớn code lặp của bản cũ biến mất: các bước chung chỉ còn viết một lần.
Thứ tự tính giá là hợp đồng#
Đây là chỗ dễ sai nhất. Viết ra rõ ràng và test kỹ:
basePrice = strategy.computeBasePrice(...)
priceAfterCoupon = basePrice − discount
fee, tax = chỉ áp cho thanh toán thẻ, tính trên priceAfterCoupon
finalPrice = priceAfterCoupon + fee + tax
Lưu cả: originalPrice (cho hoa hồng referral), discount, fee, tax, finalPriceLuôn dùng kiểu Decimal cho tiền, làm tròn 2 chữ số một cách tường minh. 0.1 + 0.2 trong JavaScript không bằng 0.3, và sai số sẽ lộ ra khi cộng nhiều tuỳ chọn phần trăm.
Coupon cũng cần giữ chỗ ngay khi tạo đơn (tăng lượt dùng có điều kiện, nguyên tử), rồi trả lại nếu thanh toán thất bại hoặc hết hạn. Nếu chỉ ghi nhận khi đã thanh toán, hai người có thể cùng dùng một coupon chỉ cho phép dùng một lần.
Thanh toán: ba nhánh, một nguyên tắc#
| Nhánh | Luồng |
|---|---|
| Số dư (balance) | Đồng bộ: trừ tiền, gọi provider, trả credential ngay |
| Thẻ (Stripe) | Tạo checkout session, chờ webhook rồi mới cấp proxy |
| Crypto (gateway) | Tạo invoice, chờ webhook rồi mới cấp proxy |
Nhánh balance là nhánh duy nhất cấp proxy trong cùng request, nên phải cẩn thận nhất:
- Trừ số dư trong transaction có khoá dòng user (SELECT … FOR UPDATE). Thiếu khoá, hai request đồng thời đều thấy đủ tiền và cấp hai proxy.
- Gọi API provider bên ngoài transaction. Không bao giờ giữ khoá database trong lúc chờ HTTP.
- Provider lỗi thì hoàn tiền trong một transaction khác. Nếu hoàn tiền cũng lỗi, đánh dấu CRITICAL_ERROR và báo admin ngay. Tuyệt đối không im lặng.
Webhook cần ba lớp bảo vệ:
- Xác thực chữ ký trên raw body, so sánh bằng timingSafeEqual.
- Chống xử lý lặp bằng bảng webhook_events có unique key theo event id. Insert trùng sẽ lỗi, và mình trả 200 ngay.
- Trả 200 kể cả khi cấp proxy thất bại. Đây là bài học đắt nhất. Nếu trả 500, gateway sẽ gửi lại webhook, hệ thống gọi provider thêm lần nữa và có thể cấp trùng. Thay vào đó: đánh dấu CRITICAL_ERROR, đưa vào hàng đợi xử lý lại có kiểm soát, báo admin, và gửi email cho khách "đã nhận tiền, đang cấp proxy".
Tầng provider: đừng tin mạng#
Mỗi nhà cung cấp có API, cách xác thực, định dạng response riêng. Mình viết mỗi provider một adapter, và có ba quy tắc áp cho tất cả.
1. Chỉ retry thao tác idempotent. Đọc dữ liệu hay đặt giá trị tuyệt đối thì retry thoải mái. Với request đồng bộ của khách, giới hạn số lần thử và tổng thời gian, và dùng AbortController để thật sự huỷ request khi timeout.
2. Thao tác cộng dồn cần "safe wrapper". API cấp bandwidth của nhiều provider là cộng thêm, không phải đặt giá trị. Nếu request timeout ở phía mình nhưng provider vẫn xử lý xong, retry sẽ cấp hai lần. Cách xử lý:
before = đọc bandwidth hiện tại (retry được)
assign(+N GB) (chỉ gọi MỘT lần, không retry)
after = đọc lại bandwidth (retry được)
delta = after − before
= N → ổn
> N → bị cấp trùng, gọi remove(delta − N)
< N → chưa cấp đủ, cấp bù phần thiếu3. Thao tác hai bước cần bù trừ. Chuyển GB giữa hai subscription gồm "trừ bên A" rồi "cộng bên B". Luôn trừ trước, cộng sau, để không tạo ra GB từ không khí. Nếu bước cộng lỗi thì cộng trả lại cho A. Nếu bước bù trừ cũng lỗi, đánh dấu và báo admin.
Ngoài ra, log mọi request tới provider nhưng che các trường nhạy cảm (API key, password proxy, token), và cắt ngắn response body.
Vòng đời: cron làm phần việc thầm lặng#
Sau khi bán, phần lớn công việc nằm trong các job chạy nền (@nestjs/schedule cộng Bull queue):
- Đồng bộ bandwidth từ provider về database theo chu kỳ, để dashboard hiển thị đúng dung lượng còn lại.
- Auto-renew cho từng loại sản phẩm: trừ balance hoặc thu qua thẻ đã lưu, rồi gia hạn ở provider.
- Dọn whitelist IP và thu hồi bandwidth đã hết hạn.
- Thông báo sắp hết hạn, theo thời gian hoặc theo dung lượng còn lại.
- Dọn đơn NOT_PAID quá hạn, đồng thời trả lại coupon đã giữ chỗ.
Mỗi job bật tắt và đổi lịch chạy được bằng biến môi trường, và đều phải idempotent: chạy lại sau khi crash không được gia hạn hai lần.
API cho khách và cho reseller#
Khách lớn thường muốn tự động hoá, nên hệ thống thường có hai loại API:
- Public API xác thực bằng API key: xem và quản lý proxy của chính họ, có whitelist method, rate limit và đếm số lần gọi theo sản phẩm.
- Reseller API cho đối tác bán lại: giá riêng cho từng reseller, luôn thanh toán bằng balance, có log hoạt động riêng.
Một bài học thực tế: khi viết lại hệ thống, mình thiết kế lại toàn bộ API nội bộ theo REST cho gọn, nhưng giữ nguyên đường dẫn và định dạng của API công khai. Khách và reseller đang gọi chúng từ code của họ, đổi là phá vỡ hợp đồng.
Bảo mật và vận hành#
- Cloudflare Turnstile cho đăng ký và đăng nhập; rate limit theo IP và theo user (dùng Redis, không lưu trong bộ nhớ process).
- Hash mật khẩu bằng argon2, 2FA bằng TOTP, đăng nhập Google và Discord.
- Validate biến môi trường lúc khởi động: secret đủ dài, và các cờ kiểu "bỏ qua kiểm tra chữ ký" bị cấm tuyệt đối ở production.
- Sentry cho lỗi, log có cấu trúc bằng pino, Swagger ở /docs.
- Thuế (ví dụ VAT EU) chỉ áp cho kênh cần áp; giới hạn số tiền theo phương thức thanh toán để giảm rủi ro gian lận thẻ.
Checklist#
- Landing rộng cho SEO: trang theo sản phẩm, theo quốc gia, theo tích hợp, sinh tĩnh; công cụ miễn phí trên cùng domain.
- Affiliate attribution fail open, không bao giờ làm hỏng trang.
- Một pipeline mua hàng chung, mỗi sản phẩm một strategy.
- Thứ tự tính giá viết thành tài liệu và có test; tiền dùng Decimal.
- Coupon giữ chỗ nguyên tử, trả lại khi đơn hết hạn.
- Nhánh balance có khoá dòng; gọi provider ngoài transaction; hoàn tiền có kiểm soát.
- Webhook: kiểm chữ ký, bảng webhook_events chống lặp, luôn trả 200, lỗi cấp proxy thì đưa vào hàng đợi xử lý lại.
- Provider: chỉ retry thao tác idempotent; thao tác cộng dồn dùng safe wrapper; thao tác hai bước có bù trừ.
- Cron idempotent cho đồng bộ, gia hạn, dọn dẹp, thông báo.
- API công khai giữ ổn định như một hợp đồng.
Kết#
Bề ngoài, một website bán proxy chỉ là landing page cộng bảng giá cộng dashboard. Nhưng vì không sở hữu IP, mọi đồng tiền khách trả đều phải đi qua API của một bên thứ ba có thể chậm, lỗi, hoặc xử lý xong mà không báo lại. Kiến trúc tốt ở đây không nằm ở công nghệ mới nhất, mà ở chỗ giả định mạng sẽ hỏng và thiết kế để khi nó hỏng, khách vẫn không mất tiền và mình vẫn biết chuyện gì đã xảy ra.
Các dự án liên quan: Lightning Proxies và Clean Proxies.
Enjoyed this? Get the next one by email.
Keep reading
3 Tips To Become A Powerful Human Being
Harnessing personal power is key to leading a fulfilling life. To do this, reduce your words and movements by 50 percent to conserve energy and enhance focus. Chanting a mantra can also generate personal power. This power is not about domination, but about becoming effective and empowered, allowing you to walk through life with dignity and grace.
Swapping out newbie coders for AI: What can it do and where does it fall short?
So, we're gonna chat about whether AI (you know, artificial intelligence) can take over the jobs of newbie programmers. Sure, AI's pretty cool - it can handle the boring stuff, make your code look sharp, and even cut down on all that manual testing. But, can it fully take over? Nah, not really. Beginner programmers bring their own flair to coding, thanks to their learning journeys and their fresh-out-of-the-box ideas. Plus, they're really good at picking up new tech and working with their team. So, while AI's a great sidekick in coding, it's not about to steal the limelight from our entry-level programmers anytime soon.