Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

paycrest_router-0.1.0.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

paycrest_router-0.1.0-py3-none-any.whl (10.7 kB view details)

Uploaded Python 3

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

Hashes for paycrest_router-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0244867be51b7cc3913e90a712771732ad427c757cc51f172c93a453bb5ca12b
MD5 a84892debf401ac3af3670f36b6f85c3
BLAKE2b-256 4ab66388e5ad10705fd51ed4dbf2da3585be13a9e7f0e64db532d0e2c180a970

See more details on using hashes here.

File details

Details for the file paycrest_router-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for paycrest_router-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 14eac1d024e0d45b1ad96402f07159d412616dd13d7dc34c7cc051a9c7d53461
MD5 1dd541922ab6b310dd9412d009e78c43
BLAKE2b-256 4dc76786274da1d18a94311ec3832d31592fd8faa25ca7275f49c598b447929e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page