Getting Started
Prerequisites
- .NET 8 SDK (8.0+)
- Docker (for PostgreSQL)
- Node.js 20+ (for client-side tools, optional)
- ngrok (to expose the API to mobile/webapp over HTTPS)
Running Locally
# 1. Start PostgreSQL
cd backend
docker-compose up -d
cd Redatium.Api
# 2. Initialize the database
psql -U postgres -h localhost -p 5432 -d SOLIDUS -f Database/recreate_database.sql
psql -U postgres -h localhost -p 5432 -d SOLIDUS -f Database/seed_database.sql
# 3. Run the API
dotnet run
# Listening on http://0.0.0.0:5032
# 4. Expose via ngrok (required for mobile app)
ngrok http 5032
# Copy the HTTPS URL to mobile/.env (REACT_APP_API_BASE_URL) and webapp/.env (VITE_API_BASE_URL)
Swagger
In development mode, Swagger UI is available at http://localhost:5032/swagger.
Authentication Flow
Wallet-Based Auth (SIWE)
- Client checks wallet registration via
GET /user/wallet/check/{walletAddress} - Client-side SIWE signing (MetaMask/WalletConnect) — verified locally in the browser/app, not on the backend
- Most endpoints read the userId from a
Request<T>header field or fromCurrentUserService, which extracts it from the HTTP context - The backend trusts the wallet address sent by the client
Note: State channel endpoints (/api/statechannel/*) currently require no authentication — they use wallet address matching only.
Base URL and Conventions
Development base URL: https://<ngrok-subdomain>.ngrok-free.dev (changes on each ngrok restart)
Request Format
All POST endpoints accept:
{
"header": {
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
},
"payload": {
// endpoint-specific data
}
}
Response Format
All endpoints return:
{
"payload": { /* result data */ },
"exception": null
}
On error:
{
"payload": null,
"exception": {
"code": "DP-404",
"description": "Resource not found"
}
}
Error codes follow a prefix convention:
DP-4xx: Domain/business errorsTE-5xx: Technical/infrastructure errorsSC-4xx: State channel errors
Users
/userC# Types: UserWalletDto
// DTOs
public class UserWalletDto
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string WalletAddress { get; set; }
public decimal Balance { get; set; }
}GET/user/walletGet Wallet
Response UserWalletDto
| Field | Type | Description |
|---|---|---|
id | uuid | Wallet record ID |
userId | uuid | Owner user ID |
walletAddress | string | Ethereum address |
balance | decimal | Current balance |
---
GET/user/wallet/check/{walletAddress}Check Wallet Registration
No authentication required.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
walletAddress | string | Ethereum address to check |
Response
---
Datasets
/datasetC# Types: DatasetDto, CreateDatasetDto, BrowseDatasetRequestDto, BrowseDatasetDto
public class DatasetDto
{
public Guid Id { get; set; }
public Guid OwnerId { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public Guid? DatasetTypeId { get; set; }
public string? Type { get; set; } // Joined from DatasetType
public string? TypeMachineName { get; set; }
public string? SourceInfo { get; set; } // JSON metadata
public decimal? QualityScore { get; set; }
public DateTime Created { get; set; }
public DateTime? Changed { get; set; }
}
public class CreateDatasetDto
{
public Guid OwnerId { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public Guid? DatasetTypeId { get; set; }
public string? SourceInfo { get; set; }
public decimal? QualityScore { get; set; }
}
public class BrowseDatasetRequestDto
{
public int PageLimit { get; set; } = 20;
public int PageOffset { get; set; } = 0;
public Guid? DatasetTypeId { get; set; }
public int? MinRating { get; set; }
public DateTime? CreatedFrom { get; set; }
public DateTime? CreatedTo { get; set; }
public string? SearchTerm { get; set; }
public string SortField { get; set; } = "created";
public string SortOrder { get; set; } = "desc";
}
public class BrowseDatasetDto
{
public Guid Id { get; set; }
public Guid OwnerId { get; set; }
public string? OwnerUsername { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public string? Type { get; set; }
public DateTime Created { get; set; }
public decimal? QualityScore { get; set; }
public decimal? AverageRating { get; set; }
public decimal? Price { get; set; }
public int TotalSales { get; set; }
}GET/dataset/listList My Datasets
Returns all datasets owned by the authenticated user.
Response ReturnListDatasetDto
---
GET/dataset/{id}Get Dataset
Path Parameters
| Parameter | Type |
|---|---|
id | uuid |
Response DatasetDto
---
POST/dataset/createCreate Dataset
Request Body CreateDatasetDto
| Field | Type | Required | Description |
|---|---|---|---|
ownerId | uuid | Yes | Owner user ID |
name | string | Yes | Dataset name |
description | string | No | Description |
datasetTypeId | uuid | No | Category type ID |
sourceInfo | string | No | JSON with source metadata |
qualityScore | decimal | No | Quality score 0-5 |
Response DatasetDto
---
POST/dataset/updateUpdate Dataset
Request Body UpdateDatasetDto
| Field | Type | Required |
|---|---|---|
id | uuid | Yes |
ownerId | uuid | Yes |
name | string | No |
description | string | No |
datasetTypeId | uuid | No |
sourceInfo | string | No |
qualityScore | decimal | No |
---
POST/dataset/deleteDelete Dataset
Request Body DeleteDatasetDto
| Field | Type | Required |
|---|---|---|
datasetId | uuid | Yes |
---
POST/dataset/browseBrowse Marketplace
The main marketplace search endpoint. Returns datasets from all users.
Request Body BrowseDatasetRequestDto
| Field | Type | Default | Description |
|---|---|---|---|
pageLimit | int | 20 | Max results |
pageOffset | int | 0 | Skip count |
datasetTypeId | uuid | null | Filter by type |
minRating | int | null | Min average rating |
createdFrom | datetime | null | Created after |
createdTo | datetime | null | Created before |
searchTerm | string | null | Free text search |
sortField | string | "created" | Sort column |
sortOrder | string | "desc" | asc or desc |
Response List
---
GET/dataset/typesList Dataset Types
Response List
---
Data Sharing Requests
/requestC# Types: DataSharingRequestDto, CreateDataSharingRequestDto, ListDataSharingRequestDto
public class DataSharingRequestDto
{
public Guid Id { get; set; }
public Guid RequestorId { get; set; }
public Guid OwnerId { get; set; }
public Guid DatasetId { get; set; }
public string Status { get; set; } // "pending", "approved", "declined", "completed"
public string? Purpose { get; set; }
public string? DatasetName { get; set; }
public string? DatasetType { get; set; }
public decimal? Price { get; set; }
public string? OwnerUsername { get; set; }
public string? RequestorUsername { get; set; }
public string? MagnetUri { get; set; }
public string? TransferStatus { get; set; } // "seeding", "downloading", "completed"
public DateTime? SeedingStartedAt { get; set; }
public DateTime? DownloadCompletedAt { get; set; }
public string? HashLock { get; set; }
public string? EscrowTxHash { get; set; }
public string? EscrowSecret { get; set; }
public string? EscrowState { get; set; }
public DateTime? EscrowExpiry { get; set; }
public DateTime? BuyerConfirmedAt { get; set; }
public string? ClaimTxHash { get; set; }
public DateTime Created { get; set; }
}
public class CreateDataSharingRequestDto
{
public Guid? RequestorId { get; set; } // Auto-set from auth if null
public Guid OwnerId { get; set; }
public Guid DatasetId { get; set; }
public string? Purpose { get; set; }
}
public class ListDataSharingRequestDto
{
public int PageLimit { get; set; } = 10000;
public int PageOffset { get; set; } = 0;
public string? SortField { get; set; }
public string? SortOrder { get; set; }
public string? Status { get; set; }
public Guid? RequestorId { get; set; }
public Guid? OwnerId { get; set; }
}GET/request/{id}Get Request
Response DataSharingRequestDto
---
POST/request/listList Requests
Request Body ListDataSharingRequestDto
| Field | Type | Default | Description |
|---|---|---|---|
pageLimit | int | 10000 | Max results |
pageOffset | int | 0 | Skip count |
sortField | string | "Created" | Sort column |
sortOrder | string | "desc" | asc or desc |
status | string | null | Filter: pending, approved, declined, completed |
requestorId | uuid | null | Filter by requestor |
ownerId | uuid | null | Filter by dataset owner |
Response ReturnListDataSharingRequestDto — { data: DataSharingRequestDto[], metadata }
---
POST/request/createCreate Request
Request Body CreateDataSharingRequestDto
| Field | Type | Required | Description |
|---|---|---|---|
requestorId | uuid | No | Auto-set from auth if null |
ownerId | uuid | Yes | Dataset owner |
datasetId | uuid | Yes | Target dataset |
purpose | string | No | Reason for request |
---
POST/request/respondRespond to Request
Request Body RespondDataSharingRequestDto
| Field | Type | Required | Description |
|---|---|---|---|
id | uuid | Yes | Request ID |
status | string | Yes | approved or declined |
---
POST/request/create-bulkBulk Create Requests
Request Body CreateBulkDataSharingRequestDto
| Field | Type |
|---|---|
requests | CreateDataSharingRequestDto[] |
Response BulkRequestResultDto — { successful: DataSharingRequestDto[], failed: FailedRequestDto[] }
---
POST/request/{id}/start-seedingStart Seeding
Called by the seller after creating a torrent.
Request Body StartSeedingDto
| Field | Type | Required |
|---|---|---|
magnetUri | string | Yes |
---
GET/request/{id}/magnet-uriGet Magnet URI
Response { magnetUri: string }
---
POST/request/{id}/complete-downloadComplete Download
Marks the transfer as completed by the buyer. No request body.
---
POST/request/{id}/cancel-downloadCancel Download
Cancels an in-progress download. No request body.
---
POST/request/{id}/set-priceSet Price
Request Body SetPriceDto
| Field | Type | Required |
|---|---|---|
price | decimal | Yes |
---
Transactions
/transactionGET/transaction/listList Transactions
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
pageLimit | int | 100 | Max results |
pageOffset | int | 0 | Skip count |
sortField | string | null | Sort column |
sortOrder | string | null | asc or desc |
status | string | null | Filter by status |
datasetId | uuid | null | Filter by dataset |
Response ReturnListDataTransactionDto — { data: DataTransactionDto[], metadata }
DataTransactionDto: { id, buyerId, sellerId, datasetId, datasetName, datasetType, amount, status, blockchainTx, created, buyerUsername, sellerUsername }
---
POST/transaction/createCreate Transaction
Request Body CreateDataTransactionDto
| Field | Type | Required |
|---|---|---|
buyerId | uuid | Yes |
sellerId | uuid | Yes |
datasetId | uuid | Yes |
amount | decimal | Yes |
---
POST/transaction/create-bulkBulk Create Transactions
Request Body CreateBulkDataTransactionDto — { transactions: CreateDataTransactionDto[] }
Response BulkTransactionResultDto — { successful: [], failed: [] }
---
GET/transaction/status?transactionId={uuid}Get Transaction Status
Response DataTransactionDto
---
GET/transaction/tokenList Token Transactions
Query Parameters
| Parameter | Type | Description |
|---|---|---|
pageLimit | int | Max results |
pageOffset | int | Skip count |
transactionType | string | Filter by type |
Response ReturnListTokenTransactionDto
TokenTransactionDto: { id, userId, amount, type, relatedTxId, created }
---
GET/transaction/recent-activityRecent Activity
Returns the top 20 most recent transactions for the dashboard.
---
Notifications
/notificationPOST/notification/listList Notifications
Request Body NotificationListRequestDto
| Field | Type | Default |
|---|---|---|
pageLimit | int | 100 |
pageOffset | int | 0 |
isRead | bool | null |
Response ReturnListNotificationDto
NotificationDto: { id, userId, type, message, status, created }
---
POST/notification/readMark as Read
Request Body NotificationReadRequestDto
| Field | Type | Required |
|---|---|---|
notificationId | uuid | Yes |
---
State Channels
/api/statechannelC# Types: StateChannelDto, SignedStateDto, VirtualEscrowDto, OpenChannelRequest, SubmitSignedStateRequest
public class StateChannelDto
{
public Guid Id { get; set; }
public string ChannelId { get; set; } // bytes32 hex
public string Participant0 { get; set; } // Seller address
public string Participant1 { get; set; } // Buyer address
public long ChannelNonce { get; set; }
public string Allocation0 { get; set; } = "0"; // Seller allocation (wei)
public string Allocation1 { get; set; } = "0"; // Buyer allocation (wei)
public long LatestTurnNum { get; set; }
public string Status { get; set; } // "Open", "Closed", "Disputed"
public string ChannelType { get; set; } // "DataTransfer"
public string? OpenTxHash { get; set; }
public string? CloseTxHash { get; set; }
public DateTime Created { get; set; }
}
public class SignedStateDto
{
public Guid Id { get; set; }
public string ChannelId { get; set; }
public long TurnNum { get; set; }
public decimal Allocation0 { get; set; }
public decimal Allocation1 { get; set; }
public bool IsFinal { get; set; }
public string? AppDataJson { get; set; }
public string? Signature0 { get; set; }
public string? Signature1 { get; set; }
public int ChunkIndex { get; set; }
public string? ChunkHash { get; set; }
public string StateType { get; set; } = "GAME"; // GAME, PRE_FUND_SETUP, POST_FUND_SETUP, CONCLUDE
public DateTime Created { get; set; }
}
public class VirtualEscrowDto
{
public Guid Id { get; set; }
public string PurchaseId { get; set; }
public string BuyerChannelId { get; set; }
public string SellerChannelId { get; set; }
public Guid? DatasetId { get; set; }
public string? DatasetName { get; set; }
public string BuyerAddress { get; set; }
public string SellerAddress { get; set; }
public decimal Price { get; set; }
public string HashLock { get; set; }
public string? SecretPreimage { get; set; }
public string Status { get; set; }
public string? MagnetUri { get; set; }
public DateTime? TimeoutAt { get; set; }
public DateTime Created { get; set; }
}
public class OpenChannelRequest
{
public string CounterpartyAddress { get; set; }
public decimal DepositAmountEth { get; set; }
public string ChannelType { get; set; }
}
public class SubmitSignedStateRequest
{
public string ChannelId { get; set; }
public long TurnNum { get; set; }
public string Allocation0 { get; set; } = "0";
public string Allocation1 { get; set; } = "0";
public bool IsFinal { get; set; }
public string? AppDataJson { get; set; }
public string Signature0 { get; set; }
public string Signature1 { get; set; }
public int ChunkIndex { get; set; }
public string? ChunkHash { get; set; }
public string StateType { get; set; } = "GAME";
}GET/api/statechannel/{channelId}Get Channel
Response StateChannelDto
---
GET/api/statechannel/user/{walletAddress}Get User Channels
Returns all channels where this wallet is a participant.
Response List
---
POST/api/statechannel/closeClose Channel
Request Body CooperativeCloseRequest
| Field | Type | Required |
|---|---|---|
channelId | string | Yes |
finalAllocation0 | decimal | Yes |
finalAllocation1 | decimal | Yes |
signature0 | string | Yes |
signature1 | string | Yes |
---
GET/api/statechannel/{channelId}/statesGet Signed States
Response List
SignedStateDto: { id, channelId, turnNum, allocation0, allocation1, isFinal, appDataJson, signature0, signature1, chunkIndex, chunkHash, stateType, created }
---
POST/api/statechannel/{channelId}/statesSubmit Signed State
Request Body SubmitSignedStateRequest
| Field | Type | Default | Description |
|---|---|---|---|
channelId | string | — | Channel ID |
turnNum | long | — | Turn number |
allocation0 | string | "0" | Seller allocation (wei) |
allocation1 | string | "0" | Buyer allocation (wei) |
isFinal | bool | — | Is this a final state |
appDataJson | string | null | App-specific JSON |
signature0 | string | — | Seller signature |
signature1 | string | — | Buyer signature |
chunkIndex | int | — | Chunk index |
chunkHash | string | null | Hash of delivered chunk |
stateType | string | "GAME" | GAME, PRE_FUND_SETUP, POST_FUND_SETUP, CONCLUDE |
---
GET/api/statechannel/purchases/seller/{address}Get Seller Purchases
Response List
VirtualEscrowDto: { id, purchaseId, buyerChannelId, sellerChannelId, datasetId, datasetName, buyerAddress, sellerAddress, price, hashLock, secretPreimage, status, magnetUri, timeoutAt, created }
---
GET/api/statechannel/purchases/buyer/{address}Get Buyer Purchases
Response List
---
POST/api/statechannel/purchase/proposePropose Purchase
Request Body ProposePurchaseRequest
| Field | Type | Required |
|---|---|---|
buyerChannelId | string | Yes |
datasetId | uuid | Yes |
sellerAddress | string | Yes |
ownerId | uuid | No |
price | decimal | Yes |
priceEth | string | Yes |
hashLock | string | Yes |
---
POST/api/statechannel/purchase/acceptAccept Purchase
Request Body AcceptPurchaseRequest
| Field | Type | Required |
|---|---|---|
purchaseId | string | Yes |
hashLock | string | Yes |
magnetUri | string | Yes |
---
POST/api/statechannel/settle-purchaseSettle Purchase
Request Body SettlePurchaseRequest
| Field | Type | Default |
|---|---|---|
channelId | string | — |
buyerPayout | string | "0" |
sellerPayout | string | "0" |
---
GET/api/statechannel/purchase/{purchaseId}Get Purchase
Response VirtualEscrowDto
---
PSD2 Banking
/adapters/psd2POST/adapters/psd2/consents/initiateInitiate Consent
Starts the NBG PSD2 OAuth2 consent flow.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
expirationDate | datetime | Optional consent expiry |
Response InitiateConsentResponseDto
---
POST/adapters/psd2/tokenExchange Token
Request Body ExchangeTokenRequestDto
| Field | Type | Required |
|---|---|---|
code | string | Yes |
Response TokenResponseDto
{
"Blockchain": {
"Enabled": true,
"Network": "Sepolia",
"ChainId": 11155111,
"RpcUrl": "https://eth-sepolia.g.alchemy.com/v2/
"WebSocketRpcUrl": "wss://eth-sepolia.g.alchemy.com/v2/
"EnableWebSocket": true,
"ContractAddress": "0xc0E4605CC9bD6768B785121c214B039946922082",
"RegistryContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",
"TransactionContractAddress": "0xD1dCe36c968938953576BD57b3ECf62b6e9E8555",
"EventPollingIntervalSeconds": 10,
"StartFromBlock": 10532939,
"BatchSize": 10,
"MaxBlockRetryThreshold": 100000,
"RequiredConfirmations": 2,
"MaxCatchupBlocks": 10000,
"ContractPollingDelayMs": 1000
}
}
var response = await SafeExecutor.ExecuteAsync(async () =>
{
return await _service.DoSomething(request.Payload!);
});
return Ok(response);
SELECT * FROM "BlockchainSyncState";
SELECT * FROM chain_events WHERE channel_id = '0x...' ORDER BY block_number;
SignalR Hub Reference
BlockchainHub (/hubs/blockchain)
Connection: new HubConnectionBuilder().withUrl("https://<api>/hubs/blockchain").build()
| Client Method | Parameters | Description |
|---|---|---|
SubscribeToWallet |
walletAddress: string |
Join wallet group for targeted events |
UnsubscribeFromWallet |
walletAddress: string |
Leave wallet group |
ProposeState |
channelId: string, signedStateJson: string |
Broadcast state to channel participants |
| Server Event | Payload | Description |
|---|---|---|
StateProposed |
{ channelId, signedStateJson } |
State broadcast to channel group |
TransferHub (/hubs/transfer)
Connection: new HubConnectionBuilder().withUrl("https://<api>/hubs/transfer").build()
| Client Method | Parameters | Description |
|---|---|---|
JoinChannel |
channelId: string |
Join channel group for P2P signaling |
LeaveChannel |
channelId: string |
Leave channel group |
RelaySignaling |
channelId: string, signalingData: string |
WebRTC SDP/ICE relay |
| Server Event | Payload | Description |
|---|---|---|
PeerJoined |
connectionId |
New peer in channel |
PeerLeft |
connectionId |
Peer left channel |
SignalingMessage |
connectionId, signalingData |
WebRTC signaling data |
ChallengeDetected |
{ channelId, turnNum, expiry } |
On-chain ChallengePlayed event. Known bug: Backend emits ChallengeDetected (line 2733) but clients listen for ChallengeRaised — event never received; only polling fallback works |
ChallengeResponded |
{ channelId, turnNum } |
Challenge response detected |
ChannelConcluded |
{ channelId, sellerBalance, buyerBalance } |
On-chain ChannelConcluded event. Known bug: Backend emits ChannelConcluded (line 2671) but clients listen for ChannelFinalized — event never received |
Blockchain Event Indexing Configuration
File: backend/Redatium.Api/appsettings.Development.json section Blockchain
{
"Blockchain": {
"Enabled": true,
"Network": "Sepolia",
"ChainId": 11155111,
"RpcUrl": "https://eth-sepolia.g.alchemy.com/v2/<key>",
"WebSocketRpcUrl": "wss://eth-sepolia.g.alchemy.com/v2/<key>",
"EnableWebSocket": true,
"ContractAddress": "0xc0E4605CC9bD6768B785121c214B039946922082",
"RegistryContractAddress": "0xabE1d17925519290c68c68eBDdbF3B96329EE5d6",
"TransactionContractAddress": "0xD1dCe36c968938953576BD57b3ECf62b6e9E8555",
"EventPollingIntervalSeconds": 10,
"StartFromBlock": 10532939,
"BatchSize": 10,
"MaxBlockRetryThreshold": 100000,
"RequiredConfirmations": 2,
"MaxCatchupBlocks": 10000,
"ContractPollingDelayMs": 1000
}
}
Set Enabled: false to disable blockchain polling entirely (useful when working on UI-only features).
Configuration Reference
appsettings.Development.json
| Section | Key | Description |
|---|---|---|
ConnectionStrings.DefaultConnection |
Host=localhost;Port=5432;Database=SOLIDUS;Username=postgres;Password=postgres |
PostgreSQL connection |
Cors.AllowedOrigins |
["*"] |
CORS origins (dev: all) |
JwtSettings.SecretKey |
(in appsettings.json) | JWT signing key |
JwtSettings.Issuer |
JWT issuer | |
JwtSettings.Audience |
JWT audience | |
Blockchain.* |
(see section 5) | Event indexer config |
Psd2Settings.* |
NBG sandbox OAuth credentials |
Database Scripts
Scripts are in backend/Redatium.Api/Database/Scripts/, numbered 01_Users.sql through 32_ChainEvents.sql. To add a new table, create a new numbered script and add it to recreate_database.sql.
Common Patterns
SafeExecutor
All controller actions wrap service calls in SafeExecutor.ExecuteAsync():
var response = await SafeExecutor.ExecuteAsync(async () =>
{
return await _service.DoSomething(request.Payload!);
});
return Ok(response);
This catches BusinessException (4xx), TechnicalException (5xx), and generic exceptions, mapping them to the standard Response<T> format.
Adding a New Endpoint
- Create DTO in
DTOs/{Feature}/ - Define interface in
Interfaces/I{Feature}Service.cs - Implement in
Services/{Feature}Service.cs - Register in
Program.cs:builder.Services.AddScoped<IFeatureService, FeatureService>() - Create
Controllers/{Feature}Controller.cs
Debugging Blockchain Events
Check the BlockchainSyncState table to see where the indexer is:
SELECT * FROM "BlockchainSyncState";
Check indexed chain events:
SELECT * FROM chain_events WHERE channel_id = '0x...' ORDER BY block_number;
Troubleshooting
| Problem | Solution |
|---|---|
| API not reachable from mobile | Ensure ngrok is running and URL is updated in mobile .env |
| Blockchain polling not starting | Check Blockchain:Enabled is true and RPC URL is valid |
| Events being missed | Check RequiredConfirmations (increase if chain is congested) |
| Database connection errors | Verify Docker container is running: docker ps |
| CORS errors from webapp | In development, all origins are allowed. Check that AllowCredentials is set. |
| SignalR connection fails | Ensure the API is running on 0.0.0.0:5032 (not localhost only) |