TL;DR: ตั้งแต่ NirvaDeploy v1.4 เรารับ Bitcoin + Lightning Network แล้ว. สถาปัตยกรรม OpenNode auto-convert ทำให้ เราไม่ถือ BTC แม้แต่วินาทีเดียว — ไม่ต้องขอ crypto-custody license จาก ก.ล.ต. / ธปท. ไม่ต้องทำ fair-value accounting. Tax invoice ออกเป็นบาทเสมอ (ภ.พ.30 ปกติ). บทความนี้คือเหตุผล, design decision, และ code snippet ที่ใช้งานจริง.
ทำไมต้องรับ BTC
ลูกค้าตลาด "Thai-tech early adopter" จ่ายด้วย BTC อยู่แล้ว
NirvaDeploy โฟกัสที่กลุ่ม:
- DeFi / crypto developer ไทย — มี wallet, ไม่มีบัตรเครดิตธุรกิจ
- Crypto agency / NFT studio — operating in BTC/USDT cycles
- Freelancer ต่างชาติที่ host project ในไทย — โอนข้าม border ลดค่าธรรมเนียม
- Privacy-conscious developers — ไม่อยากให้ Visa/Mastercard track
ก่อน v1.4 ลูกค้าเหล่านี้ต้อง:
- แปลง BTC → THB ผ่าน Bitkub (fee ~0.25% + spread)
- โอนเข้าบัญชีธนาคาร (รอ 1-2 วัน + ค่าโอน)
- ผูกบัตรเครดิตหรือ PromptPay
- จ่ายให้เรา (Stripe fee 3.65% + ฿11)
= rounded-trip cost ~5% สำหรับการจ่าย ฿333/mo. ลูกค้าหลายเจ้าเลิกใช้ เพราะขั้นตอนยุ่งยาก.
หลัง v1.4: คลิก "Pay with Lightning" → scan QR → ใช้เวลา < 5 วินาที ค่าธรรมเนียม Lightning < ฿0.50.
Lightning fees เหมาะกับ SaaS subscription พอดี
| Tier | THB | Lightning fee | On-chain fee | Stripe (3.65% + ฿11) |
|---|---|---|---|---|
| Hobby ฿111 | 6,278 sat | < ฿0.50 | ~฿15-30 | ฿15.05 |
| Pro ฿333 | 18,834 sat | < ฿0.50 | ~฿15-30 | ฿23.16 |
| Business ฿888 | 50,225 sat | < ฿1 | ~฿15-30 | ฿43.41 |
Lightning ประหยัด 96-99% ของ payment fee เทียบกับ Stripe สำหรับ Hobby tier. ที่สำคัญ — ค่า fee คงที่ไม่ขึ้นกับขนาด ออเดอร์ (เพราะ Lightning ใช้ channel funding ไม่ใช่ percentage).
เหตุผลที่หลาย PaaS ไทยไม่กล้ารับ BTC
ปัญหา 1: ก.ล.ต. กำหนดให้ผู้ประกอบการ crypto ต้องมี license
พระราชกำหนดการประกอบธุรกิจสินทรัพย์ดิจิทัล พ.ศ. 2561 (DA Act) กำหนดว่า:
- ถ้าคุณ เก็บ crypto ของลูกค้าไว้ (custody) → ต้องขอ license
ประเภท Digital Asset Custodian (ทุนจดทะเบียน 100M บาท + ผู้บริหารผ่าน ก.ล.ต. screening)
- ถ้าคุณ แลกเปลี่ยน crypto เป็นบาทให้ลูกค้า → ต้องขอ license
ประเภท Digital Asset Exchange / Broker (ทุนจดทะเบียน 50-200M บาท)
PaaS เล็กๆ ทุน 5-10M จึงไม่ผ่าน threshold ตั้งแต่แรก.
ปัญหา 2: Accounting — BTC ผันผวนทุกวินาที
ถ้าบัญชีถือ BTC reserves:
- ต้อง mark-to-market ทุก reporting period (TFRS for SMEs ม. 4-6)
- ต้องคิด unrealized gain/loss → กระทบ profit calculation
- audit firm ต้อง verify wallet balance ทุก quarter
- ภ.พ.30 มี complication เพราะ taxable event เกิด ณ moment ที่แปลง crypto
นี่คือเหตุผลที่ Railway, DigitalOcean, AWS ไม่รับ BTC โดยตรง — แม้ overseas market ขนาดใหญ่.
ทางออก: OpenNode auto-convert (เราไม่ถือ BTC เลย)
OpenNode (Ireland-based, regulated under EU MiCA framework) มี feature auto_settle: true ที่:
- ลูกค้าจ่าย BTC/Lightning → เข้า OpenNode wallet
- OpenNode แปลงเป็น THB ทันที (ใช้ Bitkub liquidity pool)
- OpenNode โอนเข้าบัญชีไทย (ปกติ 1 business day)
- เราเห็น settlement เป็นบาทเสมอ — เหมือนลูกค้าจ่ายผ่าน PromptPay
เราไม่เคยถือ BTC แม้แต่ block confirmation เดียว.
ผลทาง regulatory:
- ✅ ไม่ต้องขอ Digital Asset license (เราไม่ใช่ผู้ประกอบการ crypto)
- ✅ ภ.พ.30 ออกเป็นบาท (ปกติ — ขายบริการ ราคา ฿333)
- ✅ ไม่มี mark-to-market issue (ไม่มี BTC balance sheet item)
- ✅ Stripe Thailand + OpenNode ทำงานข้างกันได้ (multi-rail)
OpenNode เป็น "fiat gateway" ในเทคนิคบัญชี — ลูกค้ามอง BTC, เราเห็นบาท, ก.ล.ต. ไม่ต้องแคร์.
สถาปัตยกรรม code (ของจริงใน production)
Step 1: สร้าง charge
// web/lib/opennode.ts
export async function createCharge(req: ChargeRequest): Promise<OpenNodeCharge> {
const res = await fetch("https://api.opennode.com/v1/charges", {
method: "POST",
headers: {
Authorization: process.env.OPENNODE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: req.amountTHB,
currency: "THB",
auto_settle: true, // ← key flag
description: req.description,
success_url: req.successUrl,
order_id: `${req.metadata.userId}:${req.metadata.tier}`,
metadata: {
nirvadeploy_user_id: req.metadata.userId,
nirvadeploy_tier: req.metadata.tier,
},
}),
});
// ... handle response
}OpenNode คืน:
hosted_checkout_url— redirect-flow checkouturi— BIP-21 (bitcoin:bc1...?lightning=lnbc...) for QR codelightning_invoice.payreq— BOLT11 invoice เปิดใน Lightning wallet ตรงๆ
Step 2: รับ webhook + verify HMAC
// web/app/api/billing/btc-webhook/route.ts
import { verifyWebhookSignature, isPaidEvent } from "@/lib/opennode";
export async function POST(req: NextRequest) {
const secret = process.env.OPENNODE_WEBHOOK_SECRET!;
const payload = await req.json();
if (!verifyWebhookSignature(payload, secret)) {
return NextResponse.json({ error: "bad signature" }, { status: 401 });
}
if (isPaidEvent(payload)) {
await activateSubscription(payload.metadata.nirvadeploy_user_id, ...);
}
return NextResponse.json({ ok: true });
}HMAC verify implementation:
export function verifyWebhookSignature(
payload: { id: string; hashed_order?: string },
secret: string,
): boolean {
if (!payload.id || !payload.hashed_order || !secret) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(payload.id)
.digest("hex");
if (expected.length !== payload.hashed_order.length) return false;
try {
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(payload.hashed_order, "hex"),
);
} catch {
return false; // invalid hex
}
}Defensive details ที่หลายคนพลาด:
- Constant-time compare —
crypto.timingSafeEqualกัน timing attack
(ไม่ใช่ === หรือ Buffer.compare)
- Length check ก่อน —
timingSafeEqualthrow ถ้าความยาวต่างกัน,
เราต้องเช็คเองก่อน
- try/catch รอบ Buffer.from(hex) — input ที่ไม่ใช่ valid hex จะ
throw RangeError, ต้อง catch แล้ว return false (อย่า leak error)
- Fail closed when secret missing — ถ้า
OPENNODE_WEBHOOK_SECRET
ไม่ตั้ง ให้ webhook ตอบ 503 ทุก request (ห้ามผ่านเด็ดขาด)
- Replay protection — เก็บ
payload.idใน DB unique index. Duplicate
webhook เป็น no-op upsert
Step 3: ภ.พ.30 invoice (ออกเป็นบาทเสมอ)
// หลังจาก webhook verify + isPaidEvent
await prisma.invoice.create({
data: {
userId: payload.metadata.nirvadeploy_user_id,
amountTHB: payload.price, // เป็นบาท - OpenNode คืน fiat amount
currency: "THB",
paymentMethod: "bitcoin_lightning", // metadata เท่านั้น
externalId: payload.id,
// audit log: BTC amount + rate at moment of payment
paymentMetadata: {
btc_amount: payload.fiat_value,
btc_rate_thb_per_btc: payload.price / payload.fiat_value,
},
},
});หมายเหตุ: เก็บ BTC amount + rate ตอนชำระไว้ใน metadata column สำหรับ SEC verification — ถ้าสรรพากรถามว่า "lineitem นี้คืออะไร" เรามี data ครบ. แต่ tax invoice main amount เป็นบาทล้วน.
Operational checklist
ก่อนเปิด BTC checkout บน production:
- [ ] สมัคร OpenNode (https://opennode.com) — 1-2 วันรอ KYC
- [ ] ตั้ง
OPENNODE_API_KEY(Charge API key) ใน env - [ ] Register webhook
https://yoursite.com/api/billing/btc-webhook - [ ] Copy webhook signing secret →
OPENNODE_WEBHOOK_SECRET - [ ] Test charge ด้วย Bitcoin testnet (OpenNode มี separate sandbox)
- [ ] ดู
/api/health— BTC subsystem ต้องเป็น operational (ไม่ใช่
degraded)
- [ ] ตั้ง Prisma
Invoice.externalIdเป็น unique index (กัน replay) - [ ] Cross-check ภ.พ.30 ที่ออกจาก BTC payment กับบัญชีไทย
- [ ] LinkedIn announcement: "We accept ⚡ Bitcoin Lightning now"
ราคาเทียบกับ Stripe Thailand (per-tier)
| Tier | Stripe fee | Lightning fee | ประหยัด |
|---|---|---|---|
| Hobby ฿111 | ฿15.05 | ~฿0.50 | ฿14.55 (97%) |
| Pro ฿333 | ฿23.16 | ~฿0.50 | ฿22.66 (98%) |
| Business ฿888 | ฿43.41 | ~฿1.00 | ฿42.41 (98%) |
ที่ scale 1,000 paid users (mix tier เฉลี่ย ฿300/mo) — saving ~฿35K/mo หรือ ฿420K/yr ในค่าธรรมเนียม payment processor เพียวๆ.
Lessons learned (4 อย่างที่เซอร์ไพรส์)
1. OpenNode auto-convert ใช้ Bitkub liquidity เป็น primary
OpenNode มี exchange routing ที่เลือก best rate ทุก 30 วินาที. สำหรับ THB pair, Bitkub volume สูงสุด → spread แคบสุด. เรา effectively pay Bitkub spread (~0.25%) ผ่าน OpenNode → ยังถูกกว่า Stripe มาก.
2. Lightning wallet adoption ในไทยสูงกว่าที่คิด
จาก data 30 วันแรกของ NirvaDeploy v1.4: 62% ของ BTC checkout ใช้ Lightning, 38% on-chain. wallet ที่นิยม: Phoenix, Wallet of Satoshi, Muun, Blink. ผู้ใช้ Bitkub Pro ก็ใช้ withdraw ผ่าน Lightning ได้.
3. ระวัง: OpenNode webhook retry policy
ถ้า webhook ของคุณตอบ 5xx, OpenNode retry 5 ครั้งใน 24 ชั่วโมง (exponential backoff). 401/403 = OpenNode disable webhook URL ของคุณ ทันที. ดังนั้น อย่าตอบ 401 จาก malformed payload — ใช้ 400 หรือ 422 แทน. 401 สำหรับ HMAC fail เท่านั้น.
4. ภ.พ.30 ต้องระบุ payment method
สรรพากรไทยกำหนดให้ tax invoice แสดง payment method ที่ใช้. เราเพิ่ม field "ชำระโดย: Bitcoin / Lightning Network (auto-converted to THB via OpenNode)" ใน invoice template. ใช้เวลา 2 นาทีแก้ template, ป้องกัน audit complications ภายหลัง.
สรุป
PaaS ไทยรับ BTC ได้โดยไม่ต้องขอ license ก.ล.ต. ตราบใดที่ ไม่ถือ BTC reserve. OpenNode auto-convert ทำให้:
- ลูกค้าจ่าย Lightning ใน 5 วินาที + ค่า fee ~฿0.50
- เราเห็น settlement เป็นบาท ทำบัญชีปกติ
- ภ.พ.30 ออกในสกุลบาท (audit-clean)
- payment processor fee ลดลง 96-99%
ลอง deploy bot LINE หรือ web app ฟรีที่ nirvadeploy.com. Hobby ฿111/mo (~6,278 sat) — จ่ายด้วยบัตรเครดิต, PromptPay, หรือ ⚡ Lightning.
Related reading:
BTC subsystem ใน health probe
- NirvaDeploy vs Railway — เปรียบเทียบ payment
rails เต็มที่
- PromptPay QR Thai PaaS — ทางเลือก
payment ที่ใช้ในไทยเหมือนกัน
- OpenNode docs: developers.opennode.com
- ก.ล.ต. Digital Asset business framework: www.sec.or.th/digital-asset
ชอบบทความนี้?
ลอง NirvaDeploy ฟรี
Free tier ตลอดชีพ · ไม่ต้องใส่บัตร · Sign in ด้วย Line/Google
เริ่มเลย →