Shop, autobuy and ticket bots can take Litecoin, USDT, Bitcoin and more with CoinsFlow. Your bot makes an invoice and posts a Pay button. A signed webhook tells it, within seconds, that a payment is on its way, and on Litecoin about 2.5 minutes later that it is paid. The buyer needs no account.
A shop, autobuy or ticket flow
- A buyer runs your
/buycommand or opens a ticket. Your bot makes an invoice withPOST /v1/invoices: the price in dollars, the coin, and the buyer's Discord ids inmetadata, which the buyer never sees. - The bot answers with a link button to the invoice's
paymentUrl. The payment page shows the address, the exact amount, a QR code and a countdown. - Seconds after the buyer sends Litecoin or Bitcoin,
invoice.detectedreaches your webhook: post “payment seen” in the ticket. Tick this event when you add the endpoint: it is not one of the defaults. - At the first confirmation, about 2.5 minutes on Litecoin,
invoice.paidarrives. Deliver the product or grant the role, once per invoice. - Withdraw to your own wallet when you like: by hand, automatically, or from the bot with
POST /v1/withdrawals.
A $20 Litecoin order, in numbers
| Price | $20.00, asked in LTC at the current price, rounded up to 6 decimals |
|---|---|
| Payment seen | seconds after the buyer sends it: invoice.detected |
| Paid | at 1 confirmation, about 2.5 minutes: invoice.paid |
| Our fee | $0.10 (0.5%), taken in LTC when it is credited |
| Buyer’s network fee | usually a fraction of a cent, paid by the buyer on top |
| In your balance | about $19.90 worth of LTC, withdrawable at once |
The /buy command
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js";
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "buy") return;
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const res = await fetch("https://api.coinsflow.app/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COINSFLOW_API_KEY}`,
"Idempotency-Key": interaction.id, // a retry never makes a second invoice
"Content-Type": "application/json",
},
body: JSON.stringify({
chain: "litecoin", asset: "LTC", priceAmount: "20.00",
description: "VIP role",
metadata: { userId: interaction.user.id, guildId: interaction.guildId, channelId: interaction.channelId },
}),
});
if (!res.ok) return interaction.editReply("Payments are not available right now. Try again in a minute.");
const invoice = await res.json();
const pay = new ButtonBuilder().setStyle(ButtonStyle.Link).setLabel("Pay in Litecoin").setURL(invoice.paymentUrl);
await interaction.editReply({
content: "Your invoice for the VIP role. The page shows the address, the amount and a QR code.",
components: [new ActionRowBuilder().addComponents(pay)],
});
});The amount follows the Litecoin price once an hour until it is paid, so the bot sends the link, not a number: the page always shows what is due. Every field is in the API docs.
The webhook: seen, then paid
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.COINSFLOW_WEBHOOK_SECRET; // whsec_...
const app = express();
function verified(header, body) {
try {
const parts = Object.fromEntries(String(header ?? "").split(",").map((p) => p.split("=", 2)));
const t = String(parts.t ?? "");
if (!/^\d{1,12}$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const given = Buffer.from(String(parts.v1 ?? ""), "hex");
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.`).update(body).digest();
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
} catch {
return false;
}
}
app.post("/webhooks/coinsflow", express.raw({ type: "application/json" }), async (req, res) => {
if (!Buffer.isBuffer(req.body) || !verified(req.get("CoinsFlow-Signature"), req.body)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
if (!event.type.startsWith("invoice.")) return res.sendStatus(200);
if (event.test && !process.env.TESTING) return res.sendStatus(200); // test events only while you test
const { id, metadata } = event.data;
if (!metadata?.channelId) return res.sendStatus(200); // not one of the bot's invoices
try {
const channel = await client.channels.fetch(metadata.channelId);
if (event.type === "invoice.detected") {
await channel.send(`<@${metadata.userId}> Payment seen. It counts at the first confirmation.`);
}
if (event.type === "invoice.paid" && !(await delivered(id))) { // your database: once per invoice
const guild = await client.guilds.fetch(metadata.guildId);
const member = await guild.members.fetch(metadata.userId);
await member.roles.add(process.env.VIP_ROLE_ID);
await markDelivered(id);
await channel.send(`<@${metadata.userId}> Paid. Your role is on.`);
}
res.sendStatus(200);
} catch {
res.sendStatus(500); // CoinsFlow sends it again later
}
});
app.listen(3000);Add the endpoint in the dashboard, under Developers, with invoice.detected and invoice.paid ticked. It must be an https:// address on the public internet. A bot that cannot take requests can ask GET /v1/invoices/{id} for the status instead. How the signature works is in the docs.
A middleman flow, run by your bot
If your bot holds a trade until both sides are done, your CoinsFlow balance can be where the money waits.
- Your bot makes an invoice for the trade, and the buyer pays it. The money is credited to your balance when the invoice is paid: on Litecoin, at the first confirmation.
- When the trade is done, your bot asks for a quote with
GET /v1/withdrawals/quote, then pays the seller withPOST /v1/withdrawals, with the quoted fee asmaxFeeand anIdempotency-Key. - The seller receives exactly the amount sent; the fee comes from your balance on top.
withdrawal.confirmedtells your bot the payout is confirmed on chain.
A buyer who holds the same coin in a CoinsFlow balance can pay from it with Pay with CoinsFlow: the invoice is paid and final at once, with no block to wait for.
This is your bot and your service: the money is in your balance, and your bot decides when it moves. Letting other sellers take payments through your bot as their own payment service needs our written agreement first (Terms, section 6).
Test it first
Make the invoice with "test": true, then press the buttons on its payment page, or call POST /v1/invoices/{id}/simulate. Your bot gets the same webhooks, marked "test": true, and no money moves. The webhook code above acts on them only while TESTING is set, so a test never grants a role on your live bot. Follow Discord's own rules for bots as well as ours.