Node.js/Express API for quoting and executing Minecraft Bedrock Marketplace purchases (Minecoins), plus helpers to read balances/entitlements and fetch creator/offer metadata. Hardened with JWT auth, CORS allow‑list, Helmet, request/response logging, OpenAPI docs, and sensible timeouts.
PlayFab-Purchase Service exposes a compact, production‑ready façade over Minecraft Bedrock Marketplace endpoints:
- Quote and execute virtual currency purchases (Minecoins).
- Resolve creator lists and offer details when Marketplace integration is enabled.
- Inspect balances and entitlements for the signed‑in MC session.
- Utility route to decode tokens (JWS/JWE/opaque) during development.
The service is stateless and proxy/CDN friendly. It uses keep‑alive HTTP agents, strict input validation, standardized error shapes, and colorized request logs with correlation IDs.
Repository: https://github.com/Daniel-Ric/PlayFab-Purchase-Service
- 🔐 JWT authentication middleware for all business routes.
- 🧰 CORS allow‑list, Helmet hardening, compression, and request rate limiting.
- 📄 OpenAPI 3 spec served at
/openapi.jsonand Swagger UI at/api-docs(toggle). - 🔗 PlayFab → MC token exchange helper (via PlayFab SessionTicket).
- 🪙 Purchases: quote price and perform Minecoin purchase with resilient error mapping.
- 🧾 Inventory: read virtual currency balances and entitlements.
- 🧑🎨 Marketplace: list creators, fetch creator summary and offer details (optional integration).
- 🧪 Debug: decode common auth token formats quickly during dev.
- 📈 Pretty logs (or JSON) with timings, method badges, and request IDs.
# 1) Clone & install
git clone https://github.com/Daniel-Ric/PlayFab-Purchase-Service
cd PlayFab-Purchase-Service
npm ci
# 2) Configure environment
cp .env.example .env
# IMPORTANT: set JWT_SECRET (>= 16 chars), and optionally MARKETPLACE/XLink toggles
# 3) Start (development)
NODE_ENV=development node src/server.js
# 4) Production
NODE_ENV=production LOG_PRETTY=false node src/server.jsDefault base URL: http://localhost:8090
The process validates env vars at boot using Joi (
src/config/env.js). Missing/invalid values cause startup failure.
| Variable | Default | Notes |
|---|---|---|
PORT |
8090 |
HTTP port |
NODE_ENV |
development |
development |
JWT_SECRET |
— (required) | ≥ 16 chars; used to sign/verify client JWTs |
TRUST_PROXY |
"loopback" |
Express trust proxy setting (boolean or string) |
CORS_ORIGIN |
* |
Comma‑separated allow‑list; * allows all |
HTTP_TIMEOUT_MS |
15000 |
Axios timeout for upstreams |
LOG_PRETTY |
true in dev |
Colorized vs compact logs |
| Variable | Default | Notes |
|---|---|---|
MC_GAME_VERSION |
1.21.62 |
Passed when minting MC token |
MC_PLATFORM |
Windows10 |
Device platform string |
PLAYFAB_TITLE_ID |
20ca2 |
Used for PlayFab endpoints and payload tags |
ACCEPT_LANGUAGE |
en-US |
Forwarded where applicable |
| Variable | Default | Notes |
|---|---|---|
SWAGGER_ENABLED |
true |
Serve /api-docs and /openapi.json |
SWAGGER_SERVER_URL |
— | Override server URL in the spec |
| Variable | Default | Notes |
|---|---|---|
ENABLE_MARKETPLACE_API |
false |
Enable calls to the external Marketplace API |
MARKETPLACE_API_BASE |
"" |
Base URL of Marketplace API |
ENABLE_XLINK_API |
false |
Allow xLink-issued tokens as fallback |
XLINK_API_BASE |
"" |
Base URL for xLink (if used) |
Client ──► Bearer JWT ─┐
│ ┌────────────────────┐
Headers (MC/XLink/PF) ─┼────────► │ Express API │
│ │ • Routes │
│ │ • Middleware │
│ └─────────┬──────────┘
│ │
│ ▼
│ ┌────────────────────┐
│ │ Services │
│ │ • minecraft │
│ │ • purchase │
│ │ • marketplace │
│ └─────────┬──────────┘
│ │
│ ▼
│ ┌────────────────────┐
│ │ utils/http + axios │──► MC / PlayFab / Marketplace
│ └────────────────────┘
- Keep‑alive agents, bounded redirects, and strict
validateStatus. - Per‑request correlation ID via
X-Request-Id(generated if missing) printed with latency.
- Auth: All business routes require
Authorization: Bearer <jwt>signed withJWT_SECRET. - Input validation:
Joischemas at route boundaries; consistent error model. - CORS: allow‑list using
CORS_ORIGIN(supports*). - Helmet: baseline headers; CSP disabled for Swagger UI compatibility.
- Compression: gzip compression enabled.
Obtain a client JWT from your own identity system (this service only verifies the JWT using JWT_SECRET). Send it on each call:
Authorization: Bearer <jwt>
To act on behalf of a Minecraft player, provide either:
x-mc-token: <Minecraft authorization header>(preferred), orx-playfab-session: <PlayFab SessionTicket>→ the service exchanges it for an MC token.
Optional Marketplace headers (when enabled):
x-marketplace-token: <bearer>orx-xlink-token: <bearer>
Content-Type: application/json; charset=utf-8- All responses are JSON; errors are standardized.
- Correlation ID echoed as
X-Request-Id.
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /healthz |
Liveness | ❌ |
| GET | /readyz |
Readiness | ❌ |
| GET | /openapi.json |
OpenAPI 3 schema | ❌ |
| GET | /api-docs |
Swagger UI (toggle) | ❌ |
| Method | Path | Description |
|---|---|---|
| GET | /marketplace/creators |
Map of creator display names → IDs (from store config) |
| GET | /marketplace/creator/summary?creator=<id> |
Condensed list of a creator’s offers (Marketplace/xLink token optional) |
| GET | /marketplace/offer/details?offerId=<id> |
Offer details including price/display properties |
| Method | Path | Description | |
|---|---|---|---|
| GET | /inventory/balances |
Player virtual currency balances | |
| GET | `/inventory/entitlements?includeReceipt=true | false` | Player entitlements (optionally with receipts) |
| Method | Path | Description |
|---|---|---|
| POST | /purchase/quote |
Resolve price/metadata for an offer before buying |
| POST | /purchase/virtual |
Execute a Minecoin purchase for the offer |
| Method | Path | Description |
|---|---|---|
| POST | /debug/decode-token |
Decode one token or a map of tokens |
See OpenAPI paths for the authoritative schema.
No auth. Basic service probes.
Headers: authorization (JWT), x-mc-token (required). Returns a dictionary { displayName: id }.
Headers: authorization (JWT). Requires ENABLE_MARKETPLACE_API=true and MARKETPLACE_API_BASE. Optional x-marketplace-token or x-xlink-token.
Same integration rules as above. Returns offer metadata (price, display properties, etc.).
Headers: authorization (JWT), x-mc-token (required). Returns Minecoin and other virtual currency balances.
Headers: authorization (JWT), x-mc-token (required). Returns { count, entitlements: [] } when called via /inventory; purchase routes return the raw upstream payload.
Headers: authorization (JWT) and either x-mc-token or x-playfab-session.
Body:
{ "offerId": "<id>", "price": 123 }- When Marketplace integration is enabled, the service fetches details and picks the authoritative price.
- When disabled, you must pass
price(> 0).
Response:
{ "offerId": "...", "price": 123, "details": { /* offer detail or {offerId} */ } }Headers: authorization (JWT) and either x-mc-token or x-playfab-session.
Body:
{ "offerId": "<id>", "price": 123, "xuid": "<optional>" }Response (example):
{ "correlationId": "...", "deviceSessionId": "...", "seq": 42, "transaction": { /* upstream */ } }BASE=http://localhost:8090
TOKEN="<jwt>"
MC="<minecraft-auth-header>"
ST="<playfab-session-ticket>"
# Creators map
curl -sS "$BASE/marketplace/creators" \
-H "Authorization: Bearer $TOKEN" \
-H "x-mc-token: $MC"
# Quote an offer (MC token or PlayFab session)
curl -sS -X POST "$BASE/purchase/quote" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "x-mc-token: $MC" \
-d '{"offerId":"<offerId>"}'
# Execute a virtual purchase
curl -sS -X POST "$BASE/purchase/virtual" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "x-playfab-session: $ST" \
-d '{"offerId":"<offerId>","price":1230}'
# Balances
curl -sS "$BASE/inventory/balances" \
-H "Authorization: Bearer $TOKEN" -H "x-mc-token: $MC"
# Entitlements (with receipts)
curl -sS "$BASE/inventory/entitlements?includeReceipt=true" \
-H "Authorization: Bearer $TOKEN" -H "x-mc-token: $MC"
# Debug: decode multiple tokens
curl -sS -X POST "$BASE/debug/decode-token" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"tokens": {"mc":"'$MC'","pf":"'$ST'"}}'- Global:
600 req/minper instance (express-rate-limit). - Purchase limiter: included (
src/middleware/rateLimit.js) for optional per‑route use (window=60s,max=20).
429 responses use a friendly JSON body:
{ "error": { "code": "TOO_MANY_REQUESTS", "message": "Too many purchase requests" } }- Spec:
GET /openapi.json(always available whenSWAGGER_ENABLED=true). - UI:
GET /api-docs(Swagger UI). - Global security scheme:
BearerAuth(JWT).
Request logger prints a single line on response finish:
HH:MM:SS [OK|WARN|ERR] <METHOD> <url> <status> <ms> #<id>
- Pretty mode (
LOG_PRETTY=true): color badges via chalk. - Compact mode: JSON‑like plain text.
- Each response includes
X-Request-Id. Providex-correlation-idorx-request-idto propagate.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 8090
CMD ["node","src/server.js"]version: "3.8"
services:
purchase:
build: .
ports: ["8090:8090"]
environment:
PORT: 8090
JWT_SECRET: ${JWT_SECRET}
SWAGGER_ENABLED: "true"
LOG_PRETTY: "false"
ENABLE_MARKETPLACE_API: "false"
restart: unless-stoppedapiVersion: apps/v1
kind: Deployment
metadata: { name: playfab-purchase }
spec:
replicas: 2
selector: { matchLabels: { app: playfab-purchase } }
template:
metadata: { labels: { app: playfab-purchase } }
spec:
containers:
- name: api
image: ghcr.io/your-org/playfab-purchase:latest
ports: [{ containerPort: 8090 }]
envFrom: [{ secretRef: { name: purchase-secrets } }]
readinessProbe: { httpGet: { path: "/readyz", port: 8090 }, initialDelaySeconds: 5 }
livenessProbe: { httpGet: { path: "/healthz", port: 8090 }, initialDelaySeconds: 10 }
---
apiVersion: v1
kind: Service
metadata: { name: playfab-purchase }
spec:
selector: { app: playfab-purchase }
ports: [{ port: 80, targetPort: 8090 }]server {
listen 80;
server_name purchase.example.com;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}- Propagate
X-Request-Idacross hops; logs include the short id suffix. - Place a CDN/edge in front if desired; responses are small and cacheable where appropriate.
- Ensure upstream domains are reachable from your network; timeouts are 15s by default.
- 401/403: Missing/invalid JWT; ensure clients send
Authorization: Bearer <jwt>. - 400: Missing headers (
x-mc-tokenorx-playfab-session), invalid payload fields. - 500: Upstream errors; the response includes
{ error: { code, message, details? } }. - Marketplace disabled: Calls to
/marketplace/creator/summaryor/marketplace/offer/detailsreturn 500 withMarketplace API disabledunless toggled on.
Do I need both MC token and PlayFab session?
No. Provide either x-mc-token or x-playfab-session. If you send the SessionTicket, the service mints an MC token for you.
Where does price come from?
If Marketplace integration is enabled, price is resolved from offer details. Otherwise you must provide price when quoting/purchasing.
What if the player already owns the item?
Purchase will fail with AlreadyOwned mapped to a 500 with code: "AlreadyOwned" in details.
How are errors shaped?
{
"error": {
"code": "BAD_REQUEST | UNAUTHORIZED | FORBIDDEN | NOT_FOUND | INTERNAL | HTTP_4xx/5xx",
"message": "Human readable",
"details": { /* optional */ },
"stack": "... (non‑prod only)"
}
}## [1.2.0] - 2025-11-11
### Added
- Initial public release of PlayFab-Purchase Service
- Swagger UI and OpenAPI spec
- Marketplace creator/offer integrations (feature‑flagged)
- Token decoder utility
### Changed
- —
### Fixed
- —- Fork and create a feature branch.
- Add tests where applicable.
- Keep code style consistent and small modules.
- Update README/OpenAPI when behavior changes.
- Open a PR with clear description and logs/screenshots when relevant.
This project