Documentation
¶
Overview ¶
Package relay routes one transaction to whichever ramp provider offers the best price for it.
What "best" means ¶
Every quote is reduced to one comparable number: RateQuote.EffectiveRate, the fiat that changes hands per unit of crypto after every fee on both legs. Maximise it on an off-ramp — more shillings for the same USDC — and minimise it on an on-ramp, where it is a cost. Never compare providers on a headline rate: Fonbnk's exchangeRateAfterFees mixes a pre-fee local amount with a post-fee USD one and moves opposite to the user's outcome, and YellowCard publishes no priced quote at all.
Why the amount matters ¶
Fees are banded and often flat. A 30 KES fee is 1.2% of a 2,498 KES payout and 15% of a 200 KES one, so the cheapest provider at one size is not the cheapest at another. RateRequest therefore always carries the real amount and nothing caches a rate per corridor.
The switch ¶
Router is a kill switch, not a feature gate. With routing off it quotes only the default provider — YellowCard — so flipping ENABLE_PAYMENT_PROVIDER_RELAY_SWITCH off is safe to do under load and restores the previous behaviour exactly. With routing on, one provider failing is tolerated and logged; every provider failing is an error.
The margin guard ¶
Best routes. BestWithinMargin additionally quotes the opposite direction and refuses when a round trip would sell crypto for less than it costs to buy back, beyond the configured floor. That doubles the quote calls, so it belongs on treasury-scale movements rather than on every borrower transaction. Spread exposes the same figures for monitoring.
Adding a provider ¶
This package knows nothing about any vendor. Implement RateSource, register it, and it competes on equal terms with everything else:
registry := relay.NewRegistry()
registry.MustRegister(sources.NewYellowCardSource(...))
registry.MustRegister(myBankSource{})
router, err := relay.New(relay.Config{Registry: registry, Default: "yellowcard"})
A source only has to turn a corridor and an amount into one EffectiveRate. Use SourceErr to build failures so they land in the same domain as the platform's own, and return an error rather than a zero rate — a zero would win an on-ramp by being smallest.
Implement the optional DirectionalSource on a one-way rail so the Router skips it for the direction it cannot serve, rather than calling it and logging a failure on every transaction.
The platform's own sources live in the sources subpackage, which is the only thing here that imports a vendor. Importing relay alone pulls in no provider client.
Provider quirks the platform's sources absorb ¶
YellowCard publishes buy and sell rates named from the customer's side: sell is USD to local and belongs to an off-ramp, buy is local to USD and belongs to an on-ramp. Swapping them inverts every routing decision without failing anything. Its channels are ramp-scoped too, so an off-ramp must not be priced against a deposit channel's fees.
Fonbnk prices a quote for real, with the amount on the crypto leg in both directions — the side we always know — so one call prices the corridor at the size actually being moved.
Index ¶
- Constants
- func SourceErr(op string) oops.OopsErrorBuilder
- type Config
- type DirectionalSource
- type RateQuote
- type RateRequest
- type RateSource
- type Registry
- type Router
- func (r *Router) Best(ctx context.Context, req RateRequest) (*RateQuote, error)
- func (r *Router) BestWithinMargin(ctx context.Context, req RateRequest) (*RateQuote, error)
- func (r *Router) DefaultProvider() string
- func (r *Router) Enabled() bool
- func (r *Router) Registry() *Registry
- func (r *Router) Spread(ctx context.Context, req RateRequest) (*Spread, error)
- type Spread
Constants ¶
const ( DirectionOffRamp = "off_ramp" DirectionOnRamp = "on_ramp" )
Directions a corridor can run in.
Variables ¶
This section is empty.
Functions ¶
func SourceErr ¶
func SourceErr(op string) oops.OopsErrorBuilder
SourceErr starts an error builder for a RateSource implementation, so a builder's own source reports failures in the same domain as the platform's.
Types ¶
type Config ¶
type Config struct {
// Registry holds the sources to quote. Required.
Registry *Registry
// Enabled turns routing on. Off sends every request to Default.
Enabled bool
// Default is the provider used when routing is off, and the one a caller
// falls back to when routing is unavailable.
Default string
// MinRoundTripMarginPct is the floor BestWithinMargin enforces, as a
// fraction. Zero means break-even.
MinRoundTripMarginPct float64
Logger *slog.Logger
}
Config wires a Router.
type DirectionalSource ¶
type DirectionalSource interface {
RateSource
SupportsDirection(direction string) bool
}
DirectionalSource narrows a source to the directions it can price. Optional: a source that does not implement it is asked for every direction.
type RateQuote ¶
type RateQuote struct {
Provider string
Direction string
FiatCurrency string
CryptoAmount float64
FiatAmount float64
EffectiveRate float64
// Payload is the provider's own quote, for a caller that goes on to open
// an order against it.
Payload any
}
RateQuote is one provider's price for a corridor. EffectiveRate is fiat per unit of crypto after all fees — never a provider's headline rate.
type RateRequest ¶
type RateRequest struct {
Direction string
FiatCurrency string
CountryCode string
CryptoAmount float64
}
RateRequest asks every source to price one corridor. CryptoAmount is always in crypto units; fees are banded, so a rate needs the real amount.
type RateSource ¶
type RateSource interface {
Name() string
QuoteRate(ctx context.Context, req RateRequest) (*RateQuote, error)
}
RateSource prices one corridor with one provider. Implement it to add a provider the platform has never heard of.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the rate sources a Router may quote.
Register everything during boot, then freeze: reads are safe for concurrent use afterwards, concurrent registration is not. Mirrors offramp.Registry.
func (*Registry) All ¶
func (r *Registry) All() []RateSource
All returns every registered source in registration order.
func (*Registry) Get ¶
func (r *Registry) Get(name string) (RateSource, bool)
Get returns a source by name.
func (*Registry) MustRegister ¶
func (r *Registry) MustRegister(source RateSource)
MustRegister panics on a failed registration, for boot wiring with no useful recovery.
func (*Registry) Register ¶
func (r *Registry) Register(source RateSource) error
Register adds a source, keyed by its Name. A nil source, an empty name or a duplicate is a boot-time misconfiguration and fails loudly.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router picks the provider offering the best effective rate.
func NewWithSources ¶
func NewWithSources(cfg Config, sources ...RateSource) (*Router, error)
NewWithSources is New over an inline set of sources, for callers that have no other use for a Registry.
func (*Router) Best ¶
Best returns the winning quote for a corridor. Routing off quotes only the default provider; routing on tolerates one source failing, not all.
func (*Router) BestWithinMargin ¶
BestWithinMargin is Best plus the round-trip guard. Doubles the quote calls, so it suits treasury-scale movements rather than every transaction.
func (*Router) DefaultProvider ¶
DefaultProvider names the provider used when routing is off.
type Spread ¶
type Spread struct {
FiatCurrency string
CryptoAmount float64
// BuyRate is the fiat cost of one crypto unit on the best on-ramp;
// SellRate is the fiat yield of one on the best off-ramp.
BuyRate float64
SellRate float64
BuyFrom string
SellTo string
// Margin is (sell - buy) / buy. Negative means a round trip loses money.
Margin float64
}
Spread is the round trip across both directions at one amount.