nirva|deploy
2026-05-15·11 min read·NirvaDeploy team
#Bitcoin#Lightning#OpenNode#Thai-SaaS#payments#regulatory#MCP
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 ลูกค้าเหล่านี้ต้อง:

  1. แปลง BTC → THB ผ่าน Bitkub (fee ~0.25% + spread)
  2. โอนเข้าบัญชีธนาคาร (รอ 1-2 วัน + ค่าโอน)
  3. ผูกบัตรเครดิตหรือ PromptPay
  4. จ่ายให้เรา (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 พอดี

TierTHBLightning feeOn-chain feeStripe (3.65% + ฿11)
Hobby ฿1116,278 sat< ฿0.50~฿15-30฿15.05
Pro ฿33318,834 sat< ฿0.50~฿15-30฿23.16
Business ฿88850,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 ที่:

  1. ลูกค้าจ่าย BTC/Lightning → เข้า OpenNode wallet
  2. OpenNode แปลงเป็น THB ทันที (ใช้ Bitkub liquidity pool)
  3. OpenNode โอนเข้าบัญชีไทย (ปกติ 1 business day)
  4. เราเห็น 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 checkout
  • uri — BIP-21 (bitcoin:bc1...?lightning=lnbc...) for QR code
  • lightning_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 ที่หลายคนพลาด:

  1. Constant-time comparecrypto.timingSafeEqual กัน timing attack

(ไม่ใช่ === หรือ Buffer.compare)

  1. Length check ก่อนtimingSafeEqual throw ถ้าความยาวต่างกัน,

เราต้องเช็คเองก่อน

  1. try/catch รอบ Buffer.from(hex) — input ที่ไม่ใช่ valid hex จะ

throw RangeError, ต้อง catch แล้ว return false (อย่า leak error)

  1. Fail closed when secret missing — ถ้า OPENNODE_WEBHOOK_SECRET

ไม่ตั้ง ให้ webhook ตอบ 503 ทุก request (ห้ามผ่านเด็ดขาด)

  1. 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)

TierStripe feeLightning 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

rails เต็มที่

payment ที่ใช้ในไทยเหมือนกัน


ชอบบทความนี้?

ลอง NirvaDeploy ฟรี

Free tier ตลอดชีพ · ไม่ต้องใส่บัตร · Sign in ด้วย Line/Google

เริ่มเลย →