A proof-of-concept B2B loyalty marketplace API where brands tokenize loyalty points and users exchange them across programs. Built as a technical demonstration.
Elixir Phoenix Ash Framework Broadway PostgreSQL| Method | Path | Description |
|---|---|---|
| GET | /api/programs | List all active loyalty programs |
| GET | /api/programs/:id | Get a specific program |
| POST | /api/programs | Create a new program |
| GET | /api/members | List all members |
| GET | /api/members/:id | Get a specific member |
| POST | /api/members | Create a new member |
| GET | /api/members/:id/balances | List balances for a member |
| POST | /api/members/:id/deposit | Deposit points into a balance |
| GET | /api/rates | List all exchange rates |
| POST | /api/rates | Create/update an exchange rate |
| POST | /api/exchanges | Request a point exchange |
| GET | /api/exchanges/:id | Get exchange transaction status |
| GET | /api/health | Health check with DB status and uptime |
| Code | Name | Currency | ID |
|---|---|---|---|
| FUEL | FuelSaver Card | litres | f5f0db05-a6b8-44d9-aec0-468567bac28d |
| SHOP | ShopRewards Plus | points | feb00fff-d806-4b7c-8f9d-770b5aa4afc2 |
| SKY | SkyMiles Airways | miles | ca8d3c6b-6e3a-4d08-88ec-68f33fa23dfa |
| STAY | StayLoyalty Hotels | nights | f846eef0-e6df-43bf-87fe-59a3552c4e95 |
| Name | External ID | ID | |
|---|---|---|---|
| Alice Chen | alice@example.com | USR-001 | 70772519-c197-4bec-bb7d-f5c25d2fc9ae |
| Bob Mueller | bob@example.com | USR-002 | 9347c42c-f452-4aab-9b80-5dc8d00b4089 |
| Demo User 1772694719779 | demo-1772694719779@test.com | DEMO-1772694719779 | 4fe3e8ad-e781-441c-9037-763ec26db7db |
| Demo User 1772696500760 | demo-1772696500760@test.com | DEMO-1772696500760 | 142d368e-61f4-4054-abea-25473929bfc4 |
| From | To | Rate | |
|---|---|---|---|
| SKY | → | SHOP | 2.5 |
| SHOP | → | SKY | 0.3 |
| SKY | → | STAY | 0.8 |
| STAY | → | SKY | 1.1 |
| SHOP | → | FUEL | 1.5 |
| FUEL | → | SHOP | 0.6 |
Run a complete exchange lifecycle: deposit points, exchange between programs via Broadway pipeline, and verify updated balances.
curl https://qiibee-loyalty-api.josboxoffice.com:80/api/programs
curl -X POST https://qiibee-loyalty-api.josboxoffice.com:80/api/members/70772519-c197-4bec-bb7d-f5c25d2fc9ae/deposit \
-H "Content-Type: application/json" \
-d '{"program_id":"f5f0db05-a6b8-44d9-aec0-468567bac28d","amount":5000}'curl -X POST https://qiibee-loyalty-api.josboxoffice.com:80/api/exchanges \
-H "Content-Type: application/json" \
-d '{
"member_id": "70772519-c197-4bec-bb7d-f5c25d2fc9ae",
"from_program_id": "f5f0db05-a6b8-44d9-aec0-468567bac28d",
"to_program_id": "feb00fff-d806-4b7c-8f9d-770b5aa4afc2",
"from_amount": 100,
"idempotency_key": "unique-key-123"
}'curl https://qiibee-loyalty-api.josboxoffice.com:80/api/members/70772519-c197-4bec-bb7d-f5c25d2fc9ae/balances
┌─────────────────────────────────────────────────────────────┐
│ AshJsonApi Router │
│ POST /api/exchanges GET /api/programs GET /api/members │
└──────────────┬──────────────────────────────────┬───────────┘
│ │
┌───────▼────────┐ ┌────────▼─────────┐
│ Exchange Domain │ │ Programs Domain │
│ (Ash.Domain) │ │ (Ash.Domain) │
│ │ │ │
│ - Transaction │ │ - Program │
│ - ExchangeRate │ │ - Member │
│ │ │ - Balance │
└───────┬─────────┘ └──────────────────┘
│ │
┌───────▼─────────┐ ┌──────────────────┘
│ Broadway Pipeline│ │
│ │ │ ┌──────────────────────┐
│ - pull from queue│ │ │ RateServer (GenServer)│
│ - apply rate │◄──────┘ │ │
│ - debit/credit │◄─────────│ - ETS-backed cache │
│ - ack/fail │ │ - periodic refresh │
└───────┬──────────┘ └──────────────────────┘
│
┌───────▼──────────┐
│ PostgreSQL 16 │
└──────────────────┘
Application Supervision Tree:
└── Supervisor (one_for_one)
├── Repo
├── PubSub
├── ExchangeQueue (GenServer + :queue)
├── RateServer (GenServer + ETS)
├── ExchangePipeline (Broadway)
└── Endpoint
| Decision | Rationale |
|---|---|
| Ash Framework | Declarative resource modeling eliminates boilerplate controllers, JSON views, and context modules while auto-generating JSON:API endpoints |
| Ash Transactions | Atomic debit/credit/status transitions via transaction? true — if any step fails the entire exchange rolls back |
| ETS Rate Cache | O(1) concurrent reads for exchange rates without hitting PostgreSQL on every exchange |
| Broadway Pipeline | Async exchange processing with configurable concurrency (4 processors, batching). Swap in SQS/RabbitMQ for production |
| Idempotency Keys | Unique key per exchange prevents double-spend; duplicate requests return existing transaction |
| CHECK Constraint | PostgreSQL points >= 0 constraint on balances as database-level safety net |