On this page
- Fianto does the billing (a monthly or yearly USDC price, renewals pulled from the member's wallet into yours) and your bot does Discord: add the role on subscription.created, remove it on subscription.ended.
- The Discord user ID travels in the checkout session as customer_reference and comes back on every subscription event, so the webhook knows whom to change.
- The bot's own role must sit above the paid role, or Discord refuses the change. You pay nothing; on plans created since 6 October 2026, members pay a flat 0.01 USDC network fee with each charge today.
What do you need to sell a Discord role for USDC?
You need four pieces: a Discord bot in your server with the Manage Roles permission, a recurring USDC price in your Fianto dashboard, a command that sends each member to a checkout, and a webhook route that changes the role. Nothing in Fianto talks to Discord. Fianto bills the member and sends signed events; the bot reads those events and calls Discord's API.
The link between the two is the member's Discord user ID, a long number that names one account on Discord. Your bot knows it the moment someone runs a command. It puts the ID in the checkout session's customer_ field (up to 128 characters), and Fianto returns it as data. on every subscription event, the renewals and the ending included.
A Fianto price renews every 30 days (monthly) or every 365 days (yearly). The member approves once from a Solana wallet, and each renewal is pulled from that wallet into yours without a new signature. If you want the background on the approval, read how stablecoin subscriptions renew on Solana.
Which way of charging for a Discord role should you pick?
Pick by what members pay with and how much of the bot you want to own. These are four common patterns as of 15 October 2026; of the four, only the last puts USDC into your own wallet on a schedule:
| Approach | What decides the role | Who runs the bot | Good fit when |
|---|---|---|---|
| A hosted paid-community service | A card subscription at that service | The service | Your members pay by card and you accept its fees |
| Token gating, for example Collab.Land | What tokens the member's wallet holds | The service | Access should follow holdings, not a payment |
| A Fianto shop with Zapier | Does not work for roles | Zapier | Never: a shop sale gives an email, and Discord needs a user ID |
| Your own bot plus Fianto webhooks | A USDC subscription to your price | You | You want USDC, monthly or yearly, paid into your wallet |
Token gating is a different idea from a subscription. Collab.Land's documentation defines it this way: "This refers to the ability to create roles on social platforms such as Discord and Telegram, that are only accessible to users who meet certain token-related criteria." Roles follow what a wallet holds, and nobody pays anyone each month in that model. Token-gating vs subscriptions compares the two for memberships.
The no-code route looks tempting and fails on one detail. Zapier's Discord actions "Add Role" and "Remove User Role" both need a user_ and a role_. A Fianto shop sends Zapier the buyer's email, the order id and the subscription id, never a Discord ID or username, so there is nothing to map them from. A bot of your own is the honest answer for a recurring role. What a shop does send to Zapier is listed in Zapier and Make automations for USDC sales.
How do you set up the bot and the role in Discord?
- Create an application in the Discord Developer Portal, add a bot user and copy its token. Keep the token on your server only.
- Invite the bot to your server with the Manage Roles permission. Discord's add and remove role endpoints both say they require MANAGE_ROLES.
- Create the paid role, give it access to the paid channels, and copy its role ID and your server (guild) ID.
- In Server Settings, drag the bot's own role above the paid role. This step is the one people miss.
- Register a /join slash command for the bot.
A bot can grant roles to other users that are of a lower position than its own highest role.
If the bot's role sits below the paid role, every add fails however correct your code is. Check the order again whenever someone rearranges roles.
How does a member get from /join to a USDC checkout?
The /join handler creates a Fianto checkout session in subscription mode and replies with its URL, visible only to that member. Discord's docs require an initial response within 3 seconds of receiving the interaction, so defer first with an ephemeral reply (the EPHEMERAL flag, value 64, makes a message only that user can see), then post the link as a follow-up. Interaction tokens are valid for 15 minutes, which covers the API call easily. In a server, the member who ran the command is in the interaction's member field.
import { Fianto } from '@fianto/sdk';
const fianto = new Fianto(); // reads FIANTO_APP_ID and FIANTO_APP_SECRET
const PRICE_ID = 'fian_price_...'; // your monthly or yearly price
// Call from the /join handler after deferring the reply as ephemeral.
export async function joinLink(discordUserId: string, guildId: string) {
const orderId = `dc_${discordUserId}_${Date.now()}`;
const session = await fianto.checkoutSessions.create(
{
mode: 'subscription',
order_id: orderId,
price_id: PRICE_ID,
customer_reference: discordUserId,
metadata: { guild_id: guildId },
success_url: 'https://example.com/joined',
cancel_url: 'https://example.com/join',
},
{ idempotencyKey: orderId },
);
return session.url; // returned only once: post it in the follow-up
}A fresh order_ per attempt matters. Once a subscription session has completed under an order id, Fianto refuses another under the same one with order_, and repeating an id while its session is still open returns that session with url: null. Check your own records before creating a session, so a member who already pays is told so instead of subscribing twice. The session stays open 30 minutes by default. After subscribing, the checkout page offers links to the customer portal and does not send the member to success_, so tell them in the follow-up message to come back to Discord.
Members approve once and renew every 30 or 365 days. You pay nothing, and there is no Fianto fee during the beta.
See memberships on FiantoWhich webhook events add and remove the Discord role?
Add the role on subscription., sent when the first period is paid and final on chain, and remove it on subscription.. A subscription creates no order, so order. never fires for it. Renewals need no Discord call at all: the role simply stays. Discord answers 204 on success for both the add (PUT) and the remove (DELETE).
import express from 'express';
import { webhooks } from '@fianto/express';
import { db } from './db'; // your own store: seen event ids, subscriptions per member, pending grants
const API = 'https://discord.com/api/v10';
const { DISCORD_BOT_TOKEN, GUILD_ID, PAID_ROLE_ID } = process.env;
async function setRole(userId: string, method: 'PUT' | 'DELETE') {
const res = await fetch(`${API}/guilds/${GUILD_ID}/members/${userId}/roles/${PAID_ROLE_ID}`, {
method,
headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}` },
});
if (res.status === 204) return true;
if (res.status === 404) return false; // not in the server (or a wrong id): keep it as pending
// 403 (role order), 429 or an outage: throw, the route answers 500 and Fianto retries.
throw new Error(`Discord answered ${res.status}`);
}
const app = express();
app.post(
'/webhooks/fianto',
webhooks({
secret: process.env.FIANTO_WEBHOOK_SECRET,
onSubscriptionCreated: async (event) => {
const userId = event.data.customer.reference;
if (userId === null || (await db.seen(event.id))) return;
await db.startSubscription(event.data.id, userId);
const granted = await setRole(userId, 'PUT');
await db.record(event.id, userId, granted ? 'granted' : 'pending'); // pending: retry when they rejoin
},
onSubscriptionEnded: async (event) => {
const userId = event.data.customer.reference;
if (userId === null || (await db.seen(event.id))) return;
await db.endSubscription(event.data.id);
// A newer subscription from the same member may already be paying: delivery is unordered.
if ((await db.hasActiveSubscription(userId)) === false) await setRole(userId, 'DELETE');
await db.record(event.id, userId, 'removed');
},
}),
);
app.use(express.json()); // parsers for other routes go after the Fianto route
app.listen(3000);Fianto signs each request with the Standard Webhooks scheme, and the SDK middleware checks the signature before your callback runs, so a stranger cannot post a fake "created" event and get a role. Delivery is at least once and in no fixed order, which is why the handler skips event ids it has seen and, on an ending, keeps the role if the member has a newer subscription. Mount the route before any body parser; verifying webhook signatures covers the details, and webhooks for access covers the access pattern in general. The same pattern for a Telegram group is in paid Telegram group for USDC.
What happens when a renewal fails or a member leaves the server?
A failed renewal does not end the subscription at once. The first counted failure, for example an empty wallet, sets it to past due and sends subscription.. Fianto tries four times in all, at the period boundary and then about one hour, six hours and one day after it; if the fourth attempt fails, the subscription ends and subscription. arrives with end_ set to PAYMENT_. Keeping the role during past due costs you at most a day of access and saves a member who topped up late from a round trip.
- The member cancels. Members cancel in the customer portal at fianto.xyz/my with one signature. The subscription runs to the end of the paid period, you get subscription.cancel_scheduled, and subscription.ended at the end, when the role goes.
- Removing the wallet approval is not a cancel. The next renewal fails as a counted failure, the subscription goes past due, and it ends after the fourth failed attempt.
- A member who left the server has no member record to change. Store the grant as pending and apply it when they rejoin, or the role goes missing.
- You can end a subscription yourself with POST /v1/subscriptions/{id}/cancel, at now or at period_end, for a member you ban.
A lifetime role instead of a monthly one
For a one-time role, sell a one-time price in a payment-mode session and add the role on order., which does carry the customer reference. If the whole server is paid and there are no free channels, you can skip roles altogether: create single-use invites (set max_ to 1, from Discord's range of 0 to 100 where 0 means unlimited, and max_ to 0 for never) and sell them from a Fianto shop as delivery codes, one per buyer. An invite grants membership, not a role, and it cannot be taken back when anything ends, so this only suits one-time access.
How much does a paid Discord role cost to run on Fianto?
You pay nothing, not even a Solana network fee, and the price you set is what lands in your wallet. There is no Fianto fee during the beta; after it, the member pays about 0.1 to 0.5% on top, and you still pay nothing. The member pays a flat 0.01 USDC network fee with each charge today, the first one included, on plans created since 6 October 2026, plus the Solana fee of the one transaction they sign, under $0.01.
The subscribe also pays Solana account rent in SOL: about 0.00144 SOL as of 15 October 2026 for the subscription account, returned when the member reclaims it after the subscription ends, and about 0.00119 SOL once for wallet setup on a wallet's first subscription, not returned. Solana is lowering rent in phases, and the checkout shows the exact live amount before anything is signed.
Payments start only after Fianto approves your merchant account, and there is no test mode. Run fianto trigger subscription. to post a signed local sample to the route and check that it verifies and reaches your handler. The CLI signs it with your FIANTO_WEBHOOK_SECRET (or --secret), and the sample's customer reference is the placeholder user_42, so expect Discord to refuse the role call itself. Then join your own server with a small real price. More in testing a USDC integration without a test mode, and the event reference is in the Fianto webhook docs.
- Discord: Add and Remove Guild Member Role (MANAGE_ROLES, 204 response)
- Discord: Permissions and role hierarchy
- Discord: Receiving and responding to interactions (3 seconds, 15-minute tokens, ephemeral flag)
- Discord: Create Channel Invite (max_uses, max_age)
- Discord: API reference (base URL, versions, bot Authorization header)
- Zapier: Discord integration actions
- Collab.Land: token-gated communities
- Fianto docs: webhook event types
- Fianto docs: subscriptions via the API