Erlaube geprüften Apps, dein Vito mit PIN-Bestätigung zu belasten, und verwalte Zahlungsanfragen auf deiner Käufe-Seite.
Erfordert eine Silver-Mitgliedschaft oder höher — sowohl für die Bewerbung als auch für jeden authentifizierten Aufruf. Läuft die Mitgliedschaft des Schlüsselinhabers aus, wird das Projekt eingefroren, bis er sie erneuert.
Eine REST-API, mit der deine Anwendung das Vito-Guthaben eines Nutzers aus Discord heraus verwendet — lesen, belasten, gutschreiben oder zwischen Nutzern bewegen. Alle Endpunkte liefern JSON und sind unter /v1 versioniert.
Vito bewegt sich nie zu oder von echtem Geld. Es wandert ausschließlich zwischen Vetox-Guthaben.
Jeder Endpunkt liegt unter https://api.vetox.io/public/vito.Die auf dieser Seite genannten Pfade — /v1/deduct, /v1/balance/:discordId und die übrigen — sind relativ zu diesem Präfix, das dir das SDK selbst voranstellt. Rufst du einen direkt auf, nimm die vollständige URL:
https://api.vetox.io/public/vito/v1/deduct
https://api.vetox.io/v1/deduct ist keine Route und liefert 404.
Du baust mit Node.js? Baue REST-Aufrufe nicht selbst — nutze das offizielle Paket @vetox-bot/vito. Es deckt alle neun Endpunkte samt Webhook-Prüfung ab und übernimmt Idempotenz und Retries für dich. Siehe Offizielles Node.js-SDK weiter unten.
Dein Schlüssel und dein Signing Secret werden genau einmal angezeigt. Nach der Genehmigung hast du ein 7-Tage-Fenster, um sie im Tab „API Keys“ aufzudecken. Vetox speichert nur einen Hash und kann sie nicht erneut anzeigen — verpasst du das Fenster, musst du rotieren.
Halte ihn ausschließlich serverseitig — wer ihn besitzt, kann deine Nutzer belasten
Rotiere im Tab „API Keys“. Der vorherige Schlüssel funktioniert weitere 24 Stunden als Karenzzeit, damit du ohne Ausfall deployen kannst
Das Webhook-Signing-Secret rotiert separat, mit eigener 24-Stunden-Überlappung
Das offizielle Paket @vetox-bot/vito kapselt alle neun Endpunkte sowie die Webhook-Prüfung. Es übernimmt den Idempotency-Key-Header, Retries mit exponentiellem Backoff, Timeouts und die Klassifizierung von Fehlern für dich.
Erfordert Node.js 20 oder neuer. Das Paket hat keinerlei Laufzeitabhängigkeiten — es nutzt das eingebaute fetch und node:crypto — und liefert ESM und CommonJS zusammen mit vollständigen TypeScript-Definitionen.
import { VitoClient } from '@vetox-bot/vito';const vito = new VitoClient({ apiKey: process.env.VITO_API_KEY });
Lässt du apiKey weg, liest das SDK VITO_API_KEY aus der Umgebung. Das Schlüsselformat wird bereits beim Erzeugen geprüft, sodass ein fehlerhafter Schlüssel sofort scheitert, statt dich einen Roundtrip und ein 401 zu kosten.
Der Schlüssel bewegt Geld — halte ihn immer serverseitig, niemals in einem Client-Bundle oder im Browser. console.log(vito) gibt [redacted] statt des Schlüssels aus, und das SDK verweigert jede baseUrl mit http:// für einen nicht-lokalen Host, damit der Schlüssel nicht im Klartext übertragen wird.
Jede Methode liefert direkt das ausgepackte data-Feld — du entpackst success oder data nie selbst. Zusätzlich nimmt jede Methode Optionen pro Aufruf entgegen: { timeoutMs, maxRetries, signal, headers }, und die schreibenden Methoden zusätzlich { idempotencyKey }.
const wallet = await vito.balance.retrieve('123456789012345678');if (!wallet.hasPin) { // Noch keine Wallet-PIN, also kann keine Belastung bestätigt werden.}
Vermerke die Bestellung hier nur als ausstehend. Die Belastung ist noch nicht erfolgt — liefere erst beim Webhook confirmation.completed aus, nicht bei dieser Antwort.
const credit = await vito.payments.credit( { discordId: '123456789012345678', amount: 250, reason: 'Erstattung für order_10423', merchantRef: 'refund_10423', }, // Leitest du den Key aus deinem eigenen Datensatz ab, ist ein erneuter Lauf aus // einer Job-Queue sicher: eine Wiederholung liefert das Originalergebnis, // statt ein zweites Mal auszuzahlen. { idempotencyKey: 'refund_10423' },);console.log(credit.transactionId, credit.ownerBalance, credit.recipientBalance);
const since = Date.now() - 24 * 60 * 60 * 1000;// Ein async Generator, der Seiten bei Bedarf holt — keine manuelle Blätterschleife.for await (const tx of vito.transactions.iterate({ limit: 100 })) { if (tx.date < since) break; // neueste zuerst // tx.type ist die Richtung relativ zu tx.discordId: 1 = eingehend, 0 = ausgehend.}
Das SDK sendet bei jeder Schreiboperation (deduct, credit, transfer) einen Idempotency-Key-Header. Übergibst du keinen, erzeugt es ihn einmal pro Aufruf und sendet exakt denselben Key bei jedem Retry erneut, sodass ein Retry den Vorgang niemals zweimal buchen kann.Übergib einen eigenen Key, wenn derselbe logische Vorgang aus einem neuen Prozess wiederholt werden kann — Job-Runner, erneute Zustellung aus einer Queue oder ein geplanter Lauf:
Netzwerkfehler, Timeouts, 408, 429, 5xx und 409 VITO_IDEMPOTENCY_IN_PROGRESS
Wird nie wiederholt
409 VITO_IDEMPOTENCY_CONFLICT, alle übrigen 4xx und auth.rotateKey()
auth.rotateKey() ist bewusst ausgenommen — ein Retry erzeugt dort einen zweiten Schlüssel und entwertet den, den der erste Versuch zurückgegeben hat.
Ist Retry-After länger als maxRetryDelayMs (dein Stundenkontingent ist also wirklich aufgebraucht), wirft das SDK sofort VitoRateLimitError, statt dein Anfrage-Budget zu verschlafen.
import { Webhooks, VitoSignatureVerificationError } from '@vetox-bot/vito';try { const event = Webhooks.constructEvent({ payload: rawRequestBody, // der Raw Body — siehe Warnung unten signature: req.header('x-vito-signature') ?? '', secret: process.env.VITO_WEBHOOK_SECRET, }); if (event.eventType === 'confirmation.completed') { // Der Typ von data wird anhand von eventType automatisch eingegrenzt. await fulfil(event.data.merchantRef); }} catch (error) { if (error instanceof VitoSignatureVerificationError) { // Auslieferung mit 400 ablehnen — gefälscht, veraltet oder neu serialisiert. }}
constructEvent prüft das 5-Minuten-Replay-Fenster, vergleicht in konstanter Zeit gegen jedeh1-Signatur im Header — funktioniert damit automatisch während der 24-stündigen Rotationsüberlappung — parst anschließend die Payload und gibt das typisierte Event zurück.In einem Next.js-Route-Handler (App Router) nutzt du die Variante, die den Raw Body selbst liest:
Die Signatur deckt die rohen Bytes ab. Hänge in Express express.raw({ type: 'application/json' }) an die Webhook-Route — express.json() verbraucht den Body, und danach scheitert jede Prüfung. Rufe in Next.js nicht request.json() vor constructEventFromRequest auf.
Die Zustellung erfolgt mindestens einmal. Dedupliziere über event.eventId, bevor du irgendeinen Seiteneffekt ausführst.
Dein Schlüssel allein kann das Vito eines Nutzers nicht bewegen. Jede Belastung erfordert die Freigabe des Nutzers mit seiner Wallet-PIN, auf vetox.io — niemals in deiner App und niemals in Discord.
1
Deine App ruft POST /v1/deduct auf
Mit dem Nutzer, dem Betrag, der auslösenden guildId und den Artikeldetails.
2
Vito liefert eine confirmUrl
Eine ausstehende Bestätigung, 10 Minuten gültig. Der Nutzer erhält zusätzlich eine DM.
3
Der Nutzer bestätigt mit seiner PIN
Auf vetox.io.
4
Vito bucht und benachrichtigt
Das Guthaben wird belastet, die Transaktion gespeichert und ein signierter Webhook gesendet, sofern konfiguriert.
5
Deine App prüft und schließt ab
Signatur prüfen, dann den Inhalt freischalten oder den Artikel ausliefern.
Schließe deine Aktion ausschließlich bei confirmation.completed ab — niemals bei der Antwort von /deduct. Zu diesem Zeitpunkt ist die Belastung noch nicht endgültig.
POST /v1/add schreibt einem Nutzer Vito aus deinem eigenen Guthaben gut — für Belohnungen oder Erstattungen. Dieselben Felder wie /deduct, außer guildId und product.
Anders als eine Belastung hat eine Gutschrift keinen Bestätigungsschritt — sie wird sofort gebucht. Erfordert den Scope credit:create und ausreichendes Guthaben, sonst antwortet der Aufruf mit 402 VITO_INSUFFICIENT_OWNER_FUNDS.
Jede Belastung wird an dich ausgezahlt, abzüglich der Plattformgebühr — nach demselben Schema wie In-App-Vito-Transfers, basierend auf deiner Mitgliedschaftsstufe:
Deine Mitgliedschaft
Gebühr
Normal, Silver, Gold
7 %
Platinum
6 %
Diamond
5 %
Beträge von 5 Vito oder weniger sind gebührenfrei, und Gutschriften über /v1/add sind immer gebührenfrei.
Trage im Tab „Settings“ eine oder mehrere https-Callback-URLs ein. Vito sendet ein signiertes POST, sobald eine Bestätigung einen Endzustand erreicht.
Webhooks werden nur ausgeliefert, wenn dein Projekt sowohl eine Callback-URL als auch ein Signing Secret hat. Decke das Secret (whsec_…) einmalig im Tab „API Keys“ auf.
Jede Auslieferung trägt einen X-Vito-Signature-Header:
X-Vito-Signature: ts=<unix>;h1=<hex>
Zwei weitere Header begleiten jede Auslieferung — nutze X-Vito-Event-Id als Deduplizierungsschlüssel, denn ein Retry sendet dieselbe ID erneut:
Header
Enthält
X-Vito-Event-Id
Stabile ID für dieses Event — über Retries hinweg identisch
X-Vito-Event-Type
z. B. confirmation.completed
Während einer Signing-Secret-Rotation enthält der Header mehr als eine Signatur, die neueste zuerst:
X-Vito-Signature: ts=<unix>;h1=<neu>;h1=<alt>
Akzeptiere die Auslieferung, wenn irgendeinh1 passt. Ein Verifier, der nur den ersten liest, lehnt jeden Webhook ab, bis er das neue Secret ausgerollt hat — was den Sinn der 24-Stunden-Überlappung zunichtemacht.
Unter Node.js: Webhooks.constructEvent aus @vetox-bot/vito erledigt all das für dich — das Replay-Fenster, den Abgleich mit jederh1-Signatur und den Vergleich in konstanter Zeit — und liefert das typisierte Event zurück. Der Code unten ist für eine eigene Implementierung oder eine andere Sprache.
Berechne den HMAC über <ts>:<rawBody> mit deinem Signing Secret neu und vergleiche in konstanter Zeit.
const crypto = require('crypto');function verify(rawBody, header, secret) { const parts = header.split(';'); const ts = parts.find(p => p.startsWith('ts='))?.slice(3); const sigs = parts.filter(p => p.startsWith('h1=')).map(p => p.slice(3)); if (!ts || sigs.length === 0) return false; // Reject anything older than ~5 minutes — replay protection. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = Buffer.from( crypto.createHmac('sha256', secret).update(`${ts}:${rawBody}`).digest('hex'), ); // Any matching signature is valid — a rotation emits several. return sigs.some(sig => { const actual = Buffer.from(sig); // timingSafeEqual throws when the lengths differ, so check first. return ( actual.length === expected.length && crypto.timingSafeEqual(actual, expected) ); });}
Prüfe gegen den Raw Body, bevor JSON-Parsing oder Middleware ihn umschreibt.
Zusätzlich gilt ein Limit pro IP in Höhe der halben Minutenquote, mindestens jedoch 30.
Unter Last scheitern Schreib-Endpunkte geschlossen — eine Belastung wird abgelehnt, statt ein Double-Spend zu riskieren. Lesende Endpunkte scheitern offen. Behandle eine abgelehnte Schreiboperation als „nicht passiert“ und wiederhole sie.
Sende einen Idempotency-Key-Header, um Retries sicher zu deduplizieren.