Initial commit — Performance West telecom compliance platform

Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-04-27 06:54:22 -05:00
commit f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions

View file

@ -0,0 +1,95 @@
"""Crypto offramp module.
Converts coins received by SHKeeper into USD at RelayFi bank via
Coinbase Prime sole provider per plan. See
/home/justin/.claude/plans/swirling-napping-sonnet.md.
Exports:
Quote, ExecutionResult, ProviderStatus dataclasses
Rail literal type alias
OfframpError, InsufficientBalanceError, KycHoldError exception hierarchy
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from typing import Literal, Optional
Rail = Literal["ach", "wire", "rtp"]
# Supported coins (XMR intentionally excluded — Coinbase Prime doesn't offramp it).
SUPPORTED_COINS: set[str] = {
"BTC", "ETH", "MATIC", "LTC", "TRX", "BNB", "DOGE", "USDC",
}
@dataclass
class Quote:
"""Pre-execute price quote from the offramp provider."""
from_coin: str
to_currency: str = "USD"
amount_coin: Decimal = Decimal("0")
estimated_usd_cents: int = 0
fx_rate_usd: Decimal = Decimal("0") # 1 coin = N USD
fee_usd_cents: int = 0 # provider fee estimate
valid_until: Optional[datetime] = None
quote_id: Optional[str] = None # provider-side quote handle, if any
@dataclass
class ExecutionResult:
"""Outcome of calling `execute()` — the sell + wire request was accepted."""
provider_ref: str # Prime order id / transfer id
accepted_usd_cents: int # what the provider promised to settle
fill_price_usd: Decimal # actual coin→USD price achieved
state: Literal["submitted", "partial", "completed", "failed"]
rail: Rail
memo: Optional[str] = None # e.g., "PW-ORDER-FO-2026-0042"
raw: dict = field(default_factory=dict) # provider-native response for debugging
@dataclass
class ProviderStatus:
"""Current state of a previously-executed offramp."""
provider_ref: str
state: Literal[
"submitted", "selling", "selling_complete", "wiring", "completed",
"failed", "held",
]
settled_usd_cents: int = 0
fill_price_usd: Optional[Decimal] = None
wire_tx_id: Optional[str] = None
error_message: Optional[str] = None
raw: dict = field(default_factory=dict)
# ── Exception hierarchy ─────────────────────────────────────────────────
class OfframpError(Exception):
"""Base for all offramp errors."""
class InsufficientBalanceError(OfframpError):
"""Exchange side doesn't have enough of the coin credited yet."""
class KycHoldError(OfframpError):
"""Exchange paused our withdrawal for AML/KYC review — admin must resolve."""
class SlippageExceededError(OfframpError):
"""Realized fill price was outside MAX_SLIPPAGE_BPS of quote."""
class ProviderDegradedError(OfframpError):
"""Repeated 5xx/429 — circuit breaker tripped."""
__all__ = [
"Rail", "SUPPORTED_COINS",
"Quote", "ExecutionResult", "ProviderStatus",
"OfframpError", "InsufficientBalanceError", "KycHoldError",
"SlippageExceededError", "ProviderDegradedError",
]

View file

@ -0,0 +1,423 @@
"""Bridge (by Stripe) offramp provider — sole provider per plan.
Docs: https://apidocs.bridge.xyz/
Auth: REST with `Api-Key` header + `Idempotency-Key` header on POSTs.
Bridge accepts crypto at a deposit address and settles USD to a
pre-registered external bank account via RTP / FedNow / wire / ACH.
Unlike Coinbase Prime (which is a two-step: sell on exchange, then wire
withdrawal), Bridge is a single-transfer model: create a transfer, send
coins to its deposit address, and Bridge handles the conversion + payout
to the destination bank.
Env vars:
BRIDGE_API_KEY Bridge API key
BRIDGE_API_URL default https://api.bridge.xyz
BRIDGE_RELAY_EXTERNAL_ACCOUNT_ID pre-registered RelayFi account id
BRIDGE_DEVELOPER_FEE_USD optional platform fee (defaults 0)
Flow:
1. prepare_transfer(coin, target_usd_cents, rail, memo, idempotency_key)
POST /v0/transfers returns {transfer_id, deposit_address}
2. SHKeeper payout to deposit_address
3. Bridge auto-detects deposit, converts, wires USD to Relay via RTP
4. status(transfer_id) poll until state='payment_submitted' | 'completed'
Supported coins (BRIDGE_ACCEPTS restricted to what Bridge takes):
USDC / USDT / BTC / ETH / SOL (and more) drops XMR + MATIC/TRX/BNB/DOGE.
See `accepts_coins` below. If the SHKeeper webhook delivers an
unsupported coin, the orchestrator flips the job to state='manual'.
"""
from __future__ import annotations
import json
import logging
import os
import time
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Optional
import httpx
from . import (
ExecutionResult,
KycHoldError,
OfframpError,
ProviderDegradedError,
ProviderStatus,
Quote,
Rail,
)
logger = logging.getLogger(__name__)
BRIDGE_URL = os.environ.get("BRIDGE_API_URL", "https://api.bridge.xyz")
# Coins Bridge takes at source. Verify against the live API at integration
# time — this list reflects Bridge's publicly supported asset menu as of
# our integration date. Unsupported coins force manual handling.
BRIDGE_ACCEPTS_COINS: set[str] = {"USDC", "USDT", "BTC", "ETH"}
# Coin → Bridge "currency" slug + default "payment_rail" (source chain)
_COIN_TO_BRIDGE: dict[str, dict[str, str]] = {
"USDC": {"currency": "usdc", "payment_rail": "ethereum"},
"USDT": {"currency": "usdt", "payment_rail": "ethereum"},
"BTC": {"currency": "btc", "payment_rail": "bitcoin"},
"ETH": {"currency": "eth", "payment_rail": "ethereum"},
}
# ── Circuit breaker (module-level) ──────────────────────────────────────
class _CircuitBreaker:
"""Trips if ≥3 provider errors in rolling 10-min window."""
ERROR_WINDOW_SEC = 600
ERROR_THRESHOLD = 3
def __init__(self):
self._errors: deque[float] = deque()
def record_error(self):
now = time.time()
self._errors.append(now)
cutoff = now - self.ERROR_WINDOW_SEC
while self._errors and self._errors[0] < cutoff:
self._errors.popleft()
def is_tripped(self) -> bool:
now = time.time()
cutoff = now - self.ERROR_WINDOW_SEC
while self._errors and self._errors[0] < cutoff:
self._errors.popleft()
return len(self._errors) >= self.ERROR_THRESHOLD
def reset(self):
self._errors.clear()
_breaker = _CircuitBreaker()
@dataclass
class BridgeTransferPrep:
"""Result of creating a Bridge transfer. The deposit_address is where
SHKeeper should send coins; the transfer_id is what we poll for status."""
transfer_id: str
deposit_address: str
expected_usd_cents: int
source_currency: str
source_rail: str
destination_rail: Rail
raw: dict = field(default_factory=dict)
class BridgeOfframp:
"""Sole offramp provider. Uses Bridge's transfers API for both the
deposit-instructions and settlement in one atomic object.
Note on interface fit: Bridge doesn't split sell + wire into separate
calls like Coinbase Prime did. We expose a new `prepare_transfer()`
method that the orchestrator uses instead of the old
`deposit_address()` + `execute()` split. The legacy methods are
implemented as thin wrappers so the existing test surface still works.
"""
name = "bridge"
accepts_coins = BRIDGE_ACCEPTS_COINS
supports_rails: set[Rail] = {"rtp", "wire", "ach"}
def __init__(
self,
*,
api_key: Optional[str] = None,
api_url: Optional[str] = None,
relay_external_account_id: Optional[str] = None,
developer_fee_usd: Optional[Decimal] = None,
):
self.api_key = api_key or os.environ.get("BRIDGE_API_KEY", "")
self.api_url = api_url or BRIDGE_URL
self.relay_external_account_id = (
relay_external_account_id
or os.environ.get("BRIDGE_RELAY_EXTERNAL_ACCOUNT_ID", "")
)
self.developer_fee_usd = developer_fee_usd or Decimal(
os.environ.get("BRIDGE_DEVELOPER_FEE_USD", "0"),
)
missing = [
k for k, v in [
("api_key", self.api_key),
("relay_external_account_id", self.relay_external_account_id),
] if not v
]
if missing:
logger.warning(
"BridgeOfframp: missing env vars %s — calls will fail until configured.",
missing,
)
# ── Public interface ────────────────────────────────────────────────
async def prepare_transfer(
self,
*,
target_usd_cents: int,
from_coin: str,
rail: Rail = "rtp",
memo: Optional[str] = None,
idempotency_key: str,
) -> BridgeTransferPrep:
"""Create a Bridge transfer.
Idempotent: the Bridge API dedups by Idempotency-Key header, so
calling this repeatedly with the same key returns the same
transfer. Returns the deposit_address to send coins to.
"""
self._guard_breaker()
from_coin = from_coin.upper()
if from_coin not in BRIDGE_ACCEPTS_COINS:
raise OfframpError(
f"coin {from_coin} not supported by Bridge; "
f"supported = {sorted(BRIDGE_ACCEPTS_COINS)}",
)
rail_lower = rail.lower()
if rail_lower not in self.supports_rails:
raise OfframpError(f"invalid rail {rail}")
src_cfg = _COIN_TO_BRIDGE[from_coin]
body = {
"source": {
"currency": src_cfg["currency"],
"payment_rail": src_cfg["payment_rail"],
},
"destination": {
"currency": "usd",
"payment_rail": rail_lower, # rtp / wire / ach
"external_account_id": self.relay_external_account_id,
},
# amount in USD; Bridge converts the equivalent crypto on deposit
"amount": f"{target_usd_cents / 100:.2f}",
}
if memo:
body["destination"]["bank_transfer_memo"] = memo
if self.developer_fee_usd > 0:
body["developer_fee"] = str(self.developer_fee_usd)
resp = await self._request(
"POST", "/v0/transfers",
json_body=body,
idempotency_key=idempotency_key,
)
# Bridge returns the deposit instructions inside source.deposit_instructions
deposit = (resp.get("source") or {}).get("deposit_instructions") or {}
address = deposit.get("address") or ""
if not address:
raise OfframpError(
f"Bridge returned no deposit address for {from_coin}; "
f"response shape changed or account not onboarded",
)
transfer_id = resp.get("id") or resp.get("transfer_id") or ""
if not transfer_id:
raise OfframpError("Bridge returned no transfer id")
return BridgeTransferPrep(
transfer_id=transfer_id,
deposit_address=address,
expected_usd_cents=target_usd_cents,
source_currency=src_cfg["currency"],
source_rail=src_cfg["payment_rail"],
destination_rail=rail,
raw=resp,
)
async def quote(self, amount_coin: Decimal, from_coin: str) -> Quote:
"""Light price hint — Bridge gives an atomic price at transfer
creation, so this is mostly informational. For USDC/USDT it's
always ~1:1."""
self._guard_breaker()
from_coin = from_coin.upper()
if from_coin in ("USDC", "USDT"):
return Quote(
from_coin=from_coin,
amount_coin=amount_coin,
estimated_usd_cents=int(amount_coin * Decimal("100")),
fx_rate_usd=Decimal("1.00"),
fee_usd_cents=0,
valid_until=datetime.utcnow() + timedelta(seconds=60),
)
# For BTC/ETH, use Bridge's pricing endpoint if available; else
# fall back to a public CoinGecko fetch. Keep deterministic for
# tests — for production integration, replace with Bridge's
# /v0/exchange_rates or similar.
try:
rates = await self._request("GET", "/v0/exchange_rates")
pair = f"{from_coin.lower()}-usd"
rate_str = (rates.get("rates") or {}).get(pair, {}).get("rate")
if rate_str:
rate = Decimal(str(rate_str))
gross = amount_coin * rate
return Quote(
from_coin=from_coin,
amount_coin=amount_coin,
estimated_usd_cents=int(gross * Decimal("100")),
fx_rate_usd=rate,
fee_usd_cents=0,
)
except Exception as exc:
logger.debug("bridge.quote rates fetch failed (%s) — falling back", exc)
# Fallback to an approximate market quote (caller can accept 0 rate)
return Quote(
from_coin=from_coin,
amount_coin=amount_coin,
estimated_usd_cents=0,
fx_rate_usd=Decimal("0"),
fee_usd_cents=0,
)
async def status(self, provider_ref: str) -> ProviderStatus:
self._guard_breaker()
data = await self._request("GET", f"/v0/transfers/{provider_ref}")
bridge_state = (data.get("state") or "").lower()
mapped = {
"awaiting_funds": "submitted",
"funds_received": "selling_complete",
"in_review": "held",
"payment_processing": "wiring",
"payment_submitted": "wiring",
"payment_processed": "completed",
"payment_reviewed": "completed",
"completed": "completed",
"refunded": "failed",
"returned": "failed",
"failed": "failed",
"undeliverable": "failed",
}.get(bridge_state, "submitted")
if mapped == "held":
logger.warning(
"Bridge transfer %s in review (KYC/AML hold possible)",
provider_ref,
)
# Final amount received at destination — Bridge's shape:
# {"destination":{"amount":{"value":"299.00"}}} or similar
dest = (data.get("destination") or {}).get("amount") or {}
value_str = dest.get("value") or data.get("amount") or "0"
settled_cents = int(Decimal(str(value_str)) * Decimal("100"))
bank_ref = (
(data.get("destination") or {}).get("bank_transfer_reference")
or data.get("network_reference")
)
return ProviderStatus(
provider_ref=provider_ref,
state=mapped,
settled_usd_cents=settled_cents if mapped == "completed" else 0,
wire_tx_id=bank_ref,
error_message=data.get("error_message") or data.get("refund_reason"),
raw=data,
)
# ── Legacy Protocol surface (compat with the existing OfframpProvider shape) ──
#
# The orchestrator was originally designed around a Coinbase-Prime-style
# two-step flow (deposit_address, then execute). Bridge is one-step —
# prepare_transfer gives you both. For the new crypto_payment_worker
# branch ("if provider_name == 'bridge':") we call prepare_transfer
# directly. These wrapper methods let the existing Protocol callers
# (anything written against CoinbasePrimeOfframp) still function.
async def deposit_address(self, coin: str) -> str:
raise OfframpError(
"BridgeOfframp.deposit_address is not supported — "
"use prepare_transfer() (which returns both the transfer id and address).",
)
async def execute(
self,
*,
amount_coin: Decimal,
from_coin: str,
rail: Rail,
idempotency_key: str,
relay_routing: Optional[str] = None,
relay_account: Optional[str] = None,
memo: Optional[str] = None,
target_usd_cents: Optional[int] = None,
) -> ExecutionResult:
"""Legacy-Protocol wrapper around prepare_transfer. Returns an
ExecutionResult whose provider_ref is the Bridge transfer id."""
if target_usd_cents is None:
raise OfframpError(
"BridgeOfframp.execute requires target_usd_cents (USD-denominated transfer)",
)
prep = await self.prepare_transfer(
target_usd_cents=target_usd_cents,
from_coin=from_coin,
rail=rail,
memo=memo,
idempotency_key=idempotency_key,
)
return ExecutionResult(
provider_ref=prep.transfer_id,
accepted_usd_cents=prep.expected_usd_cents,
fill_price_usd=Decimal("0"), # no explicit fill price pre-settlement
state="submitted",
rail=rail,
memo=memo,
raw={"bridge_prep": prep.raw, "deposit_address": prep.deposit_address},
)
# ── Internals ───────────────────────────────────────────────────────
async def _request(
self,
method: str,
path: str,
*,
params: Optional[dict] = None,
json_body: Optional[dict] = None,
idempotency_key: Optional[str] = None,
) -> dict:
url = f"{self.api_url}{path}"
headers = {
"Api-Key": self.api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
body = json.dumps(json_body, separators=(",", ":")) if json_body else None
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.request(
method, url, headers=headers, params=params, content=body,
)
except httpx.HTTPError as exc:
_breaker.record_error()
raise OfframpError(f"Bridge request failed: {exc}") from exc
if resp.status_code == 429 or resp.status_code >= 500:
_breaker.record_error()
raise OfframpError(
f"Bridge {method} {path}{resp.status_code}: {resp.text[:200]}",
)
if resp.status_code >= 400:
err_text = resp.text[:500]
if "kyc" in err_text.lower() or "aml" in err_text.lower() or "review" in err_text.lower():
raise KycHoldError(err_text)
raise OfframpError(
f"Bridge {method} {path}{resp.status_code}: {err_text}",
)
try:
return resp.json()
except ValueError:
return {}
def _guard_breaker(self):
if _breaker.is_tripped():
raise ProviderDegradedError(
"Bridge breaker tripped — ≥3 errors in last 10min. "
"Parking in manual review per plan.",
)

View file

@ -0,0 +1,159 @@
"""In-memory mock offramp for tests.
Same method surface as CoinbasePrimeOfframp but never hits the network.
Records calls for assertion. State advances deterministically based on
``auto_complete`` (default True) or manually via ``.advance()``.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from typing import Optional
from uuid import uuid4
from . import (
ExecutionResult,
KycHoldError,
OfframpError,
ProviderStatus,
Quote,
Rail,
SUPPORTED_COINS,
)
@dataclass
class _MockTransfer:
provider_ref: str
amount_usd_cents: int
rail: Rail
state: str = "submitted"
memo: Optional[str] = None
class MockOfframp:
name = "mock"
accepts_coins = SUPPORTED_COINS
supports_rails: set[Rail] = {"ach", "wire", "rtp"}
def __init__(self, *, auto_complete: bool = True, kyc_hold_for_coins: Optional[set[str]] = None):
self.auto_complete = auto_complete
self.kyc_hold_for_coins = kyc_hold_for_coins or set()
self.calls: list[tuple[str, dict]] = []
self._transfers: dict[str, _MockTransfer] = {}
# Fixed synthetic prices
self._prices = {
"BTC": Decimal("70000.00"),
"ETH": Decimal("3500.00"),
"MATIC": Decimal("0.80"),
"LTC": Decimal("85.00"),
"TRX": Decimal("0.11"),
"BNB": Decimal("520.00"),
"DOGE": Decimal("0.15"),
"USDC": Decimal("1.00"),
}
async def quote(self, amount_coin: Decimal, from_coin: str) -> Quote:
self.calls.append(("quote", {"amount_coin": amount_coin, "from_coin": from_coin}))
fx = self._prices.get(from_coin.upper(), Decimal("1.00"))
gross = amount_coin * fx
fee = gross * Decimal("0.0008")
return Quote(
from_coin=from_coin.upper(),
amount_coin=amount_coin,
estimated_usd_cents=int((gross - fee) * 100),
fx_rate_usd=fx,
fee_usd_cents=int(fee * 100),
)
async def deposit_address(self, coin: str) -> str:
self.calls.append(("deposit_address", {"coin": coin}))
return f"mock-deposit-{coin.lower()}-{uuid4().hex[:8]}"
async def execute(
self, *,
amount_coin: Decimal, from_coin: str, rail: Rail,
idempotency_key: str,
relay_routing: Optional[str] = None,
relay_account: Optional[str] = None,
memo: Optional[str] = None,
target_usd_cents: Optional[int] = None,
) -> ExecutionResult:
self.calls.append(("execute", {
"amount_coin": amount_coin, "from_coin": from_coin, "rail": rail,
"idempotency_key": idempotency_key, "memo": memo,
"target_usd_cents": target_usd_cents,
}))
if from_coin.upper() in self.kyc_hold_for_coins:
raise KycHoldError(f"mock KYC hold for {from_coin}")
fx = self._prices.get(from_coin.upper(), Decimal("1.00"))
usd_cents = target_usd_cents or int(amount_coin * fx * 100)
ref = f"mock-tx-{uuid4().hex[:12]}"
state = "completed" if self.auto_complete else "submitted"
self._transfers[ref] = _MockTransfer(
provider_ref=ref, amount_usd_cents=usd_cents, rail=rail,
state=state, memo=memo,
)
return ExecutionResult(
provider_ref=ref,
accepted_usd_cents=usd_cents,
fill_price_usd=fx,
state=state,
rail=rail,
memo=memo,
raw={"mock": True},
)
async def status(self, provider_ref: str) -> ProviderStatus:
self.calls.append(("status", {"provider_ref": provider_ref}))
t = self._transfers.get(provider_ref)
if not t:
raise OfframpError(f"mock: no transfer {provider_ref}")
return ProviderStatus(
provider_ref=provider_ref,
state=t.state, # type: ignore[arg-type]
settled_usd_cents=t.amount_usd_cents if t.state == "completed" else 0,
)
# ── Test helpers ────────────────────────────────────────────────────
def advance(self, provider_ref: str, to_state: str):
"""Manually move a transfer forward in state."""
if provider_ref in self._transfers:
self._transfers[provider_ref].state = to_state
# ── Mock SHKeeper client ────────────────────────────────────────────────
@dataclass
class _MockPayout:
withdraw_id: str
tx_hash: Optional[str]
fee_coin: Decimal = Decimal("0")
raw: Optional[dict] = None
class MockSHKeeperClient:
"""In-memory SHKeeperClient substitute for tests — never hits the network."""
def __init__(self, *, default_balances: Optional[dict[str, Decimal]] = None):
self._balances: dict[str, Decimal] = dict(default_balances or {})
self.payouts: list[tuple[str, str, Decimal]] = []
async def payout(self, *, coin: str, destination: str, amount: Decimal):
self.payouts.append((coin, destination, amount))
self._balances[coin] = self._balances.get(coin, Decimal("0")) - amount
return _MockPayout(
withdraw_id=f"mock-shk-{uuid4().hex[:10]}",
tx_hash=f"mock-txhash-{uuid4().hex[:16]}",
)
async def balance(self, coin: str):
from .shkeeper_client import BalanceResult
bal = self._balances.get(coin.upper(), Decimal("0"))
return BalanceResult(balance_coin=bal, balance_usd=bal * Decimal("70000"))
async def withdrawal_status(self, coin: str, withdraw_id: str):
return {"status": "confirmed", "txid": f"mock-tx-{withdraw_id}"}

View file

@ -0,0 +1,127 @@
"""SHKeeper client — thin wrapper over the self-hosted SHKeeper REST API.
SHKeeper (https://github.com/vsys-host/shkeeper.io) is our crypto payment
gateway. It exposes one REST endpoint per coin at
{SHKEEPER_URL}/api/v1/{coin}/...
with bearer auth via X-Shkeeper-Api-Key header.
We use it for two things post-webhook:
1. payout() withdraw coins from the SHKeeper hot wallet to an
external address (either a Coinbase Prime deposit
address for offramp, or the cold wallet for sweeps).
2. balance() read the current hot-wallet balance for sweep sizing.
SHKeeper exposes a /payout endpoint; exact shape varies by coin but
follows the pattern documented in the project's API README. Confirm
shape against the running instance during first real test we stub
with sensible defaults and will iterate.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
import httpx
logger = logging.getLogger(__name__)
SHKEEPER_URL = os.environ.get("SHKEEPER_URL", "http://127.0.0.1:5000")
SHKEEPER_API_KEY = os.environ.get("SHKEEPER_API_KEY", "")
@dataclass
class PayoutResult:
withdraw_id: str
tx_hash: Optional[str]
fee_coin: Decimal = Decimal("0")
raw: dict = None # type: ignore[assignment]
@dataclass
class BalanceResult:
balance_coin: Decimal
balance_usd: Decimal # SHKeeper returns fiat equivalent on balance
updated_at: Optional[str] = None
class SHKeeperError(Exception):
"""Any SHKeeper API failure."""
class SHKeeperClient:
def __init__(
self,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
):
self.base = base_url or SHKEEPER_URL
self.api_key = api_key or SHKEEPER_API_KEY
if not self.api_key:
logger.warning(
"SHKeeperClient: SHKEEPER_API_KEY empty — calls will fail",
)
async def payout(
self,
*, coin: str, destination: str, amount: Decimal,
) -> PayoutResult:
"""POST /api/v1/{coin}/payout — withdraw to an external address.
Request body (per SHKeeper docs):
{"destination": "<addr>", "amount": "<decimal>"}
Response includes a withdraw_id + eventually a tx_hash.
"""
path = f"/api/v1/{coin.lower()}/payout"
body = {
"destination": destination,
"amount": str(amount),
}
resp = await self._request("POST", path, json_body=body)
return PayoutResult(
withdraw_id=str(resp.get("withdraw_id") or resp.get("id") or ""),
tx_hash=resp.get("tx_hash") or resp.get("txid"),
fee_coin=Decimal(str(resp.get("fee") or "0")),
raw=resp,
)
async def balance(self, coin: str) -> BalanceResult:
"""GET /api/v1/{coin}/balance — current hot-wallet balance."""
path = f"/api/v1/{coin.lower()}/balance"
resp = await self._request("GET", path)
return BalanceResult(
balance_coin=Decimal(str(resp.get("balance") or "0")),
balance_usd=Decimal(str(resp.get("balance_usd") or resp.get("fiat_balance") or "0")),
updated_at=resp.get("updated_at"),
)
async def withdrawal_status(self, coin: str, withdraw_id: str) -> dict:
"""GET /api/v1/{coin}/payout/{id} — poll for confirmation."""
path = f"/api/v1/{coin.lower()}/payout/{withdraw_id}"
return await self._request("GET", path)
async def _request(
self, method: str, path: str,
*, json_body: Optional[dict] = None,
) -> dict:
url = f"{self.base}{path}"
headers = {
"X-Shkeeper-Api-Key": self.api_key,
"Content-Type": "application/json",
}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.request(method, url, headers=headers, json=json_body)
except httpx.HTTPError as exc:
raise SHKeeperError(f"SHKeeper request failed: {exc}") from exc
if resp.status_code >= 400:
raise SHKeeperError(
f"SHKeeper {method} {path}{resp.status_code}: {resp.text[:300]}",
)
try:
return resp.json()
except ValueError:
return {}

View file

@ -0,0 +1,474 @@
"""Sizer — compute vendor obligations per order.
Called by ``crypto_payment_worker`` when transitioning received sizing.
Returns the list of vendor_obligations rows to insert plus the total USD
that needs to land at RelayFi (``needed_usd_cents``).
Two categories of obligation:
1. filing_fee paid via Relay debit card via Playwright flow.
Includes a +10% buffer for card-decline retries + slippage.
2. commission paid via Relay ACH 14 days post-delivery. Exact amount.
The sizer reads order data from the relevant table (compliance_orders,
formation_orders, canada_crtc_orders, bundle_orders) + existing
config tables (state_filing_fees, sales_agents) + a small static map
for vendors that don't live in a table (NECA OCN, BC Registry, etc.).
"""
from __future__ import annotations
import json
import logging
import os
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
import psycopg2
import psycopg2.extras
logger = logging.getLogger(__name__)
# ── Static vendor fee map (single source of truth for non-table vendors) ──
#
# These are vendors whose fees are fixed + don't live in a queryable
# table. Keep in sync with the COMPLIANCE_SERVICES catalog in
# api/src/routes/compliance-orders.ts when their pricing changes.
STATIC_VENDOR_FEES_CENTS: dict[str, int] = {
"nwra": 12500, # Northwest Registered Agent $125/yr
"neca_ocn": 55000, # NECA OCN registration $550 standard
"neca_ocn_expedited": 67500, # NECA OCN expedited $675
"bc_registry": 27700, # BC Registry incorporation $277 CAD ≈ rounded
"cores_frn": 0, # FCC CORES FRN — no filing fee
"usac_499_initial": 0, # USAC 499 Initial — no filing fee
"usac_499a": 0, # USAC 499-A — no filing fee (contributions separate)
"fcc_cpni": 0, # FCC CPNI cert — no filing fee
"fcc_rmd": 10000, # FCC RMD — $100 filing fee (effective 2025)
"fcc_bdc": 0, # FCC BDC — no filing fee
"fcc_stir_shaken": 0, # FCC STIR/SHAKEN — no filing fee
"fcc_63_11": 0, # FCC Foreign Carrier Affiliation — no filing fee
"fcc_calea_ssi": 0, # CALEA SSI — no filing fee (internal plan)
"dc_agent": 0, # DC Agent — our DC agent service has no external fee
}
# Default filing-fee buffer in basis points (1000 bps = 10%)
DEFAULT_FILING_BUFFER_BPS = 1000
# Commission payout happens 14 days post-delivery per commission_ledger
# lifecycle (migration 011); reserve the cash at offramp time.
COMMISSION_DUE_DAYS = 14
@dataclass
class VendorObligation:
"""Matches the shape inserted into vendor_obligations."""
order_id: str
order_type: str
obligation_kind: str # 'filing_fee' | 'commission'
vendor: str
vendor_detail: Optional[str]
amount_usd_cents: int
fx_buffer_bps: int
due_by: Optional[datetime] = None
commission_ledger_id: Optional[int] = None
@dataclass
class SizingResult:
obligations: list[VendorObligation]
filing_total_cents: int # raw filing-fee sum, pre-buffer
filing_total_with_buffer_cents: int
commission_total_cents: int
needed_usd_cents: int # what we need at Relay
notes: list[str] = field(default_factory=list)
def _db_connect():
return psycopg2.connect(os.environ.get("DATABASE_URL", ""))
def compute_obligations(
*,
order_id: str,
order_type: str,
conn=None,
) -> SizingResult:
"""Compute vendor obligations for the given order.
``order_type`` is one of: 'compliance' | 'formation' | 'canada_crtc' | 'bundle'.
"""
close_after = False
if conn is None:
conn = _db_connect()
close_after = True
try:
if order_type == "compliance":
return _size_compliance(order_id, conn)
if order_type == "formation":
return _size_formation(order_id, conn)
if order_type == "canada_crtc":
return _size_canada_crtc(order_id, conn)
if order_type == "bundle":
return _size_bundle(order_id, conn)
logger.warning("sizer: unknown order_type=%s for %s", order_type, order_id)
return SizingResult([], 0, 0, 0, 0, ["unknown order_type — zero obligations"])
finally:
if close_after:
conn.close()
# ── Compliance orders (FCC filings + OCN + DC agent + bundles) ──────────
def _size_compliance(order_id: str, conn) -> SizingResult:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"""
SELECT co.*, te.id AS entity_id,
(co.intake_data->>'expedited')::boolean AS expedited
FROM compliance_orders co
LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id
WHERE co.order_number = %s
""",
(order_id,),
)
order = cur.fetchone()
if not order:
return SizingResult([], 0, 0, 0, 0, [f"compliance order {order_id} not found"])
slug = order.get("service_slug") or ""
obligations: list[VendorObligation] = []
# OCN registration has real money to NECA; optional expedited upcharge.
if slug == "ocn-registration":
fee_key = "neca_ocn_expedited" if order.get("expedited") else "neca_ocn"
obligations.append(_filing_vo(
order_id, "compliance", "neca_ocn", None,
STATIC_VENDOR_FEES_CENTS[fee_key],
))
# Any service that includes NRA registered-agent coverage in its bundle.
# Customer opts in via intake_data.include_nwra_ra or similar; read the flag.
if (order.get("intake_data") or {}).get("include_nwra_ra"):
obligations.append(_filing_vo(
order_id, "compliance", "nwra", None,
STATIC_VENDOR_FEES_CENTS["nwra"],
))
# FCC 499-family, CPNI, RMD, BDC, STIR/SHAKEN, 63-11, CALEA SSI, DC agent,
# compliance checkup, CDR analysis, CORES FRN — no vendor filing fee.
# (Our service fee is pure markup; nothing to reserve for vendors.)
# Commission reserve if a sales agent is attached
commission = _compute_commission_obligation(
order_id, "compliance", order, slug, conn,
)
if commission:
obligations.append(commission)
filing = [o for o in obligations if o.obligation_kind == "filing_fee"]
comms = [o for o in obligations if o.obligation_kind == "commission"]
filing_total = sum(o.amount_usd_cents for o in filing)
filing_buf = int(filing_total * (1 + DEFAULT_FILING_BUFFER_BPS / 10000))
comm_total = sum(o.amount_usd_cents for o in comms)
return SizingResult(
obligations=obligations,
filing_total_cents=filing_total,
filing_total_with_buffer_cents=filing_buf,
commission_total_cents=comm_total,
needed_usd_cents=filing_buf + comm_total,
)
# ── Formation orders (LLC / Corp state filings) ─────────────────────────
def _size_formation(order_id: str, conn) -> SizingResult:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT * FROM formation_orders WHERE order_number = %s", (order_id,))
order = cur.fetchone()
if not order:
return SizingResult([], 0, 0, 0, 0, [f"formation order {order_id} not found"])
state = order.get("state_code") or order.get("state")
entity_type = (order.get("entity_type") or "llc").lower()
obligations: list[VendorObligation] = []
if state:
# state_filing_fees UNIT CONVENTIONS per column (confirmed by
# spot-checking 15+ states against real SOS fee schedules):
# llc_formation_fee / corp_formation_fee — CENTS (e.g., 7000 = $70 CA LLC)
# foreign_llc_fee / foreign_corp_fee — CENTS
# expedited_fee — DOLLARS × 10000
# (e.g., 7500000 = $750 CA same-day)
# llc_annual_fee / corp_annual_fee — mixed (some rows are franchise-tax
# amounts; verify per-state before trust)
# name_reservation_fee, franchise_tax_min — DOLLARS × 10000 in most rows
#
# This sizer only reads formation + foreign + expedited and
# normalizes expedited to cents.
formation_col = "corp_formation_fee" if entity_type.startswith("corp") else "llc_formation_fee"
foreign_col = "foreign_corp_fee" if entity_type.startswith("corp") else "foreign_llc_fee"
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
f"""
SELECT {formation_col} AS formation_cents,
{foreign_col} AS foreign_cents,
expedited_fee AS expedited_raw
FROM state_filing_fees
WHERE state_code = %s
LIMIT 1
""",
(state,),
)
fee = cur.fetchone()
if fee:
is_foreign = bool(order.get("foreign_filing") or order.get("is_foreign_entity"))
base_cents = int((fee.get("foreign_cents") or 0) if is_foreign else (fee.get("formation_cents") or 0))
expedited_cents = 0
if order.get("expedited") and fee.get("expedited_raw"):
# Divide by 100 to convert (dollars × 10000) → cents
expedited_cents = int(fee["expedited_raw"]) // 100
total = base_cents + expedited_cents
if total > 0:
obligations.append(_filing_vo(
order_id, "formation", "state_sos", state, total,
))
if order.get("include_ra_service") or order.get("include_registered_agent"):
obligations.append(_filing_vo(
order_id, "formation", "nwra", state,
STATIC_VENDOR_FEES_CENTS["nwra"],
))
commission = _compute_commission_obligation(
order_id, "formation", order, f"formation-{entity_type}", conn,
)
if commission:
obligations.append(commission)
filing = [o for o in obligations if o.obligation_kind == "filing_fee"]
comms = [o for o in obligations if o.obligation_kind == "commission"]
filing_total = sum(o.amount_usd_cents for o in filing)
filing_buf = int(filing_total * (1 + DEFAULT_FILING_BUFFER_BPS / 10000))
comm_total = sum(o.amount_usd_cents for o in comms)
return SizingResult(
obligations=obligations,
filing_total_cents=filing_total,
filing_total_with_buffer_cents=filing_buf,
commission_total_cents=comm_total,
needed_usd_cents=filing_buf + comm_total,
)
# ── Canada CRTC orders ──────────────────────────────────────────────────
def _size_canada_crtc(order_id: str, conn) -> SizingResult:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT * FROM canada_crtc_orders WHERE order_number = %s", (order_id,))
order = cur.fetchone()
if not order:
return SizingResult([], 0, 0, 0, 0, [f"canada_crtc order {order_id} not found"])
obligations: list[VendorObligation] = []
obligations.append(_filing_vo(
order_id, "canada_crtc", "bc_registry", "BC",
STATIC_VENDOR_FEES_CENTS["bc_registry"],
))
if order.get("amb_annual_price_cents"):
obligations.append(_filing_vo(
order_id, "canada_crtc", "amb_mailbox", None,
int(order["amb_annual_price_cents"]),
))
commission = _compute_commission_obligation(
order_id, "canada_crtc", order, "canada_crtc", conn,
)
if commission:
obligations.append(commission)
filing = [o for o in obligations if o.obligation_kind == "filing_fee"]
comms = [o for o in obligations if o.obligation_kind == "commission"]
filing_total = sum(o.amount_usd_cents for o in filing)
filing_buf = int(filing_total * (1 + DEFAULT_FILING_BUFFER_BPS / 10000))
comm_total = sum(o.amount_usd_cents for o in comms)
return SizingResult(
obligations=obligations,
filing_total_cents=filing_total,
filing_total_with_buffer_cents=filing_buf,
commission_total_cents=comm_total,
needed_usd_cents=filing_buf + comm_total,
)
# ── Bundle orders (sum of sub-orders) ───────────────────────────────────
def _size_bundle(order_id: str, conn) -> SizingResult:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT * FROM bundle_orders WHERE order_number = %s", (order_id,))
order = cur.fetchone()
if not order:
return SizingResult([], 0, 0, 0, 0, [f"bundle order {order_id} not found"])
# bundle_orders.child_order_numbers is a TEXT[] of the sub-order numbers
# (verify schema during implementation — may be JSONB with types).
sub_ids = order.get("child_order_numbers") or []
sub_types = order.get("child_order_types") or []
all_obligations: list[VendorObligation] = []
for sub_id, sub_type in zip(sub_ids, sub_types):
sub_res = compute_obligations(order_id=sub_id, order_type=sub_type, conn=conn)
all_obligations.extend(sub_res.obligations)
# Bundle-level commission (override the per-sub commissions if present)
commission = _compute_commission_obligation(
order_id, "bundle", order, "bundle", conn,
)
if commission:
# Drop sub-level commissions; keep only the bundle-level one
all_obligations = [o for o in all_obligations if o.obligation_kind != "commission"]
all_obligations.append(commission)
filing = [o for o in all_obligations if o.obligation_kind == "filing_fee"]
comms = [o for o in all_obligations if o.obligation_kind == "commission"]
filing_total = sum(o.amount_usd_cents for o in filing)
filing_buf = int(filing_total * (1 + DEFAULT_FILING_BUFFER_BPS / 10000))
comm_total = sum(o.amount_usd_cents for o in comms)
return SizingResult(
obligations=all_obligations,
filing_total_cents=filing_total,
filing_total_with_buffer_cents=filing_buf,
commission_total_cents=comm_total,
needed_usd_cents=filing_buf + comm_total,
)
# ── Commission obligation (shared across all order types) ───────────────
def _compute_commission_obligation(
order_id: str, order_type: str, order: dict, slug_for_override: str, conn,
) -> Optional[VendorObligation]:
"""If the order is attributed to a sales agent, reserve their commission."""
agent_id = order.get("sales_agent_id") or order.get("referral_agent_id")
if not agent_id:
return None
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"""
SELECT id, commission_type, commission_default_cents,
commission_pct, commission_overrides, active
FROM sales_agents
WHERE id = %s
""",
(agent_id,),
)
agent = cur.fetchone()
if not agent or not agent.get("active"):
return None
overrides = agent.get("commission_overrides") or {}
if isinstance(overrides, str):
try: overrides = json.loads(overrides)
except (TypeError, ValueError): overrides = {}
override = overrides.get(slug_for_override) or {}
ctype = override.get("type") or agent.get("commission_type", "flat")
amount_cents = 0
if ctype == "flat":
amount_cents = int(
override.get("flat_cents")
or agent.get("commission_default_cents")
or 0
)
elif ctype == "percent":
pct = float(override.get("pct") or agent.get("commission_pct") or 0)
# Use subtotal (pre-surcharge) as the basis; fall back to service_fee_cents
subtotal = int(
order.get("subtotal_cents")
or order.get("service_fee_cents")
or order.get("total_cents")
or 0
)
amount_cents = int(subtotal * pct / 100)
if amount_cents <= 0:
return None
# Due 14 days post-delivery. If we don't know delivery date, estimate
# from created_at + 7 days (typical service turnaround) + 14.
created = order.get("created_at") or datetime.utcnow()
due_by = created + timedelta(days=7 + COMMISSION_DUE_DAYS)
return VendorObligation(
order_id=order_id,
order_type=order_type,
obligation_kind="commission",
vendor="agent_commission",
vendor_detail=str(agent_id),
amount_usd_cents=amount_cents,
fx_buffer_bps=0, # commissions are exact, no buffer
due_by=due_by,
)
# ── Constructors ────────────────────────────────────────────────────────
def _filing_vo(
order_id: str, order_type: str, vendor: str,
vendor_detail: Optional[str], amount_cents: int,
) -> VendorObligation:
return VendorObligation(
order_id=order_id,
order_type=order_type,
obligation_kind="filing_fee",
vendor=vendor,
vendor_detail=vendor_detail,
amount_usd_cents=amount_cents,
fx_buffer_bps=DEFAULT_FILING_BUFFER_BPS,
)
# ── Persistence helpers ─────────────────────────────────────────────────
def persist_obligations(order_id: str, result: SizingResult, conn=None) -> None:
"""Upsert the SizingResult into vendor_obligations.
Idempotent: deletes existing pending rows for this order then re-inserts.
We never overwrite rows already marked reserved/paid/waived.
"""
close_after = False
if conn is None:
conn = _db_connect()
close_after = True
try:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM vendor_obligations "
"WHERE order_id = %s AND status = 'pending'",
(order_id,),
)
for o in result.obligations:
cur.execute(
"""
INSERT INTO vendor_obligations (
order_id, order_type, obligation_kind, vendor, vendor_detail,
amount_usd_cents, fx_buffer_bps, due_by, status,
commission_ledger_id
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'pending',%s)
""",
(
o.order_id, o.order_type, o.obligation_kind,
o.vendor, o.vendor_detail,
o.amount_usd_cents, o.fx_buffer_bps, o.due_by,
o.commission_ledger_id,
),
)
conn.commit()
finally:
if close_after:
conn.close()