Smart provider routing engine for the Paycrest /markets API
Project description
paycrest-router
Smart provider routing engine for the Paycrest /markets API.
Given a market order book, selects the best provider for a given corridor (token / fiat / network / side / amount) by scoring candidates on rate × reliability.
Install
pip install paycrest-router
# or from source:
pip install -e ".[dev]"
Library usage
from decimal import Decimal
from paycrest_router import fetch_book, select
# 1. Fetch the order book — no API key needed, /markets is public
book = fetch_book()
# 2. Pick the best provider for your corridor
result = select(
book,
side="sell", # "sell" = offramp (crypto → fiat), "buy" = onramp (fiat → crypto)
token="USDT", # "USDT" or "USDC"
fiat="NGN", # "NGN", "KES", "GHS", "XOF"
network="base", # "base", "arbitrum-one", "bnb-smart-chain", "polygon", "ethereum", "celo", "lisk"
token_amount=Decimal("100"),
)
# 3. Use the result in your Paycrest order payload
if result:
provider_id = result.candidate.provider_id # e.g. "gFaEppVt"
rate = result.candidate.rate # e.g. Decimal("1370.01")
success_pct = result.candidate.success_pct # e.g. Decimal("98.12")
paycrest_payload = {
"amount": "100",
"token": "USDT",
"network": "base",
"destination": {
"currency": "NGN",
"providerId": provider_id, # ← this is what you got from select()
"recipient": { ... },
},
}
else:
# No provider passed the eligibility gates.
# Omit providerId entirely — Paycrest will auto-route.
provider_id = None
Error handling
fetch_book() raises PaycrestFetchError if the API is unreachable. select() never raises — it returns None on any failure. The safe pattern:
from paycrest_router import fetch_book, select, PaycrestFetchError
try:
book = fetch_book()
except PaycrestFetchError as e:
# Network down or Paycrest API error — fall back to auto-routing
book = None
provider_id = None
if book:
result = select(book, side="sell", token="USDT", fiat="NGN",
network="base", token_amount=Decimal("100"))
if result:
provider_id = result.candidate.provider_id
# If provider_id is None, just omit it from your Paycrest payload
Async usage
from decimal import Decimal
from paycrest_router import fetch_book_async, select
book = await fetch_book_async()
result = select(book, side="sell", token="USDC", fiat="KES",
network="arbitrum-one", token_amount=Decimal("500"))
Custom config
from paycrest_router import RoutingConfig, select
config = RoutingConfig(
min_success_pct=95.0, # only trust providers with 95%+ fill rate
liquidity_buffer=1.2, # provider must hold 20% more than the payout
success_exponent=2.0, # weight reliability more heavily vs. rate
provider_denylist=["kVMyxKfB"], # never pin these providers
)
result = select(book, side="sell", token="USDT", fiat="NGN", network="base",
token_amount=Decimal("100"), config=config)
Low-level access
from paycrest_router import parse_book, filter_eligible, rank
candidates = parse_book(book) # list[RouteCandidate]
eligible = filter_eligible(candidates, side="sell", token="USDT",
fiat="NGN", network="base", token_amount=Decimal("100"))
ranked = rank(eligible, side="sell")
best = ranked[0] if ranked else None
CLI
# Find the best provider
paycrest-router select --side sell --token USDT --fiat NGN --network base --amount 100
# See all eligible ranked candidates for a corridor
paycrest-router inspect --side sell --token USDC --fiat NGN --network base --amount 500
# Dump the raw order book (useful for building test fixtures)
paycrest-router book
select output
{
"selected": true,
"provider_id": "gFaEppVt",
"side": "sell",
"token": "USDT",
"fiat": "NGN",
"network": "base",
"rate": "1370.01",
"rate_type": "floating",
"success_pct": "98.12",
"min_amount": "0.5",
"max_amount": "5000",
"balance": "3667499",
"balance_ccy": "NGN",
"balance_usd": "2648.01",
"settled": 2769
}
When no provider qualifies:
{"selected": false, "provider_id": null, "reason": "no_eligible"}
When selected is false, omit providerId from your Paycrest order.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success — including when selected is false |
1 |
Bad arguments |
2 |
Network / API fetch error |
All select flags
--side sell|buy
--token USDT|USDC
--network base|arbitrum-one|bnb-smart-chain|polygon|ethereum|celo|lisk
--fiat NGN|KES|GHS|XOF
--amount 100 token amount (sell side)
--fiat-amount 140000 fiat amount (buy side alternative)
--min-success-pct default 90.0
--liquidity-buffer default 1.1
--success-exponent default 1.0
--denylist "a,b" comma-separated provider IDs to exclude
--base-url default https://api.paycrest.io/v2
--timeout default 10.0 seconds
--retries default 2
--verbose enable debug logging
How the algorithm works
Selection runs in two phases: hard gates that disqualify providers, then a scoring formula that ranks the survivors.
Phase 1 — Hard gates (all must pass)
Given your trade (side, token, fiat, network, amount Q):
| Gate | Condition |
|---|---|
| Corridor match | side = side ∧ token = token ∧ network = network ∧ fiat = fiat |
| Amount range | min ≤ Q ≤ max |
| Liquidity (sell) | balance ≥ (Q × rate) × β |
| Liquidity (buy) | balance ≥ Q × β |
| Denomination sanity | sell: balance_ccy = fiat / buy: balance_ccy = token |
| Success rate floor | success_pct ≥ φ (null = excluded) |
| Denylist | provider_id ∉ denylist |
β = liquidity_buffer (default 1.1 — 10% headroom above the payout amount).
φ = min_success_pct (default 90.0%).
For buy side where only fiat amount F is known, token amount is derived per provider: Q = F / rate.
Phase 2 — Scoring (expected value)
Survivors are ranked by expected value — rate weighted by the probability the provider actually fills the order:
Sell (offramp) — maximise fiat received:
score = rate × (success_pct / 100)^k
Buy (onramp) — minimise fiat spent per token:
score = (success_pct / 100)^k / rate
k = success_exponent (default 1.0) controls the rate vs reliability trade-off:
| k | Effect |
|---|---|
| 0 | Ignore reliability entirely — pure rate |
| 1 | Linear penalty on failure probability (default, balanced) |
| 2 | Quadratic penalty — strongly prefer reliable providers |
| → ∞ | Only 100% providers score non-zero |
Concrete example at k=1:
| Provider | Rate | success_pct | Score (sell) |
|---|---|---|---|
| ProvA | 1359 | 99.38% | 1359 × 0.9938 = 1350.6 ← wins |
| ProvB | 1379 | 97.58% | 1379 × 0.9758 = 1345.5 |
ProvA wins despite the lower rate — the reliability gap costs ProvB more than the rate advantage gains it.
At k=0.1 the reliability term is almost completely flattened (90% → 0.9895, 99% → 0.9990 — a 0.1% difference), so score ≈ rate. This causes the highest-rate provider to always win regardless of reliability or settlement speed.
Tie-breaking
Primary sort key is score. Ties broken by balance_usd descending, then settled descending — deeper liquidity and more lifetime orders win.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paycrest_router-0.1.0.tar.gz.
File metadata
- Download URL: paycrest_router-0.1.0.tar.gz
- Upload date:
- Size: 21.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0244867be51b7cc3913e90a712771732ad427c757cc51f172c93a453bb5ca12b
|
|
| MD5 |
a84892debf401ac3af3670f36b6f85c3
|
|
| BLAKE2b-256 |
4ab66388e5ad10705fd51ed4dbf2da3585be13a9e7f0e64db532d0e2c180a970
|
File details
Details for the file paycrest_router-0.1.0-py3-none-any.whl.
File metadata
- Download URL: paycrest_router-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14eac1d024e0d45b1ad96402f07159d412616dd13d7dc34c7cc051a9c7d53461
|
|
| MD5 |
1dd541922ab6b310dd9412d009e78c43
|
|
| BLAKE2b-256 |
4dc76786274da1d18a94311ec3832d31592fd8faa25ca7275f49c598b447929e
|