Self-hosted self-custody XMR crypto payment gateway in Python
Find a file
2026-08-28 21:23:26 +02:00
caishenpay fix: object of type Decimal is not JSON serializable 2026-08-28 21:23:26 +02:00
tests Modify import package name from src to caishenpay 2026-08-12 20:32:30 +02:00
caishenpay.py Move app creation to wsgi 2026-08-12 22:45:02 +02:00
caishenpay_wsgi.py Move app creation to wsgi 2026-08-12 22:45:02 +02:00
config.yaml Add blank file for scaffolding 2026-08-06 22:39:24 +02:00
config.yaml.example Change rpc port to 18088 to match JSONRpcWallet defaults 2026-08-08 13:33:51 +02:00
LICENSE Add license 2026-08-07 22:37:23 +02:00
README.md update api example use 2026-08-28 21:19:15 +02:00
requirements.txt remove invalid version requirement 2026-08-28 20:50:30 +02:00

Caishen Pay

Python crypto payment gateway for processing XMR transactions.

Currently only supports single merchant private use as the signing encryption key is global. Multi-tenant support is in the works.

Requirements

  • monero wallet generated
  • monero-wallet-rpc
  • Python 3.9+
  • gunicorn
  • config.yaml (copy config.yaml.example)
  • CAISHENPAY_API_KEY and CAISHENPAY_ENCRYPTION_SECRET in the environment

Usage

gunicorn --workers 4 --bind 0.0.0.0:8000 caishenpay_wsgi:app

Invoice status polling background task starts inside create_app(). Every gunicorn worker tries to start it but the FileLock (<tempdir>/caishenpay-ispw.lock) lets only a single worker run the poller at a time. If that worker dies, the arbiter spawns a replacement that runs create_app() again and takes the lock. If no worker holds the lock, invoices stay pending.

Do not use preload_app as the lock must be acquired after fork in each worker.

Authentication

All /v1 routes require HTTP Bearer auth. Send the value of CAISHENPAY_API_KEY:

Authorization: Bearer <CAISHENPAY_API_KEY>

The scheme name is case-insensitive (Bearer, bearer, BEARER). The token is compared exactly; extra spaces around the key are not stripped. Missing header, wrong scheme, or wrong key all return the same 401:

{
  "error": "Unauthorized"
}

Example Usage

1. Create Invoice (POST /v1/invoices)

Request (Success)

curl -X POST http://localhost:8000/v1/invoices \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "0.150000000000",
    "currency": "XMR",
    "external_id": "order-10023",
    "callback_url": "https://merchant.example.com/api/webhook"
  }'

Response (201 Created)

{
  "invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
  "external_id": "order-10023",
  "deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
  "deposit_amount": "0.150000000000",
  "deposit_currency": "XMR",
  "seen_amount": "0",
  "status": "PENDING",
  "created_at": "2026-08-16T12:00:00Z",
  "expires_at": "2026-08-16T13:00:00Z",
  "finalized_at": null,
  "callback_url": "https://merchant.example.com/api/webhook"
}

Request (Invalid Payload / Validation Error)

curl -X POST http://localhost:8000/v1/invoices \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "-1.0",
    "currency": "INVALID"
  }'

Response (400 Bad Request)

{
  "errors": {
    "amount": [
      "Must be greater than 0."
    ],
    "currency": [
      "Must be one of: XMR."
    ]
  }
}

Response (503 Service Unavailable)

{
  "error": "Wallet backend unavailable"
}

2. Lookup Single Invoice by ID Path (GET /v1/invoices/<invoice_id>)

Request (Success)

curl "http://localhost:8000/v1/invoices/9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (200 OK)

{
  "invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
  "external_id": "order-10023",
  "deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
  "deposit_amount": "0.150000000000",
  "deposit_currency": "XMR",
  "seen_amount": "0.150000000000",
  "status": "COMPLETED",
  "created_at": "2026-08-16T12:00:00Z",
  "expires_at": "2026-08-16T13:00:00Z",
  "finalized_at": "2026-08-16T12:15:30Z",
  "callback_url": "https://merchant.example.com/api/webhook"
}

Request (Invoice Not Found)

curl "http://localhost:8000/v1/invoices/nonexistent-id" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (404 Not Found)

{
  "error": "Invoice not found"
}

3. Query Invoices or List All (GET /v1/invoices)

Request (List All Invoices)

curl "http://localhost:8000/v1/invoices" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (200 OK)

{
  "invoices": [
    {
      "invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
      "external_id": "order-10023",
      "deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
      "deposit_amount": "0.150000000000",
      "deposit_currency": "XMR",
      "seen_amount": "0.150000000000",
      "status": "COMPLETED",
      "created_at": "2026-08-16T12:00:00Z",
      "expires_at": "2026-08-16T13:00:00Z",
      "finalized_at": "2026-08-16T12:15:30Z",
      "callback_url": "https://merchant.example.com/api/webhook"
    }
  ]
}

Request (Query by invoice_id parameter)

curl "http://localhost:8000/v1/invoices?invoice_id=9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Request (Query by external_id parameter)

curl "http://localhost:8000/v1/invoices?external_id=order-10023" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Request (Conflicting Parameters Error)

curl "http://localhost:8000/v1/invoices?invoice_id=9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d&external_id=order-10023" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (400 Bad Request)

{
  "error": "Provide only one of invoice_id or external_id"
}

4. Check Balance (GET /v1/balance)

Request (Total Balance across all configured wallets)

curl "http://localhost:8000/v1/balance" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (200 OK)

{
  "balance": "12.450000000000"
}

Request (Balance for specific Currency)

curl "http://localhost:8000/v1/balance?currency=XMR" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (200 OK)

{
  "balance": "12.450000000000"
}

Request (Deposit Address Lookup without Currency - Error)

curl "http://localhost:8000/v1/balance?deposit_address=888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (400 Bad Request)

{
  "error": "Currency must be provided for address lookups"
}

Request (Unsupported Currency)

curl "http://localhost:8000/v1/balance?currency=DOGE" \
  -H "Authorization: Bearer $CAISHENPAY_API_KEY"

Response (400 Bad Request)

{
  "error": "Unsupported currency"
}

Callbacks

Pass callback_url when creating an invoice. The poller POSTs JSON to that URL once, when the invoice reaches a final status:

Status When
COMPLETED Full deposit_amount confirmed
EXPIRED Payment window closed with nothing seen
UNDERPAID Payment window closed and confirmed amount is still short

No callback for PENDING, DETECTED, or CONFIRMING. No callback if callback_url was omitted.

The body is the same payload as GET /v1/invoices. Your endpoint must return HTTP 200. Delivery is a single POST; failed deliveries are not retried yet.

Example (POST to callback_url)

{
  "invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
  "external_id": "order-10023",
  "deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
  "deposit_amount": "0.150000000000",
  "deposit_currency": "XMR",
  "seen_amount": "0.150000000000",
  "status": "COMPLETED",
  "created_at": "2026-08-16T12:00:00Z",
  "expires_at": "2026-08-16T13:00:00Z",
  "finalized_at": "2026-08-16T12:15:30Z",
  "callback_url": "https://merchant.example.com/api/webhook"
}

AI Usage

Core logic and API design written by me. AI tools were utilized for generating unit tests, acting as a security advisor during the design phase, and scaffolding documentation.