Eligibility
Different Pair Instruments carry different access requirements. PairStreet treats eligibility as an asset-level infrastructure concern: rules live with the instrument, and one engine evaluates them everywhere a Pair is touched.
An asset-level concern
A Pair Market is a token plus a financial Pair. The token is an open Solana asset. The Pair Instrument behind it may come with terms set by its issuer or provider: a tokenized fund that is not offered to U.S. persons, a municipal exposure offered only in the Americas, an instrument that requires provider verification of the holder.
PairStreet attaches those requirements to the instrument, not to the token or to the creator. Every Pair Market that pairs with an instrument inherits its rules. When an instrument’s rules change, every market paired with it follows the same rules from the next request.
The Eligibility EngineEligibility EngineEvaluates a viewer's region and verification state against a Pair Instrument's rules and returns allow or restrict before any Pair interaction. is a pure function, evaluateEligibility, in src/lib/eligibility.ts. It takes the instrument’s rules and the viewer’s country and returns a verdict. It has no I/O and no state, which makes it identical in every call site and straightforward to test.
- UserVIEWER COUNTRY
Country resolved on the server from the edge geo header.
- Pair MarketTOKEN × PAIR INSTRUMENT
The market resolves its Pair Instrument and that instrument’s status.
- Eligibility EngineevaluateEligibility()
Pure function. Geographic rules first, then verification rules.
- Provider rulesinstrument_eligibility
Rules recorded for the instrument from its issuer or provider terms.
- allow / block countriesISO 3166 alpha-2 codes
- allow / block regionsamericas, europe, asia, middle_east, africa, global
- kyc_requiredprovider verification
- accredited_onlyprovider verification
- VerdictALLOW OR RESTRICT
- Allow{ eligible: true }Allow
- Restrictcode + reasonRestricted
Figure summary: User, then Pair Market, then the Eligibility Engine, then provider rules for the Pair Instrument, then a verdict: allow, or restrict with a result code.
Rule types
Each rule is a row in instrument_eligibility: the instrument it belongs to, the rule type, a list of countries, a list of regions and an optional note shown to the viewer.
// src/lib/eligibility.ts
export type EligibilityRule = {
rule: "allow_countries" | "block_countries" | "allow_regions" | "block_regions" | "kyc_required" | "accredited_only";
countries: string[];
regions: string[];
note?: string | null;
};
export type EligibilityVerdict =
| { eligible: true }
| { eligible: false; code: "REGION_RESTRICTED" | "UNKNOWN_REGION" | "KYC_REQUIRED"; reason: string };
/**
* Pure eligibility evaluation. Infrastructure, not branding: the UI only ever
* renders "Pair unavailable in your region" and blocks the transaction.
*
* `strictUnknown`: in production an unknown viewer country cannot interact with
* instruments that carry geographic rules.
*/
export function evaluateEligibility(
rules: EligibilityRule[],
country: string | null,
opts: { strictUnknown: boolean } = { strictUnknown: false },
): EligibilityVerdict;| Rule | Uses | Restricts when |
|---|---|---|
| allow_countries | countries | The viewer’s country is not in the list. |
| block_countries | countries | The viewer’s country is in the list. |
| allow_regions | regions | The viewer’s region is not in the list, or the country has no mapped region. |
| block_regions | regions | The viewer’s region is in the list. |
| kyc_required | note | Always, until provider verification of the holder is available for the instrument. |
| accredited_only | note | Always, until provider verification of accredited status is available for the instrument. |
Countries map to regions through a fixed table in src/lib/domain.ts (for example US and BR to americas, DE and GB to europe, JP to asia). The regions are americas, europe, asia, middle_east, africa and global.
Rules in the catalog
Most instruments carry no rules. These carry rules in the current catalog:
| Pair Instrument | Rule | Note shown |
|---|---|---|
| U.S. Dollar Yield (USDY) | block_countries: US | Issuer terms: USDY is not offered to U.S. persons. |
| Swiss Government Debt | block_countries: US | Not offered to U.S. persons. |
| Brazil Equity Market | block_countries: US | Not offered to U.S. persons. |
| NYC Municipal Debt | allow_regions: americas | Available in the Americas only. |
Order of evaluation
The verdict is deterministic. The server evaluates in this order and returns at the first restriction:
- Step 01INSTRUMENT_UNAVAILABLEInstrument status
Before any rule is read, the instrument must be active. A paused, coming_soon, provider_unavailable or deprecated instrument returns INSTRUMENT_UNAVAILABLE with a reason such as “This Pair is paused.”
- Step 02Split the rules
Rules are divided into geographic rules (allow and block, by country or region) and verification rules (kyc_required, accredited_only).
- Step 03UNKNOWN_REGIONUnknown country
If the instrument has geographic rules and the viewer’s country is unknown, production returns UNKNOWN_REGION (“We could not determine your region for this Pair.”). The development network treats the viewer as eligible so the rest of the flow can be exercised.
- Step 04REGION_RESTRICTEDGeographic rules
The country is upper-cased and mapped to its region. Each geographic rule is checked in order. The first rule that fails returns REGION_RESTRICTED, with the rule’s note as the reason, or “Pair unavailable in your region.” when the rule has no note.
- Step 05KYC_REQUIREDVerification rules
If any kyc_required or accredited_only rule exists, the verdict is KYC_REQUIRED, with its note or “This Pair requires provider verification.”
- Step 06Allow
No rule restricted the viewer. The verdict is { eligible: true }.
An instrument with no rules is open to every viewer, including viewers whose country is unknown. An instrument with only verification rules does not need the viewer’s country. These cases are covered by the engine’s tests, along with country blocks, region allow-lists and the strict and lenient handling of unknown countries.
Result codes
| Code | Produced by | Meaning | Shown to the viewer |
|---|---|---|---|
| REGION_RESTRICTED | evaluateEligibility | A geographic rule excludes the viewer’s country or region. | Pair unavailable in your region |
| UNKNOWN_REGION | evaluateEligibility (strictUnknown) | Geographic rules exist and the viewer’s country is unknown. | We could not determine your region for this Pair. |
| KYC_REQUIRED | evaluateEligibility | The instrument requires provider verification or accredited status. | This Pair requires provider verification. |
| INSTRUMENT_UNAVAILABLE | eligibilityFor and provider checkEligibility | The instrument is not active. | This Pair is <status>. |
The same verdict type is exposed by every provider through checkEligibility, so a provider can add its own checks on top of the instrument’s rules. See Provider Interface.
Where the country comes from
The viewer’s country is resolved on the server from the edge. On Vercel that is the x-vercel-ip-country header; cf-ipcountry and x-country-code are accepted as fallbacks for other edges. The value must be a two-letter code, otherwise the country is treated as unknown. The browser never supplies the country in production.
// src/server/request-context.ts
export async function getViewerCountry(): Promise<string | null> {
if (isDevMode()) {
const c = (await cookies()).get(DEV_COUNTRY_COOKIE)?.value?.toUpperCase();
if (c && COUNTRIES[c]) return c;
}
const h = await headers();
const v = (h.get("x-vercel-ip-country") ?? h.get("cf-ipcountry") ?? h.get("x-country-code"))?.toUpperCase() ?? null;
return v && /^[A-Z]{2}$/.test(v) ? v : null;
}GET /api/region returns the resolved country. On the development network a cookie override, set through POST /api/region, lets engineers exercise each regional rule from the dev toolbar. In production that endpoint answers 403 and the override is ignored.
strictUnknown in production
Every production call site passes strictUnknown when PAIRSTREET_MODE is production. A viewer whose country the edge could not determine is treated as restricted for any instrument that has geographic rules. An unknown location never satisfies an allow-list, and it never slips past a block-list.
Where eligibility is enforced
Eligibility is computed on the server for each request and returned with the data the page renders. The transaction-building paths enforce it before anything is built or written.
- ·Instrument catalog and Pair pages: /pairs, /pair/[slug], GET /api/instruments and GET /api/instruments/[slug] return a verdict for every instrument.
- ·Token pages and portfolio: each Pair Market shows its instrument’s verdict, and each reward line in the portfolio carries one.
- ·Launch: the launch service evaluates the chosen instrument before preparing transactions. A restricted viewer receives HTTP 403 with code PAIR_UNAVAILABLE_IN_REGION and no transaction is built. Covered by an integration test.
- ·Claims: POST /api/rewards/claim evaluates every instrument in the request before any row is written. Covered by an integration test.
What a restricted viewer can do
| Action | Restricted viewer | Why |
|---|---|---|
| Browse instruments and Pair Markets | Open | The verdict is displayed alongside the instrument. |
| Trade the token | Open | The token is a Pump asset on Solana. Eligibility governs the financial Pair. |
| Request a Pair | Open | Demand signals are not tied to an instrument’s access terms. |
| Launch a coin paired with the instrument | Restricted | The launch is rejected before a transaction is built. |
| Claim rewards from that Pair | Restricted | Allocations stay claimable and can be claimed from an eligible region. |
Allocations themselves are computed without reference to location. A restriction gates the claim, not the allocation. See the claim flow.
User-facing state
Verification rules are part of the model today. Their verdict is KYC_REQUIRED until a provider’s verification flow is connected for that instrument, at which point the provider’s checkEligibility confirms the holder. Instruments and their providers are described in Pair Instruments and Providers.