https://rubbi-api.onrender.com/api/v1
All endpoints return JSON. Swagger UI is available at GET /docs.
- User connects wallet (MetaMask, WalletConnect, etc.) on Arbitrum.
- User signs a transaction calling
Authentication.createAccount(username)on-chain. This registers their wallet with a username. - POST /auth/register —
{ walletAddress, username }— returns{ token, user }. A virtual card is auto-created. - On subsequent visits: POST /auth/login —
{ walletAddress }— returns a fresh{ token, user }.
Authorization: Bearer <jwt>
Invalid/expired tokens return 401.
{ "sub": "0xabc...", "iat": 123, "exp": 456 }sub is the lowercased wallet address.
All errors use the standard Fastify shape:
{ "statusCode": 400, "error": "Bad Request", "message": "..." }| Code | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Invalid input, on-chain verification failed |
| 401 | Unauthorized | Missing/expired JWT, invalid webhook HMAC |
| 404 | Not Found | User/card/deposit not found |
| 409 | Conflict | Username doesn't match on-chain profile |
| 429 | Too Many Requests | Rate limit (120/min, 600/min for webhooks) |
- Default: 120 requests/minute per IP
- Webhooks: 600 requests/minute
No auth. Returns:
{ "status": "ok", "service": "rubbi-api", "timestamp": "2026-01-01T00:00:00.000Z" }No auth. Register a wallet that has already called Authentication.createAccount() on-chain.
Body:
{ "walletAddress": "0x...", "username": "alice_123" }username must match the name used in the on-chain createAccount() call (3–32 alphanumeric chars + underscores).
Response 201:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"walletAddress": "0x...",
"username": "alice_123",
"rubbiBalance": "0"
}
}A virtual card is automatically created — no separate endpoint needed.
No auth. Log in an existing user.
Body: { "walletAddress": "0x..." }
Response 200: Same shape as register.
Auth required. Returns the current user profile.
{
"id": "uuid",
"walletAddress": "0x...",
"username": "alice_123",
"rubbiBalance": "42.5",
"card": {
"id": "uuid",
"issuerId": "sudo-card-id",
"last4": "1234",
"expiryMonth": "12",
"expiryYear": "29",
"status": "ACTIVE"
}
}card is null if no card exists (shouldn't happen with auto-creation).
Auth required. Card summary.
{
"id": "uuid",
"issuerId": "sudo-card-id",
"last4": "1234",
"expiryMonth": "12",
"expiryYear": "29",
"status": "ACTIVE",
"rubbiBalance": "42.5"
}Auth required. Freeze the card.
Body: { "reason": "Lost card" } (optional, 1–140 chars)
Response: { "status": "frozen" }
Auth required. Unfreeze the card.
Body: { "reason": "Found it" } (optional, 1–140 chars)
Response: { "status": "active" }
Auth required. Reveals the full PAN (PCI-sensitive — use sparingly).
{
"cardId": "uuid",
"pan": "4111111111111111",
"last4": "1111",
"expiryMonth": "12",
"expiryYear": "29",
"status": "ACTIVE"
}Auth required. List deposits (most recent ~100).
[
{
"id": "uuid",
"txHash": "0x...",
"amountRaw": "1000000000000000000",
"amountRubbi": "1.0",
"status": "CONFIRMED",
"blockNumber": 123456,
"confirmedAt": "2026-01-01T00:00:00.000Z",
"createdAt": "2026-01-01T00:00:00.000Z"
}
]Auth required. Get a specific deposit by transaction hash.
Params: txHash — hex string (0x + 64 hex chars)
Auth required. Paginated ledger.
Query: ?page=1&pageSize=20 (pageSize max 100, defaults to 20)
{
"page": 1,
"pageSize": 20,
"total": 5,
"items": [
{
"id": "uuid",
"type": "DEPOSIT",
"amountRubbi": "10.0",
"balanceAfter": "50.0",
"description": "Deposit confirmed",
"metadata": { "txHash": "0x..." },
"createdAt": "2026-01-01T00:00:00.000Z"
}
]
}Transaction types: DEPOSIT, SPEND, REFUND, SUBSCRIPTION_PAYMENT, FAUCET
Auth required. All active subscription plans from the contract.
[
{ "planId": 1, "name": "Premium", "feeRubbi": "10.0", "active": true }
]Auth required. User's active subscriptions from the contract.
[
{ "active": true, "planName": "Premium", "feeRubbi": "10.0", "userAddress": "0x...", "planId": 1 }
]Auth required. Confirm a user-signed SubscriptionService.startSubscription(planId, email, pw) transaction.
Body: { "planId": 1, "txHash": "0x..." }
Response 201: { "txHash": "0x...", "verified": true }
Auth required. Confirm a user-signed pauseSubscription(planId) tx.
Params: planId, Body: { "txHash": "0x..." }
Response: { "txHash": "0x...", "planId": 1, "verified": true }
Auth required. Confirm a user-signed resumeSubscription(planId) tx.
Params: planId, Body: { "txHash": "0x..." }
Response: { "txHash": "0x...", "planId": 1, "verified": true }
Pattern: User signs the tx in their wallet → frontend posts the txHash → backend verifies on-chain receipt. The API never submits transactions.
Auth required. Confirm a user-signed RubbiToken.claimFaucet() tx.
Body: { "txHash": "0x..." }
Response 201: { "txHash": "0x...", "verified": true }
Auth required. Time remaining before next claim.
{ "walletAddress": "0x...", "cooldownSeconds": 84321 }Auth required. All daily and monthly salary streams.
{
"daily": [ /* StreamShape */ ],
"monthly": [ /* StreamShape */ ]
}Auth required. Single stream.
StreamShape:
{
"id": "string",
"recipient": "0x...",
"amount": "1000000000000000000",
"lastPayment": "0",
"startTime": "1700000000",
"intervalType": 0,
"active": true,
"name": "Monthly salary",
"streamOwner": "0x..."
}Auth required. Current contract streaming fees.
{ "fees": "500000000000000000" }Auth required. Confirm SalaryStreaming.createStream(details, intervalType) tx.
Body: { "txHash": "0x..." }
Response 201: { "txHash": "0x...", "streamId": "1", "verified": true }
Auth required. Confirm pauseDailyStream(streamId) or pauseMonthlyStream(streamId).
Body: { "txHash": "0x..." }
Response: { "txHash": "0x...", "streamId": 1, "verified": true }
Auth required. Same pattern for resume.
Auth required. Confirm disburseDaily() or disburseMonthly().
Body: { "txHash": "0x..." }
Response 201: { "txHash": "0x...", "verified": true }
No auth (HMAC). Sudo Africa card issuer webhook.
Headers: x-sudo-signature or x-signature — HMAC SHA-256 hex
Supported events:
authorization.request— balance check (returns"00"approve or"51"decline)card.charge/transaction.created— deducts balance, records SPEND ledger entryrefund/reversal— ignored
Response:
// Authorization approved
{ "statusCode": 200, "data": { "responseCode": "00" }, "ok": true }
// Insufficient funds
{ "statusCode": 200, "data": { "responseCode": "51" } }
// Spend processed
{ "ok": true }
// Ignored event
{ "ok": true, "ignored": true, "reason": "Unsupported event type: refund" }
// Duplicate (within 24h)
{ "ok": true, "duplicate": true }Endpoints for subscriptions, salary streaming, and faucet follow this flow:
- Frontend calls the contract method via the user's wallet (e.g.,
SubscriptionService.startSubscription(planId, email, pw)) - Wallet returns a
txHash - Frontend sends the
txHashto the API - API verifies the transaction receipt on-chain and returns
{ verified: true }
Deposits are fully on-chain driven:
- User sends RUBBI to
ModalContract.deposit(amount) - The Arbitrum listener detects
DepositSuccessful - A worker credits the user's balance, records the transaction, and funds their virtual card
- Frontend polls
GET /depositsorGET /transactionsto show the update
| Contract | Env variable |
|---|---|
| RubbiToken | RUBBI_TOKEN_ADDRESS |
| ModalContract | MODAL_CONTRACT_ADDRESS |
| Authentication | AUTH_CONTRACT_ADDRESS |
| SubscriptionService | SUBSCRIPTION_CONTRACT_ADDRESS |
| SalaryStreaming | SALARY_STREAMING_ADDRESS |
All deployed on Arbitrum (chain ID 42161 or 421614 for Sepolia testnet).
User: id (UUID), walletAddress (unique), username, rubbiBalance (Decimal), card (1:1), deposits, transactions, subscriptions
Card: id (UUID), userId (unique FK), issuerId (Sudo ID), last4, expiryMonth, expiryYear, status (ACTIVE|FROZEN|TERMINATED)
Deposit: id (UUID), userId (FK), txHash (unique), amountRaw, amountRubbi, status (PENDING|CONFIRMED|FAILED), blockNumber
Transaction: id (UUID), userId (FK), type (DEPOSIT|SPEND|REFUND|SUBSCRIPTION_PAYMENT|FAUCET), amountRubbi, balanceAfter, description, metadata (JSON)
Subscription: id (UUID), userId (FK), onChainPlanId, planName, feeRubbi, status (ACTIVE|PAUSED|CANCELLED). Unique on [userId, onChainPlanId].