Provider Interface
The TypeScript contracts that connect a Pair Market to the outside world: FinancialInstrumentProvider for Pair Instruments, the Pair Router that plans and executes acquisitions, and LaunchAdapter for the token venue.
Overview
Three boundaries keep PairStreet’s product code independent of any venue or issuer:
| Contract | File | Implementations |
|---|---|---|
| FinancialInstrumentProvider | src/server/adapters/providers/types.ts | ListedInstrumentProvider (registry), SplOnchainProvider (spl-onchain) |
| Pair Router | src/server/pair/router.ts | planAcquisition, executeAcquisition |
| LaunchAdapter | src/server/adapters/launch/types.ts | PumpLaunchAdapter (live), a development adapter |
Code reaches a provider only through getProvider(id) in the registry. A ProviderProviderA source of a Pair Instrument behind the common provider interface: an onchain venue, a tokenized-asset issuer, an RFQ desk or a broker route. is selected by the adapterKey recorded on its catalog row. Product code never imports a provider SDK and never branches on a provider name.
FinancialInstrumentProvider
/**
* FinancialInstrumentProvider: the abstraction every Pair Instrument source implements.
*
* A provider knows how to describe an instrument, prove which onchain asset is canonical,
* price it, say who may hold it, and plan/execute acquiring it. PairStreet product code
* only ever talks to this interface (via the registry), never to a provider SDK directly.
*/
export interface FinancialInstrumentProvider {
readonly id: string;
readonly kind: ProviderKind;
/** Mock providers exist only in development and their output must never be presented as real assets. */
readonly isMock: boolean;
getInstruments(): Promise<ProviderInstrument[]>;
getInstrument(ref: string): Promise<ProviderInstrument | null>;
/** The onchain representation holders actually receive. Null if the instrument has none (yet). */
getCanonicalAsset(ref: string): Promise<CanonicalAsset | null>;
getQuote(req: QuoteRequest): Promise<InstrumentQuote>;
checkEligibility(req: EligibilityRequest): Promise<EligibilityResult>;
getPurchaseRoute(req: QuoteRequest): Promise<PurchaseRoute>;
/** Execute an acquisition plan. Live providers require a configured vault signer. */
purchase(req: PurchaseRequest): Promise<PurchaseExecution>;
getTransferRestrictions(ref: string): Promise<TransferRestrictions>;
getInstrumentStatus(ref: string): Promise<InstrumentStatusReport>;
getIssuerDocuments(ref: string): Promise<IssuerDocument[]>;
}Methods
- id, kind
- The provider id from the catalog and its ProviderKind.
- isMock
- True only for development providers. A mock result is never written to a live ledger row; executeAcquisition refuses a mock execution on a live plan.
- getInstruments()
- Every instrument the provider offers, as ProviderInstrument records. The catalog sync writes them to pair_instruments.
- getInstrument(ref)
- One instrument by the provider’s own reference (pair_instruments.provider_reference), or null.
- getCanonicalAsset(ref)
- The onchain asset holders actually receive: mint, decimals, token program and verification level. Null while an instrument has no connected settlement asset. The spl-onchain adapter cross-checks decimals against Jupiter’s token list.
- getQuote(req)
- Prices converting inputAmountRaw of the settlement asset (USDC by default) into the instrument. Returns raw amounts as strings, an implied USD price, price impact and the quote source.
- checkEligibility(req)
- Verdict for a viewer country. Implementations apply the instrument’s rules through evaluateEligibility and may add provider-specific checks.
- getPurchaseRoute(req)
- The acquisition route: mode, steps, the quote it was planned on, availability and blockers. A route that is not executable says why in blockers instead of failing.
- purchase(req)
- Executes a route for a Pair Vault address. Live providers require a configured vault signer; until one exists they raise ProviderNotEnabledError.
- getTransferRestrictions(ref)
- Whether the Pair Asset is transferable, a readable summary, and its token program.
- getInstrumentStatus(ref)
- Current InstrumentStatus with a reason and timestamp. The spl-onchain adapter derives it from whether a 100 USDC route exists.
- getIssuerDocuments(ref)
- Issuer links: website, terms, prospectus, attestation, docs.
Types
export type ProviderInstrument = {
ref: string; // provider's own reference (stored as pair_instruments.provider_reference)
slug: string;
name: string;
shortName: string;
symbol: string;
description: string;
category: InstrumentCategory;
subcategory?: string;
country: string;
region: Region;
issuer: string;
assetType: AssetType;
icon: string;
searchTerms: string;
documentationUrl?: string;
};
export type CanonicalAsset = {
network: "solana";
mint: string;
decimals: number;
tokenProgram: "spl-token" | "token-2022" | "unknown";
verification: InstrumentVerification;
verifiedBy?: string;
};
export type QuoteRequest = {
ref: string;
inputMint: string; // settlement asset (USDC by default)
inputAmountRaw: bigint;
slippageBps?: number;
};
export type InstrumentQuote = {
ref: string;
inputMint: string;
inputAmountRaw: string;
outputAmountRaw: string;
outputDecimals: number;
/** USD per whole instrument unit implied by the quote. */
priceUsd: number;
priceImpactPct: number;
source: string; // e.g. "jupiter", "dev-mock"
quotedAt: string;
isMock: boolean;
raw?: unknown;
};
export type EligibilityRequest = { ref: string; country: string | null };
export type EligibilityResult =
| { eligible: true }
| { eligible: false; code: "REGION_RESTRICTED" | "UNKNOWN_REGION" | "KYC_REQUIRED" | "INSTRUMENT_UNAVAILABLE"; reason: string };
export type RouteStep = { kind: "swap" | "mint" | "rfq" | "transfer" | "simulated"; label: string; venue?: string };
export type PurchaseRoute = {
mode: RouteMode;
available: boolean;
steps: RouteStep[];
quote?: InstrumentQuote;
/** Reasons the route cannot execute right now (missing signer, no liquidity, provider paused…). */
blockers: string[];
};
export type PurchaseRequest = { route: PurchaseRoute; vaultAddress: string; inputAmountRaw: bigint };
export type PurchaseExecution = {
outputAmountRaw: bigint;
executionPriceUsd: number;
txSignature: string | null;
isMock: boolean;
};
export type TransferRestrictions = { transferable: boolean; summary: string; tokenProgram?: string };
export type InstrumentStatusReport = { status: InstrumentStatus; reason?: string; checkedAt: string };
export type IssuerDocument = { title: string; url: string; kind: "website" | "terms" | "prospectus" | "attestation" | "docs" };// src/lib/domain.ts
export const PROVIDER_KINDS = [
"tokenized_bonds",
"tokenized_securities",
"tokenized_fund",
"real_estate",
"commodity",
"onchain_dex_route",
"pair_registry",
"dev_mock",
] as const;
export const ROUTE_MODES = ["ONCHAIN_SWAP", "PROVIDER_API", "RFQ", "ISSUER_MINT", "BROKER_ROUTE", "DEV_SIMULATED"] as const;
export const INSTRUMENT_STATUSES = ["active", "paused", "coming_soon", "provider_unavailable", "deprecated"] as const;
export const INSTRUMENT_VERIFICATION = [
"dev_mock", // no real asset, development only
"listed", // listed for pairing; onchain settlement asset not connected yet
"issuer_listed", // mint published by issuer, routable, not independently verified by PairStreet
"aggregator_verified", // verified by a token-list aggregator (e.g. Jupiter verified tag)
"pairstreet_verified", // reviewed by PairStreet (manual process, future)
] as const;Raw amounts cross the interface as bigint on requests and as decimal strings on quotes, so a quote serializes to JSON without loss. Prices are USD per whole instrument unit.
Pair Router
The Pair RouterPair RouterThe execution layer that converts a Pair Vault's settlement balance into an acquisition plan for the Pair Asset and hands it to the right executor. sits between a Pair Market and its provider. It turns “this Pair Market holds X of the settlement asset” into an AcquisitionPlan, then hands an executable plan to the right executor. Design and route selection are covered in Pair Router.
// src/server/pair/router.ts (types and signatures)
export type AcquisitionPlan = {
pairMarketId: string;
instrumentRef: string;
providerId: string;
settlementMint: string;
inputAmountRaw: string;
mode: RouteMode;
route: PurchaseRoute;
executable: boolean;
blockers: string[];
ledgerMode: LedgerMode;
plannedAt: string;
};
export type PlanInput = {
pairMarketId: string;
marketLedgerMode: LedgerMode;
instrument: { providerReference: string; providerId: string; status: string; referencePriceUsd: number; decimals: number; shortName: string };
settlementMint: string;
inputAmountRaw: bigint;
};
export async function planAcquisition(input: PlanInput): Promise<AcquisitionPlan>;
export async function executeAcquisition(plan: AcquisitionPlan, vaultAddress: string): Promise<PurchaseExecution>;planAcquisition
The router asks the instrument’s provider for a purchase route, then adds its own checks. Every blocker is collected, de-duplicated and returned with the plan. A plan is executable only when the route is available and the blocker list is empty.
| Router check | Blocker added |
|---|---|
| Instrument status is not active | Instrument is <status> |
| The route mode has no executor in this build | Route mode <mode> has no executor in this build |
| A simulated route outside development | Simulated routes are disabled outside development |
| A simulated route on a live Pair Market | A live Pair Market can never settle through a simulated route |
The plan’s ledgerMode is dev_simulated only for DEV_SIMULATED routes and live otherwise, so every settlement row inherits the correct Ledger modeLedger modeEvery financial ledger row carries live or development. Production reads and writes live rows only..
executeAcquisition
executeAcquisition throws if the plan is not executable, listing its blockers. A DEV_SIMULATED plan returns the quoted output with no transaction signature. Any other mode calls the provider’s purchase with the plan’s route, the vault address and the input amount. A mock execution returned for a live plan is refused.
| Route mode | Status | Detail |
|---|---|---|
| ONCHAIN_SWAP | Planned live | Planned with live Jupiter quotes for SPL instruments. Execution activates with Pair Vault custody. |
| PROVIDER_API | Activates per route | Interface defined. Registry instruments report this mode while their route connects. |
| RFQ | Architecture | Interface defined; activates per provider. |
| ISSUER_MINT | Architecture | Interface defined; activates per provider. |
| BROKER_ROUTE | Architecture | Interface defined; activates per provider. |
| DEV_SIMULATED | Development only | Executes on the development network only and writes dev_simulated ledger rows. |
LaunchAdapter
LaunchAdapter is the token-side boundary. It is the only code that talks to launch and liquidity infrastructure. Pump is the first venue, implemented by PumpLaunchAdapter on the official @pump-fun/pump-sdk v2.0.0. Which Pump instructions each method builds is documented in Pump and Solana.
/**
* LaunchAdapter: the ONLY place PairStreet talks to launch/liquidity infrastructure.
* Pump is the first venue. React components never import an adapter; they call
* PairStreet API routes, which call services, which call the adapter.
*/
export interface LaunchAdapter {
readonly venue: "pump";
/** `dev_simulated` adapters never touch a chain; their results are flagged in every ledger row. */
readonly ledgerMode: LedgerMode;
/** Build the launch. Live: returns a partially-signed transaction (mint keypair signed) for the creator wallet. */
prepareLaunch(input: PrepareLaunchInput): Promise<PreparedLaunch>;
/** Verify the launch landed and return canonical venue addresses. */
confirmLaunch(input: ConfirmLaunchInput): Promise<ConfirmedLaunch>;
getMarket(mint: string): Promise<VenueMarket | null>;
getGraduationStatus(mint: string): Promise<GraduationStatus>;
quoteTrade(input: QuoteTradeInput): Promise<TradeQuote>;
buildBuyTransaction(input: BuildTradeInput): Promise<PreparedTrade>;
buildSellTransaction(input: BuildTradeInput): Promise<PreparedTrade>;
/** Venue-native trade history. Production ingestion goes through the indexer, not this call. */
getTrades(mint: string, opts: { limit: number; before?: string }): Promise<VenueTrade[]>;
/** Creator-fee split (Pump fee-sharing config): build the setup tx for a launched mint. Live only. */
buildFeeSetupTransaction?(input: { mint: string; creator: string; payer?: string; freshCurve?: boolean }): Promise<{ transactionBase64: string; lastValidBlockHeight: number }>;
/** Read the on-chain creator-fee split for a mint. Null when no sharing config exists yet. */
readFeeSplit?(mint: string): Promise<OnchainFeeSplit | null>;
/** Permissionless: pay out accrued creator fees to the configured shareholders. */
buildDistributeFeesTransaction?(input: { mint: string; payer: string }): Promise<{ transactionBase64: string; lastValidBlockHeight: number; distributableLamports: string; canDistribute: boolean }>;
}- prepareLaunch
- Builds the launch: create_v2 plus create_fee_sharing_config with the mint keypair pre-signed, then follow-up transactions for the fee split and an optional first buy.
- confirmLaunch
- Fetches the signature on-chain, checks it did not fail, checks the mint and creator are in it, and checks the bonding curve exists.
- getMarket / getGraduationStatus
- Reads the bonding-curve account: reserves, supply, completion, creator. Graduation status is bonding or graduated from the curve’s complete flag.
- quoteTrade
- Bonding-curve quote with Pump’s fees embedded, plus the PairStreet trading fee (0.5% of the SOL side) in protocolFeeRaw.
- buildBuyTransaction / buildSellTransaction
- Unsigned versioned transactions with buy_v2 or sell_v2 and the fee transfer, for the trader’s wallet to sign.
- getTrades
- Venue trade history. Production ingestion runs through the indexer.
- buildFeeSetupTransaction
- update_fee_shares: creator 5,000 bps, treasury 5,000 bps. Rejects a split that is already locked or a caller who is not the coin’s creator.
- readFeeSplit
- Decodes the mint’s Pump fee-sharing config: address, admin, adminRevoked and shareholders. Null if none exists.
- buildDistributeFeesTransaction
- Permissionless payout of accrued creator fees to the shareholders, with the distributable amount and whether a payout is possible now.
Adding a provider
A provider plugs in without touching the Pair Router, the settlement engine or the reward engine.
- Step 01PROVIDERSRegister the provider
Add a catalog row with its id, kind and adapterKey.
src/server/catalog/instruments.tsplaceholder values// src/server/catalog/instruments.ts export const PROVIDERS: CatalogProvider[] = [ // ... { id: "<provider-id>", name: "<Provider name>", kind: "tokenized_bonds", // a ProviderKind adapterKey: "spl-onchain", // "registry" | "spl-onchain" | a new key website: "https://<issuer-site>", description: "<one line: what the provider issues>", isMock: false, }, ]; - Step 02adapterKeyChoose or write the adapter
If the instrument is a liquid SPL or Token-2022 asset on Solana, reuse spl-onchain. Otherwise implement FinancialInstrumentProvider in a new class under src/server/adapters/providers/ and map a new adapterKey to it in the registry.
src/server/adapters/providers/registry.ts// src/server/adapters/providers/registry.ts function build() { const map = new Map<string, FinancialInstrumentProvider>(); for (const p of PROVIDERS) { if (p.adapterKey === "registry") { map.set(p.id, new ListedInstrumentProvider(p.id)); } else if (p.adapterKey === "spl-onchain") { map.set(p.id, new SplOnchainProvider(p.id, p.kind)); } } return map; } export function getProvider(id: string): FinancialInstrumentProvider; export function listProviders(): FinancialInstrumentProvider[]; - Step 03INSTRUMENTSAdd its instruments
Each instrument records providerId, its provider reference, mint, decimals, token program, verification level, status, eligibility rules, transfer summary, issuer documents and a reference price with its source. An instrument with a mint is never recorded as listed; the catalog test enforces it.
- Step 04syncCatalogSync the catalog
syncCatalog upserts providers, instruments and eligibility rules idempotently, preserving instrument ids and their statistics. Tested for repeat runs.
- Step 05blockersReport what the route needs
Until the route can execute, getPurchaseRoute returns available: false with explicit blockers, and purchase raises ProviderNotEnabledError naming what is missing. The instrument is selectable for pairing as soon as its status is active; settlement follows when its route executes.
ProviderNotEnabledError
A provider that knows exactly what it still needs says so in a typed error. The missing list names each requirement.
// src/server/adapters/providers/types.ts
export class ProviderNotEnabledError extends Error {
constructor(
message: string,
readonly missing: string[],
) {
super(message);
this.name = "ProviderNotEnabledError";
}
}The API layer maps ProviderNotEnabledError (and the launch-side AdapterNotAvailableError) to HTTP 501 with code NOT_AVAILABLE and passes the missing list through. This is the response the spl-onchain adapter’s purchase produces today:
{
"error": {
"code": "NOT_AVAILABLE",
"message": "Live Pair acquisitions are not enabled in this build.",
"missing": [
"Pair Vault custody program or multisig signer",
"Settlement executor service with Jupiter swap execution"
]
}
}