Monetize, manage and sell roles within your Discord community.
- Architecture
- Prerequisites
- Configuration
- Deployment
- Database migrations
- Basic bot setup
- Payment providers
- Supported languages
- Running tests
- Commands reference
AiryPay follows Clean Architecture with a CQRS pattern via MediatR. The solution is split into six projects:
src/
├── AiryPay.Domain # Entities, value objects, repository interfaces — no external dependencies
├── AiryPay.Application # Use cases (MediatR handlers), validators, payment service abstractions
├── AiryPay.Infrastructure # EF Core repositories, payment provider implementations, RabbitMQ
├── AiryPay.Shared # AppSettings models, YAML config binding, shared utilities
├── AiryPay.Discord # Discord.Net bot, interaction modules, slash commands, localization
└── AiryPay.Web # ASP.NET Core webhook receiver for payment callbacks
tests/
├── AiryPay.Tests.Domain # Domain entity and value object unit tests
├── AiryPay.Tests.Application # Handler and validator unit tests (Moq + FluentAssertions)
├── AiryPay.Tests.Discord # Discord interaction module tests
├── AiryPay.Tests.Infrastructure # Repository and payment service integration tests
└── AiryPay.Tests.Architecture # Architecture constraint tests (NetArchTest)
Runtime services (Docker Compose):
| Service | Image | Purpose |
|---|---|---|
airypay.discord |
airypay.discord | Discord bot process |
airypay.web |
airypay.web | HTTP server for payment callbacks |
postgres |
postgres:17 | Primary database |
rabbitmq |
rabbitmq:3.13-management | Async messaging between Web and Discord |
Payment flow: a buyer initiates a payment → airypay.web receives the provider callback → publishes a message to RabbitMQ → airypay.discord consumes the message and assigns the Discord role.
- Docker and Docker Compose v2
- .NET 8 SDK (for local development and migrations)
- A Discord application with a bot token (Discord Developer Portal)
- At least one configured payment provider (see Payment providers)
Create appsettings.json in the project root using /src/AiryPay.Discord/appsettings.json as a template.
Create paymentsettings.yaml in the project root using /paymentsettings.samle.yaml as a template.
# Default commission (%) applied to new shops. Can be overridden per shop.
defaultShopCommission: 10.0
# Minimum amount (in your base currency) a shop owner can withdraw
minimalWithdrawalAmount: 500.0
# --- Payment provider credentials ---
# Only fill in the providers you intend to use.
# Unused providers can be left with empty strings.
ruKassaSettings:
merchantId: 0
token: ""
userEmail: ""
userPassword: ""
finPaySettings:
shopId: 0
key1: ""
key2: ""
stripeSettings:
apiKey: ""
successUrl: "https://yoursite.com/success"
cancelUrl: "https://yoursite.com/cancel"
squareSettings:
accessToken: ""
sandbox: true # Set to false in production
payPalSettings:
clientId: ""
secret: ""
sandbox: true # Set to false in production
# Payment methods shown to buyers in Discord
# methodId must match the provider's internal identifier
paymentMethods:
- serviceName: "Checkout"
methodId: "card"
discordEmoji: ":credit_card:"
name: "Bank card"
description: "Pay with Visa / Mastercard"
minimalSum: 100.00Create a .env file in the project root:
# Discord bot token from the Developer Portal
DISCORD_TOKEN=""
# PostgreSQL
POSTGRES_DB=""
POSTGRES_USER=""
POSTGRES_PASSWORD=""
POSTGRES_PUBLIC_PORT="5432" # Host port mapped to the container
# RabbitMQ
RABBITMQ_HOST="rabbitmq"
RABBITMQ_USER=""
RABBITMQ_PASSWORD=""
RABBITMQ_PORT="5672"
RABBITMQ_WEB_PORT="15672" # Management UI portWarning
Never commit .env, appsettings.json, or paymentsettings.yaml to version control. They contain secrets.
1. Prepare config files
Complete appsettings.json, paymentsettings.yaml, and .env as described above.
2. Build images
docker compose build3. Apply database migrations
Start only the database first, then run migrations from inside the Discord container:
docker compose up -d postgres
docker compose run --rm airypay.discord dotnet ef database update \
--project src/AiryPay.Infrastructure/AiryPay.Infrastructure.csproj \
--startup-project src/AiryPay.Discord/AiryPay.Discord.csproj \
--context AiryPay.Infrastructure.Data.ApplicationDbContextNote
Alternatively, exec into a running container: docker compose exec airypay.discord bash
4. Start all services
docker compose up -d5. Open firewall ports
In a production environment, ports 80 and 443 must be open to receive inbound payment callbacks from providers.
6. Verify
docker compose ps # All services should show "healthy" or "running"
docker compose logs -f # Tail combined logsUpdating to a new version:
git pull
docker compose build
docker compose up -dIf the new version includes database migrations, run step 3 again before step 4.
For running without Docker (development or debugging):
1. Start PostgreSQL and RabbitMQ locally (or point to remote instances via connection strings in environment variables).
2. Set environment variables in your shell or in launchSettings.json:
export DISCORD_TOKEN="your-token"
export POSTGRES_DB="airpay"
export POSTGRES_USER="postgres"
export POSTGRES_PASSWORD="secret"
export RABBITMQ_HOST="localhost"
export RABBITMQ_USER="guest"
export RABBITMQ_PASSWORD="guest"3. Apply migrations:
dotnet ef database update \
--project src/AiryPay.Infrastructure/AiryPay.Infrastructure.csproj \
--startup-project src/AiryPay.Discord/AiryPay.Discord.csproj \
--context AiryPay.Infrastructure.Data.ApplicationDbContext \
--configuration Debug4. Run both projects (in separate terminals):
dotnet run --project src/AiryPay.Discord/AiryPay.Discord.csproj
dotnet run --project src/AiryPay.Web/AiryPay.Web.csprojCreate a new migration after changing domain entities:
dotnet ef migrations add <MigrationName> \
--project src/AiryPay.Infrastructure/AiryPay.Infrastructure.csproj \
--startup-project src/AiryPay.Discord/AiryPay.Discord.csproj \
--context AiryPay.Infrastructure.Data.ApplicationDbContext \
--configuration Debug \
--verboseApply pending migrations:
dotnet ef database update \
--project src/AiryPay.Infrastructure/AiryPay.Infrastructure.csproj \
--startup-project src/AiryPay.Discord/AiryPay.Discord.csproj \
--context AiryPay.Infrastructure.Data.ApplicationDbContext \
--configuration Debug \
--verboseRoll back to a specific migration:
dotnet ef database update <MigrationName> \
--project src/AiryPay.Infrastructure/AiryPay.Infrastructure.csproj \
--startup-project src/AiryPay.Discord/AiryPay.Discord.csproj \
--context AiryPay.Infrastructure.Data.ApplicationDbContextList applied migrations:
dotnet ef migrations list \
--project src/AiryPay.Infrastructure/AiryPay.Infrastructure.csproj \
--startup-project src/AiryPay.Discord/AiryPay.Discord.csproj \
--context AiryPay.Infrastructure.Data.ApplicationDbContextImportant
Payment systems must be configured in paymentsettings.yaml before running the setup command.
- Invite the bot to your server with the
botandapplications.commandsscopes, and grant it the Manage Roles permission. - Run the
/setupslash command in your server. - Configure your shop name, language, and available payment methods.
- Add products via
/product create. - Share the generated purchase link with your community.
Note
The bot must have a role higher than the roles it assigns. Drag the bot's role above any roles it needs to grant in your server's role list.
| Provider | Region | Sandbox | Config key |
|---|---|---|---|
| RuKassa | Russia | No | ruKassaSettings |
| FinPay | Russia | No | finPaySettings |
| Stripe | Global | Yes | stripeSettings |
| Square | US / Global | Yes | squareSettings |
| PayPal | Global | Yes | payPalSettings |
Each provider must have a corresponding entry in paymentMethods in paymentsettings.yaml to appear in the Discord UI.
Webhook URLs (register these in your provider's dashboard):
| Provider | Callback URL |
|---|---|
| RuKassa | http://your-domain/api/rukassa |
| FinPay | http://your-domain/api/finpay |
| Stripe | http://your-domain/api/stripe |
| Square | http://your-domain/api/square |
| PayPal | http://your-domain/api/paypal |
Replace your-domain with your server's public IP or domain name.
Shop owners can set their storefront language independently. The following locales are supported:
| Code | Language |
|---|---|
en |
English |
ru |
Russian |
es |
Spanish |
pt |
Portuguese |
fr |
French |
de |
German |
To add a new language, add a .resx resource file under src/AiryPay.Discord/Localization/ and include the language code in BotSupportedLanguages in appsettings.json.
Run all test suites:
dotnet testRun a specific project:
dotnet test tests/AiryPay.Tests.Application/AiryPay.Tests.Application.csprojRun with code coverage:
dotnet test --collect:"XPlat Code Coverage"| Test project | Scope |
|---|---|
Tests.Domain |
Value objects, entity invariants |
Tests.Application |
MediatR handlers, FluentValidation rules |
Tests.Discord |
Interaction modules, embed builders |
Tests.Infrastructure |
Repositories, payment service adapters |
Tests.Architecture |
Dependency direction, naming rules |
| Command | Description |
|---|---|
/setup |
Initial shop configuration wizard |
/product create |
Add a new product to your shop |
/product edit |
Edit an existing product |
/product remove |
Remove a product |
/shop info |
View shop statistics and balance |
/shop settings |
Change shop language and settings |
/withdrawal create |
Request a balance withdrawal |
{ "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "System": "Warning" } }, "WriteTo": [ { "Name": "File", "Args": { "path": "./logs/log_.txt", "rollingInterval": "Day" } } ] }, // Default culture for the bot's own UI (not per-shop language) "Language": "en-US", // Languages available to shop owners for their storefronts // Supported values: "en", "ru", "es", "pt", "fr", "de" "BotSupportedLanguages": ["en", "ru", "es", "pt", "fr", "de"], "Kestrel": { // Whitelist of IPs allowed to POST payment callbacks to /api/... // Add your payment provider's IP ranges here "AllowedIPs": ["127.0.0.1", "::1"], "Endpoints": { "Http": { "Url": "http://*:80" } } }, "Links": { "SupportUrl": "https://discord.gg/your-invite", "TermsUrl": "https://yoursite.com/terms" }, "Discord": { // Set to true during development to restrict slash commands to one server "UseStagingServer": false, "StagingServerId": 0, // RGB color used for all embed messages "EmbedMessageColor": { "R": 40, "G": 120, "B": 230 }, // Tiered rate limiting: multiple rules are all enforced simultaneously "RateLimiters": [ { "Limit": 3, "Period": "1s", "BanPeriod": "1m" }, { "Limit": 220, "Period": "10m", "BanPeriod": "1h" }, { "Limit": 1000, "Period": "2h", "BanPeriod": "2d" } ] } }