Loyalty Point Exchange API

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
4
Programs
4
Members
2
Transactions
2
Completed
0
Failed
200
Points Exchanged

API Endpoints

MethodPathDescription
GET/api/programsList all active loyalty programs
GET/api/programs/:idGet a specific program
POST/api/programsCreate a new program
GET/api/membersList all members
GET/api/members/:idGet a specific member
POST/api/membersCreate a new member
GET/api/members/:id/balancesList balances for a member
POST/api/members/:id/depositDeposit points into a balance
GET/api/ratesList all exchange rates
POST/api/ratesCreate/update an exchange rate
POST/api/exchangesRequest a point exchange
GET/api/exchanges/:idGet exchange transaction status
GET/api/healthHealth check with DB status and uptime

Live Data

Programs

CodeNameCurrencyID
FUELFuelSaver Cardlitresf5f0db05-a6b8-44d9-aec0-468567bac28d
SHOPShopRewards Pluspointsfeb00fff-d806-4b7c-8f9d-770b5aa4afc2
SKYSkyMiles Airwaysmilesca8d3c6b-6e3a-4d08-88ec-68f33fa23dfa
STAYStayLoyalty Hotelsnightsf846eef0-e6df-43bf-87fe-59a3552c4e95

Members

NameEmailExternal IDID
Alice Chenalice@example.comUSR-00170772519-c197-4bec-bb7d-f5c25d2fc9ae
Bob Muellerbob@example.comUSR-0029347c42c-f452-4aab-9b80-5dc8d00b4089
Demo User 1772694719779demo-1772694719779@test.comDEMO-17726947197794fe3e8ad-e781-441c-9037-763ec26db7db
Demo User 1772696500760demo-1772696500760@test.comDEMO-1772696500760142d368e-61f4-4054-abea-25473929bfc4

Exchange Rates

FromToRate
SKYSHOP2.5
SHOPSKY0.3
SKYSTAY0.8
STAYSKY1.1
SHOPFUEL1.5
FUELSHOP0.6

Interactive Demo

Run a complete exchange lifecycle: deposit points, exchange between programs via Broadway pipeline, and verify updated balances.

Example: curl Commands

List Programs

curl https://qiibee-loyalty-api.josboxoffice.com:80/api/programs

Deposit Points

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}'

Request Exchange (100 FUEL → SHOP)

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"
  }'

Check Balances

curl https://qiibee-loyalty-api.josboxoffice.com:80/api/members/70772519-c197-4bec-bb7d-f5c25d2fc9ae/balances

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    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

Key Design Decisions

DecisionRationale
Ash FrameworkDeclarative resource modeling eliminates boilerplate controllers, JSON views, and context modules while auto-generating JSON:API endpoints
Ash TransactionsAtomic debit/credit/status transitions via transaction? true — if any step fails the entire exchange rolls back
ETS Rate CacheO(1) concurrent reads for exchange rates without hitting PostgreSQL on every exchange
Broadway PipelineAsync exchange processing with configurable concurrency (4 processors, batching). Swap in SQS/RabbitMQ for production
Idempotency KeysUnique key per exchange prevents double-spend; duplicate requests return existing transaction
CHECK ConstraintPostgreSQL points >= 0 constraint on balances as database-level safety net