relay

package
v1.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: AGPL-3.0 Imports: 7 Imported by: 0

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

View Source
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.

func (RateQuote) Better

func (q RateQuote) Better(other RateQuote) bool

Better reports whether this quote beats other for its direction.

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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty 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) Len

func (r *Registry) Len() int

Len reports how many sources are registered.

func (*Registry) MustRegister

func (r *Registry) MustRegister(source RateSource)

MustRegister panics on a failed registration, for boot wiring with no useful recovery.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns every registered source name in registration order.

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 New

func New(cfg Config) (*Router, error)

New validates the config and returns a Router.

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

func (r *Router) Best(ctx context.Context, req RateRequest) (*RateQuote, error)

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

func (r *Router) BestWithinMargin(ctx context.Context, req RateRequest) (*RateQuote, error)

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

func (r *Router) DefaultProvider() string

DefaultProvider names the provider used when routing is off.

func (*Router) Enabled

func (r *Router) Enabled() bool

Enabled reports whether routing is on.

func (*Router) Registry

func (r *Router) Registry() *Registry

Registry returns the sources this Router quotes.

func (*Router) Spread

func (r *Router) Spread(ctx context.Context, req RateRequest) (*Spread, error)

Spread quotes both directions and reports the round trip.

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL