← Tất cả bài viết
Hướng dẫn

Tôi nhờ Claude Code xây một trình giám sát DM và mention trên X — hoàn toàn không đụng vào API của X — UnifyPort

API của X không có gói miễn phí. Đọc một tin nhắn DM tốn $0.015. Đọc một bài đăng tốn $0.005. Nếu bạn là đội hai người chỉ muốn biết khi có ai nhắn DM hoặc mention bạn trên X, nền tảng developer chính thức yêu cầu bạn nạp tiền, tạo project, chờ duyệt, bật tính phí theo lượt — trước khi xử lý được một sự kiện nào.

Tôi bỏ qua tất cả. Mở Claude Code, dán tài liệu webhook UnifyPort vào context, rồi yêu cầu nó xây một trình giám sát DM và mention X bằng Node.js. Bốn mươi phút sau, tôi có một server Express xác minh chữ ký mỗi lần giao nhận, ghi sự kiện vào file log có cấu trúc, và đẩy cảnh báo lên kênh Discord. Không cần tài khoản developer X, không phí theo lượt, không hàng đợi duyệt.

Bạn sẽ có được gì

Một server Node.js Express:

  1. Nhận sự kiện message.received từ webhook hợp nhất của UnifyPort
  2. Xác minh mỗi lần giao nhận bằng HMAC-SHA256 với signing_secret
  3. Ghi mỗi tin nhắn X vào file messages.jsonl
  4. Gửi cảnh báo tới kênh Discord qua webhook

Thời gian: dưới một giờ. Bạn cần một workspace UnifyPort đã kết nối tài khoản X qua tiện ích trình duyệt UnifyPort Exporter — nhập session một cú click, không cần thông tin developer — và một Discord webhook URL.

Chuẩn bị: đưa tài liệu vào context của Claude Code

Trước khi prompt, đưa tài liệu API vào context của agent. Claude Code đọc file trực tiếp, nên tạo unifyport-reference.md ở thư mục gốc project với:

  • Cấu trúc payload sự kiện message.received
  • Header x-unifyport-signature và hướng dẫn xác minh HMAC-SHA256
  • Lệnh tạo POST /v1/webhook-endpoints

Hoặc dán trực tiếp các phần đó vào cuộc trò chuyện. Điểm mấu chốt: tên field thật vào, code đúng ra. Không có tài liệu, agent sẽ tự bịa SDK riêng cho X hoặc kiểu sự kiện dm.received không tồn tại.

Cursor, Windsurf, Copilot đều hoạt động tương tự — dùng @Docs, bảng tài liệu, hoặc dán tài liệu vào context. Công cụ là phụ; tài liệu mới là chính.

Quá trình xây dựng, từng prompt một

Prompt đầu tiên — khung sườn:

Đọc unifyport-reference.md. Viết một Express server trong server.js với route POST /webhook. Khi event là “message.received”, in ra provider, from và text. Trả về 200 cho mọi request.

Claude Code tạo ra:

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhook", (req, res) => {
  const evt = req.body;
  if (evt.event === "message.received") {
    console.log(`[${evt.provider}] ${evt.from}: ${evt.text}`);
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

Sáu dòng handler. Vì cấu trúc sự kiện giống nhau trên cả sáu nền tảng, không cần phân tích riêng cho X — evt.provider hiển thị "x", nhưng cấu trúc giống hệt tin nhắn WhatsApp hay Telegram.

Prompt thứ hai — xác minh chữ ký:

Mỗi lần giao nhận có header x-unifyport-signature. Xác minh bằng HMAC-SHA256 với signing_secret từ biến môi trường. HMAC phải tính trên body request gốc — không phải JSON đã serialize lại. Trả 401 nếu thất bại. Dùng so sánh timing-safe.

Claude Code thêm lớp xác minh:

import crypto from "crypto";

const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

function isValid(req) {
  const sig = req.get("x-unifyport-signature") || "";
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(req.rawBody)
    .digest("hex");
  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Callback verify bắt buffer gốc trước khi Express parse JSON — nếu agent dùng req.body rồi serialize lại thành JSON, HMAC sẽ không bao giờ khớp. Vì prompt nói “body request gốc”, agent viết đúng ngay lần đầu.

Prompt thứ ba — logging và cảnh báo Discord:

Thêm hai tính năng: (1) ghi mỗi sự kiện message.received vào messages.jsonl — mỗi dòng một JSON object gồm timestamp, provider, from, text, và message_id. (2) Gửi một dòng cảnh báo tới Discord webhook URL từ biến môi trường DISCORD_WEBHOOK_URL.

File server.js hoàn chỉnh:

import express from "express";
import crypto from "crypto";
import { appendFileSync } from "fs";

const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const DISCORD_WEBHOOK = process.env.DISCORD_WEBHOOK_URL;

const app = express();
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

function isValid(req) {
  const sig = req.get("x-unifyport-signature") || "";
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(req.rawBody)
    .digest("hex");
  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

app.post("/webhook", async (req, res) => {
  if (!isValid(req)) return res.sendStatus(401);

  const evt = req.body;
  if (evt.event === "message.received") {
    const record = JSON.stringify({
      timestamp: evt.timestamp,
      provider: evt.provider,
      from: evt.from,
      text: evt.text,
      message_id: evt.message_id,
    });
    appendFileSync("messages.jsonl", record + "\n");

    if (DISCORD_WEBHOOK) {
      await fetch(DISCORD_WEBHOOK, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          content: `**[${evt.provider}]** ${evt.from}: ${evt.text}`,
        }),
      });
    }
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

Dưới 50 dòng. Xác minh chữ ký, log ra đĩa, chuyển tiếp Discord — tất cả sẵn sàng. Handler không phân nhánh theo provider vì cấu trúc sự kiện giống nhau, dù tin nhắn đến từ X, WhatsApp hay bất kỳ nền tảng nào.

Chạy thử và xem tin nhắn X đến

Kết nối tài khoản X trong bảng điều khiển UnifyPort. X dùng nhập session qua tiện ích trình duyệt UnifyPort Exporter — nhấp biểu tượng tiện ích, cấp quyền, tài khoản được liên kết trong vài giây. Không cần cổng developer, không cần nộp đơn xin API key, không hàng đợi duyệt.

Đăng ký webhook endpoint trỏ tới server của bạn với subscribed_events: ["message.received"]signing_secret. Khởi động server, rồi gửi một DM thử tới tài khoản X từ tài khoản khác. Sự kiện đến:

{
  "event": "message.received",
  "account_id": "acct_7Kp3mR",
  "provider": "x",
  "from": "user_9d4f2b",
  "text": "Hey, are you still taking freelance projects?",
  "timestamp": 1750521600,
  "message_id": "x_msg_8c1e5a"
}

Server xác minh chữ ký, ghi bản ghi vào messages.jsonl, và gửi cảnh báo tới Discord. Nếu xác minh chữ ký thất bại — 401 — kiểm tra UNIFYPORT_SIGNING_SECRET có khớp với giá trị đã đặt trên webhook endpoint không.

Mở rộng: thêm Telegram không cần sửa code

Kết nối tài khoản Telegram trong cùng workspace, đăng ký cùng webhook endpoint. Tin nhắn Telegram đến với cấu trúc y hệt — field provider khác, cấu trúc giống nhau hoàn toàn:

{
  "event": "message.received",
  "account_id": "acct_2Xn5wL",
  "provider": "telegram",
  "from": "user_4a7c8d",
  "text": "Can you hop on a call tomorrow?",
  "timestamp": 1750521660,
  "message_id": "tg_msg_3b9f1e"
}

Không cần handler mới. Không cần phân tích riêng cho Telegram. File messages.jsonl giờ chứa cả sự kiện X lẫn Telegram trong cùng schema, Discord hiển thị cảnh báo từ cả hai luồng. Thêm WhatsApp, Zalo, LINE hoặc TikTok sau này cũng cùng quy trình — kết nối tài khoản, handler hiện tại tự xử lý hết.

Khác biệt giữa cách này và dùng trực tiếp API X không chỉ là chi phí — mà là quyền truy cập. X yêu cầu tài khoản developer đã duyệt, project đã nạp tiền, và cam kết trả phí theo lượt. Con đường nhập session của UnifyPort kết nối tài khoản X cá nhân qua trình duyệt và chuẩn hóa mọi DM và mention thành cấu trúc sự kiện giống hệt năm kênh còn lại. Trỏ Claude Code vào bề mặt API đó — một schema sự kiện, một cơ chế chữ ký — và bạn sẽ có trình giám sát với xác minh, log, cảnh báo chạy trước bữa trưa. Sau đó thêm tất cả kênh khác mà không sửa handler.