No Fianto fee during the beta. Any fee comes with at least 30 days notice. See pricing
Use casesDevelopers10 min read

Sell a paid Discord role for USDC with a small bot and two webhook events.

To sell a Discord role for USDC, run a bot that holds the Manage Roles permission, let a /join command open a Fianto subscription checkout with the member's Discord user ID as the customer reference, and add or remove the role when Fianto's subscription.created and subscription.ended webhooks arrive. Fianto has no Discord integration of its own: it collects the USDC into your wallet and tells your server what happened, and your bot does the Discord part.

On this page
What do you need to sell a Discord role for USDC?Which way of charging for a Discord role should you pick?How do you set up the bot and the role in Discord?How does a member get from /join to a USDC checkout?Which webhook events add and remove the Discord role?What happens when a renewal fails or a member leaves the server?How much does a paid Discord role cost to run on Fianto?SourcesQuestions
TL;DR
  • 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_reference field (up to 128 characters), and Fianto returns it as data.customer.reference 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:

ApproachWhat decides the roleWho runs the botGood fit when
A hosted paid-community serviceA card subscription at that serviceThe serviceYour members pay by card and you accept its fees
Token gating, for example Collab.LandWhat tokens the member's wallet holdsThe serviceAccess should follow holdings, not a payment
A Fianto shop with ZapierDoes not work for rolesZapierNever: a shop sale gives an email, and Discord needs a user ID
Your own bot plus Fianto webhooksA USDC subscription to your priceYouYou 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_id and a role_id. 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?

  1. Create an application in the Discord Developer Portal, add a bot user and copy its token. Keep the token on your server only.
  2. Invite the bot to your server with the Manage Roles permission. Discord's add and remove role endpoints both say they require MANAGE_ROLES.
  3. Create the paid role, give it access to the paid channels, and copy its role ID and your server (guild) ID.
  4. In Server Settings, drag the bot's own role above the paid role. This step is the one people miss.
  5. 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.

Discord developer documentation, Permissions

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.

bot/join.ts
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_id per attempt matters. Once a subscription session has completed under an order id, Fianto refuses another under the same one with order_id_in_use, 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_url, 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 Fianto

Which webhook events add and remove the Discord role?

Add the role on subscription.created, sent when the first period is paid and final on chain, and remove it on subscription.ended. A subscription creates no order, so order.paid 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).

bot/server.ts
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);
A webhook delivery log: subscription.created, then a renewal a period later, then subscription.past_due when a renewal failed, each delivered and signed.

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.past_due. 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.ended arrives with end_reason set to PAYMENT_FAILED. 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.paid, 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_uses to 1, from Discord's range of 0 to 100 where 0 means unlimited, and max_age 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.created --forward-to http://localhost:3000/webhooks/fianto 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.

Sources
Questions

Questions about paid Discord roles with USDC.

Does Fianto have a Discord bot?

No. Fianto bills the member in USDC and sends signed webhook events; a bot you run adds and removes the role from those events.

Why does my bot get an error when it adds the paid role?

Check the role order first. Discord lets a bot grant only roles below its own highest role, and the bot needs the Manage Roles permission.

Can I sell a Discord role with Zapier and no code?

Not with a Fianto shop. Zapier's Discord role actions need the member's Discord user ID, and a shop sale only provides the buyer's email, order id and subscription id.

When should the bot remove the role?

On subscription.ended. A past-due subscription is still being retried for about a day, so removing the role then is your choice, not a requirement.

Can members pay for a Discord role yearly?

Yes. A Fianto price can be monthly, renewing every 30 days, or yearly, renewing every 365 days; there are no weekly or custom periods.

Read next.

Use cases10 min read

A paid Telegram group or channel for USDC: join requests, webhooks and removal

A paid Telegram group on USDC works best with a join-request link: anyone can ask to join, your bot sends people who have not paid a Fianto checkout link, and it approves the request once Fianto's subscription.created webhook confirms the payment. When subscription.ended arrives, one Bot API call removes the member. Fianto collects the USDC into your wallet every 30 or 365 days; the Telegram side is a small bot you run.

Developers9 min read

Verify Standard Webhooks signatures for payment events

Verifying a webhook signature means recomputing an HMAC-SHA256 over the exact bytes you received and comparing it, in constant time, with the one in the request. Under the Standard Webhooks format that Fianto uses, the signed content is webhook-id.webhook-timestamp.body, the key is the base64 part of your whsec_ secret, and the result arrives as v1,<base64> in the webhook-signature header. Check the timestamp too, then dedupe on the id.

Product7 min read

Zapier and Make automations for USDC shop sales

A Fianto shop can send its sales to Zapier or Make with no code: paste a Zapier "Catch Hook" URL or a Make "Custom webhook" URL into Connect an app in your shop settings. Fianto then posts two events as flat JSON, purchase.paid when someone pays and subscription.ended when a subscription bought in the shop ends. Only shop sales are sent: payment link and API sales, renewals and failed renewals are not.

Give the role, let the events take it back.

Register /join, point the webhook at your bot, test it with fianto trigger.

New posts are announced on X and Discord.