MiningService - Service Documentation
1. Overview
Purpose: Pi Network-style point mining service. Users ("miners") earn Mining Points (MP) over time through 24-hour mining sessions. The service manages miner profiles, mining sessions with streak bonuses, security circles (trusted groups for bonus multipliers), and referral programs.
Port: 5006 (Development, via launchSettings.json) / 8080 (Docker/Production)
Database: PostgreSQL -- mining_service on Neon PostgreSQL (ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech)
Solution: MiningService.slnx
Target Framework: .NET 10.0, C# 14
Architecture: Clean Architecture + CQRS (MediatR)
Real-time: SignalR Hub at /hubs/mining for live mining updates
Auth: JWT Bearer (Duende IdentityServer), with SignalR token via query string
2. API Endpoints
MiningController (/api/v1/mining) -- [Authorize]
| Method |
Route |
Description |
Request |
Response |
| GET |
/api/v1/mining/me?userId={guid} |
Get current miner status |
Query: userId (Guid) |
MinerStatusDto or 404 |
| POST |
/api/v1/mining/start |
Start a new 24h mining session |
Body: StartMiningRequest { UserId } |
StartMiningResult |
| POST |
/api/v1/mining/claim |
Claim mining reward from completed session |
Body: ClaimMiningRequest { UserId } |
ClaimMiningRewardResult |
| GET |
/api/v1/mining/history?userId={guid}&page=1&pageSize=20 |
Get mining history (paginated) |
Query params |
MiningHistoryDto |
| GET |
/api/v1/mining/rate?userId={guid} |
Get current mining rate breakdown |
Query: userId (Guid) |
MiningRateDto or 404 |
| GET |
/api/v1/mining/leaderboard?limit=100 |
Get top miners leaderboard |
Query: limit (int) |
LeaderboardDto -- [AllowAnonymous] |
CirclesController (/api/v1/circles) -- [Authorize]
| Method |
Route |
Description |
Request |
Response |
| GET |
/api/v1/circles/me?userId={guid} |
Get user's circle |
Query: userId (Guid) |
CircleDto or 404 |
| POST |
/api/v1/circles |
Create a new security circle |
Body: CreateCircleRequest { UserId, Name } |
CreateCircleResult (201) |
| POST |
/api/v1/circles/invite |
Invite a member to circle |
Body: InviteMemberRequest { UserId, TargetMinerId } |
200 message |
| POST |
/api/v1/circles/accept/{inviteId}?userId={guid} |
Accept circle invitation |
Path: inviteId, Query: userId |
AcceptCircleInviteResult |
| DELETE |
/api/v1/circles/members/{memberId}?ownerId={guid} |
Remove member from circle |
Path: memberId, Query: ownerId |
RemoveCircleMemberResult |
| GET |
/api/v1/circles/trust-score?userId={guid} |
Get circle trust score |
Query: userId (Guid) |
CircleTrustScoreDto or 404 |
ReferralsController (/api/v1/referrals) -- [Authorize]
| Method |
Route |
Description |
Request |
Response |
| GET |
/api/v1/referrals?userId={guid} |
Get referral code and referrals list |
Query: userId (Guid) |
ReferralsDto |
| POST |
/api/v1/referrals/apply |
Apply a referral code |
Body: ApplyReferralRequest { UserId, ReferralCode } |
ApplyReferralResult |
| GET |
/api/v1/referrals/code?userId={guid} |
Get my referral code |
Query: userId (Guid) |
ReferralCodeDto or 404 |
| GET |
/api/v1/referrals/stats?userId={guid} |
Get referral statistics |
Query: userId (Guid) |
ReferralStatsDto or 404 |
AdminController (/api/v1/admin) -- [Authorize(Roles = "Admin")]
Configuration
| Method |
Route |
Description |
Request |
Response |
| GET |
/api/v1/admin/config |
Get all system configuration |
-- |
SystemConfigDto |
| PUT |
/api/v1/admin/config |
Update system configuration |
Body: UpdateSystemConfigCommand |
UpdateConfigResult |
| GET |
/api/v1/admin/config/mining |
Get mining configuration |
-- |
MiningConfigDto |
| PUT |
/api/v1/admin/config/mining |
Update mining configuration |
Body: UpdateMiningConfigCommand |
UpdateConfigResult |
| GET |
/api/v1/admin/config/streak |
Get streak configuration |
-- |
StreakConfigDto |
| PUT |
/api/v1/admin/config/streak |
Update streak configuration |
Body: UpdateStreakConfigCommand |
UpdateConfigResult |
| GET |
/api/v1/admin/config/referral |
Get referral configuration |
-- |
ReferralConfigDto |
| PUT |
/api/v1/admin/config/referral |
Update referral configuration |
Body: UpdateReferralConfigCommand |
UpdateConfigResult |
Miner Management
| Method |
Route |
Description |
Request |
Response |
| GET |
/api/v1/admin/miners?page=1&pageSize=20&search= |
List all miners (paginated) |
Query params |
MinersListDto |
| GET |
/api/v1/admin/miners/{id} |
Get miner details |
Path: id (Guid) |
MinerDetailsDto or 404 |
| PUT |
/api/v1/admin/miners/{minerId}/suspend |
Suspend a miner account |
Body: SuspendRequest { Reason } |
200 message |
| PUT |
/api/v1/admin/miners/{minerId}/ban |
Ban a miner account |
Body: BanRequest { Reason } |
BanMinerResult |
| PUT |
/api/v1/admin/miners/{minerId}/restore |
Restore a suspended miner |
-- |
200 message |
| PUT |
/api/v1/admin/miners/{minerId}/adjust-points |
Adjust miner points |
Body: AdjustPointsRequest { Amount, Reason } |
AdjustPointsResult |
| PUT |
/api/v1/admin/miners/{minerId}/reset-streak |
Reset miner streak |
Body: ResetStreakRequest { Reason } |
ResetStreakResult |
Analytics
| Method |
Route |
Description |
Response |
| GET |
/api/v1/admin/analytics/overview |
Admin dashboard overview |
AdminOverviewDto |
| GET |
/api/v1/admin/analytics/miners |
Miner analytics |
MinerAnalyticsDto |
| GET |
/api/v1/admin/analytics/circles |
Circle analytics |
CircleAnalyticsDto (stub) |
| GET |
/api/v1/admin/analytics/referrals |
Referral analytics |
ReferralAnalyticsDto (stub) |
| GET |
/api/v1/admin/analytics/points |
Points analytics |
PointsAnalyticsDto (stub -- no handler) |
| GET |
/api/v1/admin/analytics/streaks |
Streak analytics |
StreakAnalyticsDto (stub -- no handler) |
| GET |
/api/v1/admin/audit-logs?page=1&pageSize=50 |
Audit logs |
AuditLogsDto (stub -- no handler) |
Health Endpoints (no auth)
| Route |
Description |
/health |
Full health check (includes PostgreSQL) |
/health/live |
Liveness probe (app is running) |
/health/ready |
Readiness probe (ready for traffic) |
SignalR Hub
| Endpoint |
/hubs/mining |
| JoinMinerGroup(minerId) |
Subscribe to personal mining updates |
| LeaveMinerGroup(minerId) |
Unsubscribe from personal mining updates |
| JoinLeaderboardGroup() |
Subscribe to leaderboard updates |
Server-to-client messages via IMiningHubService:
PointsUpdated -- { earnedPoints, totalPoints, streakDays }
SessionStarted -- { endTime, hourlyRate }
StreakMilestone -- { streakDays, bonusPoints }
3. Commands
Core Commands
| Command |
Parameters |
Result |
Handler |
StartMiningCommand |
UserId (Guid) |
StartMiningResult { SessionId, HourlyRate, EndTime, StreakDays } |
Validates miner active, no existing session. Recalculates rate, creates MiningSession. |
ClaimMiningRewardCommand |
UserId (Guid) |
ClaimMiningRewardResult { PointsEarned, TotalPoints, StreakDays, StreakBonus } |
Validates session ready to claim. Calculates points, increments streak, adds milestone bonuses, creates MiningHistory entries. |
CreateCircleCommand |
UserId (Guid), Name (string) |
CreateCircleResult { CircleId, Name } |
Validates miner exists, not already in/owning a circle. Creates Circle with owner as first member. |
InviteToCircleCommand |
UserId (Guid), TargetMinerId (Guid) |
bool |
Validates inviter owns a circle, target not in a circle. Adds member, recalculates rates if circle becomes valid. |
ApplyReferralCodeCommand |
UserId (Guid), ReferralCode (string) |
ApplyReferralResult { ReferralId, ReferrerId, IsActive } |
Validates no existing referrer, valid code, not self-referral. Creates inactive Referral (pending KYC). |
Circle Management Commands
| Command |
Parameters |
Result |
Handler |
AcceptCircleInviteCommand |
UserId (Guid), InviteId (Guid) |
AcceptCircleInviteResult { Success, Message } |
Simplified: adds user as member to circle by InviteId. |
RemoveCircleMemberCommand |
OwnerId (Guid), MemberId (Guid) |
RemoveCircleMemberResult { Success, Message } |
Validates owner owns circle. Calls circle.RemoveMember(). |
Admin Commands
| Command |
Parameters |
Result |
Handler |
SuspendMinerCommand |
MinerId (Guid), Reason (string) |
bool |
Calls miner.Suspend(). |
RestoreMinerCommand |
MinerId (Guid) |
bool |
Calls miner.Restore(). |
BanMinerCommand |
MinerId (Guid), Reason (string) |
BanMinerResult { Success, Message } |
Calls miner.Suspend() (note: uses Suspend, not Ban). |
AdjustMinerPointsCommand |
MinerId (Guid), Amount (decimal), Reason (string) |
AdjustPointsResult { Success, NewBalance, Message } |
Calls miner.AddBonusPoints(). |
ResetMinerStreakCommand |
MinerId (Guid), Reason (string) |
ResetStreakResult { Success, Message } |
Stub -- saves but does not actually reset streak. |
UpdateSystemConfigCommand |
Mining?, Streak?, Referral? |
UpdateConfigResult { Success, Message } |
Stub -- returns success without persisting. |
UpdateMiningConfigCommand |
BaseRate?, SessionDurationHours?, MaxSessionsPerDay? |
UpdateConfigResult |
No dedicated handler (uses UpdateSystemConfigCommandHandler pattern). |
UpdateStreakConfigCommand |
Tiers?, GracePeriodDays? |
UpdateConfigResult |
No dedicated handler registered. |
UpdateReferralConfigCommand |
BonusPerReferral?, MaxBonusCap?, KycRequired? |
UpdateConfigResult |
Stub handler -- returns success without persisting. |
4. Queries
| Query |
Parameters |
Returns |
GetMinerStatusQuery |
UserId (Guid) |
MinerStatusDto? { MinerId, Role, TotalMinedPoints, HourlyRate, DailyRate, CurrentStreak, LongestStreak, StreakBonus, HasActiveSession, SessionEndTime, Status } |
GetMiningHistoryQuery |
UserId, Page, PageSize |
MiningHistoryDto { Items[], TotalCount, Page, PageSize } -- in-memory pagination from miner's histories |
GetMiningRateQuery |
UserId (Guid) |
MiningRateDto? { BaseRate, RoleMultiplier, CircleBonus, ReferralBonus, StreakBonus, TotalRate, HourlyPoints, DailyPoints } |
GetLeaderboardQuery |
Limit (int, default 100) |
LeaderboardDto { Entries[] } -- top miners by TotalMinedPoints |
GetCircleQuery |
UserId (Guid) |
CircleDto? { CircleId, Name, OwnerId, MemberCount, TrustScore, BonusMultiplier, IsValid, Status, Members[] } |
GetCircleTrustScoreQuery |
UserId (Guid) |
CircleTrustScoreDto? { CircleId, Name, MemberCount, TrustScore, IsValid, BonusMultiplier } |
GetReferralsQuery |
UserId (Guid) |
ReferralsDto { MyReferralCode, TotalReferrals, ActiveReferrals, TotalBonusPercent, Referrals[] } |
GetReferralCodeQuery |
UserId (Guid) |
ReferralCodeDto? { ReferralCode, ShareUrl } -- URL: https://goodgo.app/invite/{code} |
GetReferralStatsQuery |
UserId (Guid) |
ReferralStatsDto? { ReferralCode, TotalReferrals, ActiveReferrals, PendingReferrals, TotalEarned, CurrentBonusRate } |
GetSystemConfigQuery |
-- |
SystemConfigDto { Mining, Streak, Referral } -- loads from in-memory ConfigurationRepository |
GetReferralConfigQuery |
-- |
ReferralConfigDto { BonusPerReferral, MaxBonusCap, KycRequired } |
GetAdminOverviewQuery |
-- |
AdminOverviewDto { TotalMiners, ActiveMiners, MinersWithActiveSession, TotalPointsMined, TotalCircles, ValidCircles, TotalReferrals, ActiveReferrals } -- uses DbContext directly |
GetMinersListQuery |
Page, PageSize, Search? |
MinersListDto { Items[], TotalCount, Page, PageSize } |
GetMinerDetailsQuery |
MinerId (Guid) |
MinerDetailsDto? -- full miner detail including rate, streak, circle |
GetMinerAnalyticsQuery |
-- |
MinerAnalyticsDto { TotalMiners, ActiveMiners, SuspendedMiners, TotalPointsMined, PointsMinedToday, RoleDistribution } |
GetCircleAnalyticsQuery |
-- |
CircleAnalyticsDto -- stub, returns zeros |
GetReferralAnalyticsQuery |
-- |
ReferralAnalyticsDto -- stub, returns zeros |
GetPointsAnalyticsQuery |
-- |
PointsAnalyticsDto -- no handler registered |
GetStreakAnalyticsQuery |
-- |
StreakAnalyticsDto -- no handler registered |
GetAuditLogsQuery |
Page, PageSize |
AuditLogsDto -- no handler registered |
5. Domain Model
Aggregates
Miner (Aggregate Root)
- File:
MiningService.Domain/AggregatesModel/MinerAggregate/Miner.cs
- Properties:
UserId, Role (MinerRole), TotalMinedPoints, CurrentRate (MiningRate, owned), ActiveSession (MiningSession?, owned), Streak (MiningStreak, owned), ReferralCode, ReferredById?, CircleId?, Status (MinerStatus), CreatedAt, UpdatedAt, RowVersion (concurrency token), MiningHistories (collection)
- Factory:
Miner.Create(userId, referralCode?, referredById?) -- generates 8-char alphanumeric referral code, raises MinerCreatedDomainEvent
- Behavior Methods:
StartMiningSession(baseRate, sessionHours) -- validates active status, no existing session; recalculates rate; creates session; raises MiningSessionStartedDomainEvent
ClaimMiningReward() -- validates session ready; calculates points + streak milestone bonuses; increments streak; creates MiningHistory entries; raises PointsMinedDomainEvent
RecalculateRate(baseRate, circleBonus, referralBonus) -- computes TotalRate formula
UpgradeRole(newRole) -- only allows upgrades
JoinCircle(circleId) -- auto-upgrades Pioneer to Contributor
LeaveCircle() -- auto-downgrades Contributor to Pioneer
Suspend(), Restore(), Ban() -- status transitions
AddBonusPoints(points, source), DeductPoints(points, reason) -- with history tracking
Milestone Bonuses (on streak claim):
| Streak Day |
Bonus MP |
| 7 |
50 |
| 14 |
100 |
| 30 |
300 |
| 60 |
500 |
| 90 |
1000 |
Circle (Aggregate Root)
- File:
MiningService.Domain/AggregatesModel/CircleAggregate/Circle.cs
- Properties:
OwnerId, Name, TrustScore (0-100), BonusMultiplier, Status (CircleStatus), CreatedAt, UpdatedAt, Members (collection of CircleMember)
- Constants:
MinMembers = 3, MaxMembers = 5, ValidCircleBonus = 0.25m (25%)
- Computed:
ActiveMemberCount, IsValid (Active status + 3+ active members)
- Factory:
Circle.Create(ownerId, name) -- owner auto-added as first member, status = Incomplete
- Behavior Methods:
AddMember(minerId) -- validates not disbanded, max 5, not duplicate; recalculates status; raises CircleCompletedDomainEvent when reaching 3 members
RemoveMember(minerId) -- cannot remove owner; deactivates member
Disband() -- sets Disbanded, zeroes bonus, deactivates all members
- Trust Score: 3 members = 60, 4 = 80, 5 = 100
CircleMember (Entity)
- File:
MiningService.Domain/AggregatesModel/CircleAggregate/CircleMember.cs
- Properties:
CircleId, MinerId, JoinedAt, IsActive
- Methods:
Deactivate(), Activate()
Referral (Aggregate Root)
- File:
MiningService.Domain/AggregatesModel/ReferralAggregate/Referral.cs
- Properties:
ReferrerId, ReferredId, ReferralCode, BonusRate (default 0.25 = 25%), IsActive, Level (default 1), CreatedAt, ActivatedAt?
- Factory:
Referral.Create(referrerId, referredId, referralCode, bonusRate, level) -- validates no self-referral, starts inactive
- Behavior Methods:
Activate() -- sets active + timestamp; raises ReferralActivatedDomainEvent
Deactivate() -- sets inactive
CalculateBonus(baseRate) -- returns baseRate * BonusRate if active, else 0
MiningHistory (Entity, child of Miner)
- File:
MiningService.Domain/AggregatesModel/MinerAggregate/MiningHistory.cs
- Properties:
MinerId, PointsEarned, Source, SessionId?, EarnedAt, HourlyRateSnapshot, StreakDaySnapshot
- Factories:
CreateFromSession(...), CreateFromBonus(...)
Configuration Aggregates (in-memory, not persisted to DB)
MiningConfiguration
- Properties:
BaseRate (0.25), SessionDurationHours (24), MaxSessionsPerDay (1), IsGloballyEnabled (true), UpdatedAt, UpdatedBy
StreakConfiguration
- Properties:
Tiers (collection of StreakTier), GracePeriodEnabled (true), GracePeriodHours (24), RecoveryCost (50 MP), FreezeTokenDays (7), UpdatedAt, UpdatedBy
- Default Tiers:
| MinDays |
MaxDays |
BonusPercent |
BadgeName |
MilestoneMpBonus |
| 1 |
2 |
0% |
-- |
0 |
| 3 |
6 |
10% |
3-day badge |
0 |
| 7 |
13 |
25% |
7-day badge |
50 |
| 14 |
29 |
50% |
14-day badge |
100 |
| 30 |
59 |
100% |
30-day badge |
300 |
| 60 |
89 |
125% |
60-day badge |
500 |
| 90 |
MAX |
150% |
90-day badge |
1000 |
ReferralConfiguration
- Properties:
BonusPercentPerReferral (0.25), MaxBonusPercent (1.0 = 100%), KycRequired (true), MaxReferralLevels (1), UpdatedAt, UpdatedBy
Value Objects
MiningRate (record)
BaseRate, RoleBonus, CircleBonus, ReferralBonus, StreakBonus
TotalRate = BaseRate * (1 + Role) * (1 + Circle) * (1 + Referral) * (1 + Streak)
DailyRate = TotalRate * 24
- Default: BaseRate = 0.25 MP/hour
MiningStreak (record)
CurrentStreak, LongestStreak, LastMiningDate, FreezeTokens, IsGracePeriod
BonusMultiplier: 0% (days 1-2), 10% (3-6), 25% (7-13), 50% (14-29), 100% (30-59), 125% (60-89), 150% (90+)
IncrementStreak() -- earns 1 freeze token per 7 days
Reset() -- zeroes CurrentStreak
MiningSession (record)
SessionId, StartTime, EndTime, HourlyRate, Status (MiningSessionStatus), AccumulatedPoints
IsReadyToClaim -- Active status and EndTime passed
CalculateEarnedPoints() -- elapsed hours * HourlyRate, capped at 24h
MarkAsClaimed(earnedPoints) -- sets Claimed status
Enumerations
MinerRole
| Value |
Name |
Bonus |
| 0 |
Pioneer |
0% (base user) |
| 1 |
Contributor |
+10% (has valid circle) |
| 2 |
Ambassador |
+25% (5+ referrals) |
| 3 |
NodeOperator |
+50% (runs node software) |
MinerStatus
| Value |
Name |
| 0 |
Active |
| 1 |
Suspended |
| 2 |
Banned |
MiningSessionStatus
| Value |
Name |
| 0 |
Active |
| 1 |
Completed |
| 2 |
Claimed |
| 3 |
Expired |
CircleStatus
| Value |
Name |
| 0 |
Incomplete (< 3 members) |
| 1 |
Active (3-5 members, valid for bonus) |
| 2 |
Disbanded |
Domain Exceptions
MiningDomainException -- base exception
MinerNotFoundException -- miner not found by ID or UserId
CircleDomainException -- circle operation failures
ReferralDomainException -- referral operation failures
6. Database Schema
Database: mining_service (PostgreSQL)
Migration: 20260117103924_InitialCreate
Table: Miners
| Column |
Type |
Constraints |
Id |
uuid |
PK |
UserId |
uuid |
NOT NULL, UNIQUE (IX_Miners_UserId) |
Role |
varchar(20) |
NOT NULL, string conversion of MinerRole enum |
TotalMinedPoints |
numeric(18,4) |
NOT NULL |
CurrentRate_BaseRate |
numeric(18,4) |
NOT NULL (owned type) |
CurrentRate_RoleBonus |
numeric(5,4) |
NOT NULL (owned type) |
CurrentRate_CircleBonus |
numeric(5,4) |
NOT NULL (owned type) |
CurrentRate_ReferralBonus |
numeric(5,4) |
NOT NULL (owned type) |
CurrentRate_StreakBonus |
numeric(5,4) |
NOT NULL (owned type) |
ActiveSession_SessionId |
uuid |
NULLABLE (owned type) |
ActiveSession_StartTime |
timestamp with time zone |
NULLABLE |
ActiveSession_EndTime |
timestamp with time zone |
NULLABLE |
ActiveSession_HourlyRate |
numeric(18,4) |
NULLABLE |
ActiveSession_Status |
varchar(20) |
NULLABLE |
ActiveSession_AccumulatedPoints |
numeric(18,4) |
NULLABLE |
Streak_CurrentStreak |
integer |
NOT NULL (owned type) |
Streak_LongestStreak |
integer |
NOT NULL |
Streak_LastMiningDate |
timestamp with time zone |
NOT NULL |
Streak_FreezeTokens |
integer |
NOT NULL |
Streak_IsGracePeriod |
boolean |
NOT NULL |
ReferralCode |
varchar(10) |
NOT NULL, UNIQUE (IX_Miners_ReferralCode) |
ReferredById |
uuid |
NULLABLE |
CircleId |
uuid |
NULLABLE |
Status |
varchar(20) |
NOT NULL |
CreatedAt |
timestamp with time zone |
NOT NULL |
UpdatedAt |
timestamp with time zone |
NOT NULL |
RowVersion |
bytea |
NOT NULL, row version (concurrency token) |
Indexes: IX_Miners_UserId (unique), IX_Miners_ReferralCode (unique)
Table: MiningHistories
| Column |
Type |
Constraints |
Id |
uuid |
PK |
MinerId |
uuid |
NOT NULL, FK -> Miners (CASCADE) |
PointsEarned |
numeric(18,4) |
NOT NULL |
Source |
varchar(50) |
NOT NULL |
SessionId |
uuid |
NULLABLE |
EarnedAt |
timestamp with time zone |
NOT NULL |
HourlyRateSnapshot |
numeric(18,4) |
NOT NULL |
StreakDaySnapshot |
integer |
NOT NULL |
Indexes: IX_MiningHistories_MinerId_EarnedAt (composite)
Table: Circles
| Column |
Type |
Constraints |
Id |
uuid |
PK |
OwnerId |
uuid |
NOT NULL |
Name |
varchar(100) |
NOT NULL |
TrustScore |
numeric(5,2) |
NOT NULL |
BonusMultiplier |
numeric(5,4) |
NOT NULL |
Status |
varchar(20) |
NOT NULL |
CreatedAt |
timestamp with time zone |
NOT NULL |
UpdatedAt |
timestamp with time zone |
NOT NULL |
Indexes: IX_Circles_OwnerId
Table: CircleMembers
| Column |
Type |
Constraints |
Id |
uuid |
PK |
CircleId |
uuid |
NOT NULL, FK -> Circles (CASCADE) |
MinerId |
uuid |
NOT NULL |
JoinedAt |
timestamp with time zone |
NOT NULL |
IsActive |
boolean |
NOT NULL |
Indexes: IX_CircleMembers_CircleId_MinerId (unique composite), IX_CircleMembers_MinerId
Table: Referrals
| Column |
Type |
Constraints |
Id |
uuid |
PK |
ReferrerId |
uuid |
NOT NULL |
ReferredId |
uuid |
NOT NULL, UNIQUE (IX_Referrals_ReferredId) |
ReferralCode |
varchar(10) |
NOT NULL |
BonusRate |
numeric(5,4) |
NOT NULL |
IsActive |
boolean |
NOT NULL |
Level |
integer |
NOT NULL |
CreatedAt |
timestamp with time zone |
NOT NULL |
ActivatedAt |
timestamp with time zone |
NULLABLE |
Indexes: IX_Referrals_ReferrerId, IX_Referrals_ReferredId (unique)
Note: Table names use PascalCase (e.g., Miners, MiningHistories), not the platform-standard snake_case. Value objects (MiningRate, MiningStreak, MiningSession) are stored as owned types with {OwnerProperty}_{ValueProperty} column naming.
7. Integration Events
Published Events (outbound)
| Event |
Fields |
Description |
PointsMinedIntegrationEvent |
EventId, OccurredOn, UserId, MinerId, Points, Source, StreakDays |
Points mined, should be credited to wallet |
ReferralActivatedIntegrationEvent |
EventId, OccurredOn, ReferrerId, ReferredUserId, BonusRate |
Referral activated after KYC |
CircleCompletedIntegrationEvent |
EventId, OccurredOn, CircleId, OwnerId, MemberCount, BonusMultiplier |
Security circle completed (3+ members) |
Consumed Events (inbound)
| Event |
Handler |
Description |
UserRegisteredIntegrationEvent |
UserRegisteredIntegrationEventHandler |
Creates new Miner profile when user registers in IAM. If referral code provided, creates Referral relationship. |
UserKycCompletedIntegrationEvent |
UserKycCompletedIntegrationEventHandler |
Activates pending referral for user who completed KYC verification. |
Note: Integration events are defined as records implementing IIntegrationEvent. Handlers use MediatR notification wrappers (UserRegisteredNotification, UserKycCompletedNotification). The actual message broker (RabbitMQ) integration for publishing/consuming is not yet wired up -- the event definitions and handlers exist but the transport layer is missing.
Domain Events
| Event |
Fields |
Raised By |
MinerCreatedDomainEvent |
MinerId, UserId |
Miner.Create() |
MiningSessionStartedDomainEvent |
MinerId, SessionId, HourlyRate |
Miner.StartMiningSession() |
PointsMinedDomainEvent |
MinerId, PointsEarned, TotalPoints, StreakDays |
Miner.ClaimMiningReward() |
StreakUpdatedDomainEvent |
MinerId, PreviousStreak, NewStreak, NewBonusMultiplier |
Defined but not raised in current code |
CircleCompletedDomainEvent |
CircleId, OwnerId, MemberCount |
Circle.RecalculateStatus() (when reaching 3 members) |
ReferralActivatedDomainEvent |
ReferralId, ReferrerId, ReferredId |
Referral.Activate() |
ConfigurationUpdatedDomainEvent |
ConfigType, UpdatedBy, UpdatedAt |
Defined but not raised in current code |
8. Dependencies
NuGet Packages
API Layer (MiningService.API.csproj):
| Package |
Version |
| MediatR |
12.4.1 |
| FluentValidation |
11.11.0 |
| FluentValidation.DependencyInjectionExtensions |
11.11.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer |
10.0.2 |
| Microsoft.EntityFrameworkCore.Design |
10.0.2 |
| Microsoft.Extensions.Http.Polly |
10.0.2 |
| Swashbuckle.AspNetCore |
7.2.0 |
| Asp.Versioning.Mvc |
8.1.0 |
| Asp.Versioning.Mvc.ApiExplorer |
8.1.0 |
| AspNetCore.HealthChecks.NpgSql |
8.0.2 |
| AspNetCore.HealthChecks.Redis |
8.0.1 |
| Hellang.Middleware.ProblemDetails |
6.5.1 |
| Serilog.AspNetCore |
8.0.3 |
| Serilog.Sinks.Console |
6.0.0 |
| Serilog.Sinks.Seq |
8.0.0 |
Infrastructure Layer (MiningService.Infrastructure.csproj):
| Package |
Version |
| Microsoft.EntityFrameworkCore |
10.0.0 |
| Npgsql.EntityFrameworkCore.PostgreSQL |
10.0.0 |
| Microsoft.EntityFrameworkCore.Tools |
10.0.0 |
| MediatR |
12.4.1 |
| Dapper |
2.1.35 |
| Microsoft.Extensions.Http.Polly |
9.0.0 |
| Polly |
8.5.0 |
| StackExchange.Redis |
2.8.16 |
Domain Layer (MiningService.Domain.csproj):
| Package |
Version |
| MediatR.Contracts |
2.0.1 |
External Service Dependencies
| Service |
Default URL |
Client Interface |
Purpose |
| IAM Service |
http://iam-service-net:8080 |
IIamServiceClient |
Get user info, validate user, KYC status |
| Wallet Service |
http://wallet-service-net:8080 |
IWalletServiceClient |
Transfer mined points to wallet, check balance |
| Social Service |
http://social-service-net:8080 |
ISocialServiceClient |
Friend suggestions for circles, friendship checks |
All external clients use Polly resilience policies:
- Retry: 3 retries with exponential backoff (2s, 4s, 8s)
- Circuit Breaker: Opens after 5 failures, stays open for 30s
9. Configuration
Environment Variables / appsettings.json
| Key |
Default |
Description |
ConnectionStrings:DefaultConnection |
Neon PostgreSQL connection string |
Primary database connection |
DATABASE_URL |
-- |
Fallback database connection string |
Redis:ConnectionString |
localhost:6379 |
Redis connection (declared but not actively used in repositories) |
Jwt:Secret |
your-super-secret-key-min-32-characters |
JWT signing key |
Jwt:Issuer |
goodgo-platform |
JWT issuer |
Jwt:Audience |
goodgo-services |
JWT audience |
Jwt:AccessTokenExpiryMinutes |
15 |
Token expiry (declared, not used internally) |
Jwt:RefreshTokenExpiryDays |
7 |
Refresh token expiry (declared, not used internally) |
ExternalServices:IamService:BaseUrl |
http://iam-service-net:8080 |
IAM service URL |
ExternalServices:WalletService:BaseUrl |
http://wallet-service-net:8080 |
Wallet service URL |
ExternalServices:SocialService:BaseUrl |
http://social-service-net:8080 |
Social service URL |
AllowedOrigins |
["http://localhost:3000", "http://localhost:5173"] |
CORS allowed origins |
ASPNETCORE_ENVIRONMENT |
Development |
Environment name |
ASPNETCORE_URLS |
http://+:8080 (Docker) |
Listen URLs |
MediatR Pipeline Order
LoggingBehavior -- logs request name, elapsed time
ValidatorBehavior -- runs FluentValidation (no validators currently defined)
TransactionBehavior -- wraps Commands in DB transaction (skips Queries by name suffix)
Docker
- Multi-stage build:
sdk:10.0 (build) -> aspnet:10.0 (runtime)
- Non-root user:
dotnetuser (UID/GID 1001)
- Port: 8080
- Health check:
curl -f http://localhost:8080/health/live (30s interval, 3 retries)
Database Startup
- Auto-applies EF Core migrations on startup (
dbContext.Database.MigrateAsync())
- Npgsql retry on failure: 5 retries, 30s max delay
Idempotency
ClientRequest entity and RequestManager are defined but not wired into the command pipeline (no commands use idempotency checks).
Known Gaps / Stubs
- No FluentValidation validators are defined for any command
- ConfigurationRepository is purely in-memory (static fields with lock); database persistence for configs is not implemented
- Analytics queries for Points, Streaks, and AuditLogs have no handlers
- BanMinerCommand handler calls
Suspend() instead of Ban()
- ResetMinerStreakCommand handler does not actually reset the streak
- UpdateSystemConfigCommand and UpdateMiningConfigCommand handlers do not persist changes
- RabbitMQ integration for publishing/consuming integration events is not connected
- Redis is declared as a dependency but not used in any repository or service
- Dapper is referenced but no raw SQL queries exist
- Table names use PascalCase instead of the platform-standard snake_case