The official reference implementation for integrating the Vetox Vito API into a Discord bot.
A small, production-quality Discord bot that runs the Vito API purchase flow end-to-end — entirely inside Discord: it shows a store, takes a payment, waits for the buyer to confirm with their PIN on vetox.io, detects completion, and delivers a product. Clone it, drop in two secrets, and watch a real payment flow work — then lift the integration into your own bot.
It's a demonstration, not a storefront. The products and license keys are illustrative, but the Vito API calls, webhook handling, and signature verification are exactly what you'd ship. Where to issue your real product is marked in the code (
completeOrderinsrc/index.js).
- Features
- How the purchase flow works
- Architecture
- Prerequisites
- Quick start
- Configuration
- Posting the store to a channel
- Customizing the store
- Completion detection: webhook vs polling
- Setting up webhooks
- Discord bot setup
- Production deployment
- Vito API reference
- Project structure
- Troubleshooting
- Security notes
- License
- 🛒 Full purchase flow —
/shop→ product picker → Buy → PIN confirmation → delivery, all in Discord. - 🔌 Complete Vito API integration — key verification,
POST /deduct, and transaction polling, in a tidy client module. - 📩 Signed webhooks — HMAC-SHA256 verification over the raw body, constant-time comparison, rotation-safe multi-signature support, and a replay-window (timestamp) check.
- 🛡️ Purchase safety — server-authoritative pricing, duplicate/concurrent-buy protection, per-product availability, and idempotent, exactly-once delivery.
- 💾 Durable orders — order state is persisted to disk with atomic writes, so deliveries survive restarts and a transaction is never fulfilled twice.
- 🔁 Two completion paths — instant webhooks and a zero-setup polling fallback, so it works out of the box.
- 🗂️ Categorised catalogue — products are grouped by category in the storefront, each with an availability flag.
- 🧾 Live audit log — every step is posted to an auto-created
#vito-demo-logchannel and the console. - ⚙️ Configurable without touching code — secrets via
.env, catalogue viaproducts.json. - 🪶 Minimal & readable — three runtime dependencies, small single-purpose modules, heavily commented for learning.
- ✅ Tested — a dependency-free
node --testsuite covers the catalogue, order store, webhook signatures, and UI helpers. - 🔐 Runs with just two secrets — a Discord bot token and a Vito API key.
- Display a shop —
/shopposts an embed with a product picker. - Initiate a purchase — clicking Buy calls
POST /public/vito/v1/deduct, creating a payment request (no Vito moves yet). - Payment-request DM — because the call includes your
guildId, the Vito API DMs the buyer from your server's own bot with the product details and a secure link. - PIN confirmation — the buyer enters their PIN on vetox.io (never inside Discord).
- Completion — the API debits the buyer, settles to the project owner, and fires a signed webhook / records the transaction.
- Delivery — the bot delivers a license key by DM and in the store reply, and logs every step.
sequenceDiagram
participant B as Buyer (Discord)
participant Bot as Vito Demo Bot
participant API as Vito API
participant Pay as vetox.io (PIN)
B->>Bot: /shop → pick product → Buy
Bot->>API: POST /deduct (discordId, amount, guildId, merchantRef)
API-->>Bot: { confirmationId, confirmUrl, expiresAt }
API-->>B: DM with secure confirm link (from your server's bot)
Bot-->>B: "Payment requested" + Complete Payment button
B->>Pay: Sign in, enter PIN
Pay->>API: Confirm
API-->>Bot: webhook confirmation.completed (signed) ⟍
API-->>Bot: GET /transactions shows it (poll) ⟋ either path
Bot-->>B: 🎉 License key (DM + store reply + log)
A thin Discord layer over a small Vito API client. Each module has one job:
| Module | Responsibility |
|---|---|
src/index.js |
Entry point: Discord client, /shop, the buy flow, completion detection, delivery, logging, boot & graceful shutdown. |
src/config.js |
Loads and validates environment configuration. |
src/vito.js |
Vito API client (auth, deduct, transactions) + webhook signature verification. |
src/webhook.js |
Express receiver for signed Vito webhooks. |
src/store.js |
Durable order lifecycle — file-backed (atomic writes), duplicate-buy guard, consumed-transaction set, and pruning. Single-instance; swap for a database if you run multiple instances. |
src/ui.js |
All Discord embeds/components + license-key generation. |
src/products.js |
Loads & validates the catalogue from products.json. |
Data flow: index.js orchestrates everything. It calls vito.js to talk to
the API, renders with ui.js, tracks orders in store.js, and receives webhooks
through webhook.js. Completion arrives via either the webhook handler or the
polling loop — both funnel into a single idempotent completeOrder.
- Node.js 18+ (uses the built-in
fetchandAbortController). - A Discord application + bot — create one at the Discord Developer Portal, then invite it to your server (see Discord bot setup).
- A Vito API key (
vito_live_…orvito_test_…) from the Vetox dashboard, with thededuct:createscope (and ideallytransactions:read). See account requirements.
# 1. Clone
git clone https://github.com/iRyoDev/vito-demo-bot.git
cd vito-demo-bot
# 2. Install
npm install
# 3. Configure — copy the template and fill in the two required values
cp .env.example .env
# VITO_API_KEY=vito_live_…
# DISCORD_BOT_TOKEN=…
# 4. Run
npm startYou should see a startup banner, /shop registered, and Vito API key: valid ✅.
Run /shop in your server and buy something to exercise the full flow.
Only
VITO_API_KEYandDISCORD_BOT_TOKENare required. With noVITO_GUILD_IDset,/shopis registered globally (it can take up to ~1h to appear the first time). SetVITO_GUILD_IDfor instant registration in your server.
All configuration is environment-based — copy .env.example to
.env and edit. Nothing in src/ needs changing to run.
| Variable | Description |
|---|---|
VITO_API_KEY |
Your Vito project key (vito_live_… / vito_test_…). |
DISCORD_BOT_TOKEN |
Token of the bot that hosts this store (must be in your server). |
| Variable | Description |
|---|---|
VITO_GUILD_ID |
Your server ID. Set it for instant /shop registration and as the originating guild for the payment-request DM. Leave blank to register globally and use each interaction's own guild. |
VITO_WEBHOOK_KEY |
Webhook signing secret (whsec_…). Enables HMAC verification on the webhook path. |
| Variable | Default | Description |
|---|---|---|
VITO_API_BASE_URL |
https://api.vetox.io |
Override only if you run your own Vito stack. |
PORT |
4000 |
Port for the local webhook receiver. |
PUBLIC_WEBHOOK_URL |
— | Public HTTPS URL of this receiver (display only, e.g. your tunnel URL). |
LOG_CHANNEL_ID |
— | Pin the log channel by ID. If unset, a #vito-demo-log channel is auto-created. |
VITO_DATA_FILE |
data/store.json |
Where the durable order store is written (atomic JSON writes). |
VITO_WEBHOOK_TOLERANCE_SEC |
300 |
Reject a signed webhook whose timestamp is older/newer than this (replay protection). 0 disables the check. |
SHOP_POST_ACCESS |
ADMINISTRATOR_ONLY |
Who may post the store into a channel — ADMINISTRATOR_ONLY or EVERYONE (see below). |
SHOP_POST_PREFIX |
!shop |
Trigger word for the text (prefix) post command. |
SHOP_POST_PREFIX_ENABLED |
true |
false disables the !shop prefix and drops the privileged Message Content intent (slash-only). |
Besides the /shop slash command (which replies to you), there's a second way to
drop the storefront into a channel as a normal message (sent via send, so it
persists and everyone can browse/buy from it). Two triggers, same behaviour:
!shop— a prefix text command (SHOP_POST_PREFIX, default!shop)./shop-post— a slash command.
Who can use it is controlled server-side by SHOP_POST_ACCESS:
| Value | Meaning |
|---|---|
ADMINISTRATOR_ONLY (default) |
Only members with the Administrator permission. |
EVERYONE |
Any member. |
⚠️ The!shopprefix requires the privileged "Message Content" intent. Enable it in the Discord Developer Portal → your App → Bot → Privileged Gateway Intents → Message Content, or the bot will fail to log in (it prints a clear hint if this happens). Don't want the privileged intent? SetSHOP_POST_PREFIX_ENABLED=false— the prefix is disabled, the intent is not requested, and the bot runs on non-privileged intents with only the/shop-postslash command (which never needs the intent).
The catalogue lives in products.json at the repo root — edit
it to change the storefront without touching any code. Each entry:
{
"id": "game-pro-1y",
"name": "GamePro License — 1 Year",
"description": "Unlock all premium features in GamePro for 12 months.",
"price": 25,
"type": "digital_key",
"category": "Apps & Software",
"available": true,
"imageUrl": "https://picsum.photos/seed/gamepro/400/240"
}| Field | Notes |
|---|---|
id |
Unique string. Duplicate ids are dropped with a warning. |
name / description |
Shown in embeds; also sent to the Vito API. name may contain Discord custom-emoji markup (e.g. <a:Nitro:123…>) — it renders in embeds, and the picker falls back to the emoji's name so labels never show raw markup. |
price |
Positive integer, in Vito. This is the only source of truth for what a buyer is charged — read server-side, never from the client. Keep it small so a test wallet can cover it. |
type |
Product type sent to the API (e.g. digital_key). |
category |
(optional) Grouping label for the storefront. Defaults to Other. |
available |
(optional) false hides the product from the picker and blocks purchase. Defaults to true. |
imageUrl |
Must be a direct https:// image URL — a page URL (e.g. an image-host landing page) won't render as a Discord thumbnail and the Vito API may reject it. |
Invalid entries are skipped with a warning; if the file is missing or empty, a built-in demo catalogue is used so the bot always runs.
The /shop banner is assets/vito-banner.png — replace that file to rebrand the
storefront (it's attached to the embed, so no image host is needed). If the file
is absent, the embed simply renders without a banner.
The bot confirms a purchase two ways, and the log tells you which one fired:
-
Polling (default, zero setup). Polls
GET /public/vito/v1/transactionsevery few seconds and delivers as soon as the deduction completes. Needs thetransactions:readscope; if it's missing, the bot logs a one-time notice. -
Webhook (instant, signature-verified). When you point your project's callback URL at this server, deliveries arrive immediately and are HMAC-verified.
Both routes converge on one idempotent delivery, so even if a webhook and a poll race, the key is delivered exactly once.
- Expose the bot over HTTPS — for local dev, use a tunnel:
ngrok http 4000 # or: cloudflared tunnel --url http://localhost:4000 - In the Vetox dashboard, set your project's callback URL to
https://<your-tunnel>/webhook/vito(callback URLs must behttps://). - Set
VITO_WEBHOOK_KEY(whsec_…) so the bot verifies theX-Vito-SignatureHMAC and labels deliveries "signature verified".
Full event list, payload fields, and the signature scheme are in
docs/VITO_API.md §5.
Invite the bot with the bot and applications.commands scopes.
| Permission | Why |
|---|---|
| Send Messages | Post the storefront and replies. |
| Attach Files | Attach the /shop banner image. |
| Manage Channels (optional) | Auto-create the #vito-demo-log channel. Without it, logging falls back to the console. |
Gateway intents — the core flow uses only the non-privileged Guilds intent.
The !shop prefix command additionally needs Guild Messages + the
privileged Message Content intent: enable Message Content in the
Developer Portal → your App →
Bot → Privileged Gateway Intents, or the bot won't log in. If you don't
want the privileged intent, use the /shop-post slash command instead (no extra
intent needed) — see Posting the store to a channel.
The buyer should share a server with the bot so the payment-request and license
DMs can reach them — the confirmUrl works regardless.
For a complete, command-by-command guide to running this as a real service —
process manager (pm2 / systemd) with auto-restart, exposing the webhook,
verifying the deployment, updating, backups & recovery, managing products
safely, and the digital-key delivery / fulfilment flow — see
docs/DEPLOYMENT.md.
Quick production install:
npm ci --omit=dev
npm test # optional; no secrets needed
npm i -g pm2
pm2 start src/index.js --name vito-store --time
pm2 save && pm2 startup # restart on bootFulfilment is a placeholder.
newLicenseKey()mints a random key, not a real product key. Wire your own fulfilment intocompleteOrder(src/index.js) — the single, idempotent delivery point — before selling real goods. Seedocs/DEPLOYMENT.md.
Every endpoint, webhook event, scope, signature detail, and Discord permission is
documented in docs/VITO_API.md — the canonical reference
for reproducing this integration. Endpoints used:
| Method | Path | Purpose |
|---|---|---|
GET |
/public/vito/v1/auth/verify |
Validate the API key (startup). |
POST |
/public/vito/v1/deduct |
Create a payment request. |
GET |
/public/vito/v1/transactions |
Poll for completion (fallback). |
Inbound: POST /webhook/vito receives confirmation.completed / .failed /
.cancelled / .expired events.
vito-demo-bot/
├─ src/
│ ├─ index.js # entry: Discord client, buy flow, completion, delivery, boot
│ ├─ config.js # env loading + validation
│ ├─ vito.js # Vito API client + webhook signature verification
│ ├─ webhook.js # Express webhook receiver
│ ├─ store.js # durable, file-backed order lifecycle + dedup + pruning
│ ├─ ui.js # Discord embeds/components + license-key generation
│ └─ products.js # catalogue loader/validator
├─ test/ # node --test suite (products, store, vito, ui)
├─ assets/ # storefront banner (vito-banner.png)
├─ docs/
│ ├─ VITO_API.md # endpoint, webhook, scope & permission reference
│ └─ DEPLOYMENT.md # production deploy, operations, backup & recovery guide
├─ data/ # persisted order store (gitignored, auto-created)
├─ products.json # editable product catalogue
├─ .env.example # configuration template
├─ package.json
└─ README.md
A dependency-free test suite runs on the built-in Node test runner:
npm test # node --testIt covers the catalogue loader/validation, the durable order store (duplicate-buy protection, persistence round-trip, pruning), webhook signature + replay-window verification, and the UI helpers (emoji-safe picker labels, availability). No Discord token or Vito key is needed — the suite exercises the pure logic paths.
| Symptom | Likely cause / fix |
|---|---|
Missing required config: … |
Set VITO_API_KEY and DISCORD_BOT_TOKEN in .env. |
Vito API key check failed |
Wrong/expired key, or it lacks access. Check the dashboard. |
/deduct rejected |
Missing deduct:create scope, TOS not accepted, inactive membership, insufficient balance, or a non-https product image. The exact reason is shown in Discord. |
/shop doesn't appear |
Global registration can take ~1h. Set VITO_GUILD_ID for instant registration. |
| Completion never detected | Add the transactions:read scope or configure a webhook tunnel. |
Webhook returns 401 |
The signature didn't verify or its timestamp is outside the replay window — confirm VITO_WEBHOOK_KEY matches the dashboard secret and the server clock is roughly correct (or raise/disable VITO_WEBHOOK_TOLERANCE_SEC). |
No #vito-demo-log channel |
Grant Manage Channels, or set LOG_CHANNEL_ID. Logs still go to the console. |
- Secrets stay in
.env(gitignored). Never commit.env; share only.env.example. - Webhooks are secure by default: with no
VITO_WEBHOOK_KEYset, inbound webhooks are ignored (acked but not acted on) so a forged delivery can't hand out a key — set the secret to enable the webhook completion path (polling still delivers meanwhile). When set, the HMAC is computed over the raw request body with a constant-time comparison, and deliveries outsideVITO_WEBHOOK_TOLERANCE_SECare rejected as replays. - Prices are server-authoritative — the client only ever sends a product id; the amount charged is read from
products.json, never from the client. - Use a stable idempotency key per purchase (this bot uses the order id) so retries can't double-charge. A duplicate/concurrent Buy for the same product reuses the in-flight order instead of opening a second charge.
- Orders are persisted to
data/store.jsonwith atomic writes, so deliveries survive a restart and a transaction is never fulfilled twice. This is a single-instance store — for multiple instances, move orders and the fulfilled-transaction set into a shared database with unique constraints.
MIT © Ryo (iRyoDev). The storefront, products, and license keys are illustrative; this project demonstrates the Vito payment flow and is not a real store.