Integrate a ramp provider
Use this router for a provider-owned integration PR. Keep the reusable provider adapter in packages/sdp-payments, shared public types in packages/sdp-types, and API/webhook/DB orchestration in apps/sdp-api.
Inputs
docs — provider API documentation URL; use it as the source of truth for auth, endpoints, payloads, status semantics, and signatures.
capabilities — on-ramp, off-ramp, or both; supported entity types and Solana asset rails; manual_instructions, hosted, or session_widget quote delivery.
sandbox — credentials, test accounts, rate limits, webhook registration steps, and provider-specific sandbox limitations.
Complete the ramp intake and follow apps/sdp-docs/content/docs/reference/provider-onboarding.mdx before opening the PR. The dashboard ramp flow is active; there is no Payments v2 override cookie.
Sequence
Do them in this order. For unsupported directions, skip business-flow implementation but still satisfy the required RampProvider methods with empty rail/entity support and explicit typed rejection; only createOnrampQuote and listExternalAccountDetails are optional today.
- register-provider — add the id, package client, API schemas/dispatch, availability, setup registry, mode-keyed config, webhook registration decision, and dashboard catalog. Make the skeleton compile.
- rail-discovery — declare which fiat/crypto rails you support.
- integrate-estimate — rate preview; the cheapest live end-to-end check (no DB, no KYC).
- counterparty-requirements — required readiness contract for every provider, including providers that immediately return
ready or reject an unsupported direction.
- integrate-onramp / integrate-offramp — the quote flow(s) for the direction(s) you support.
- integrate-webhook — settlement events and reconciliation.
Adding the id breaks exhaustive registries and switches. Fix those failures without adding fallbacks, then follow register-provider for the non-exhaustive schemas, public quote types, translations, and UI catalogs the compiler cannot discover from the new union member alone.
Everything is discriminated on events
Every provider-facing surface is a closed union keyed by provider id + event kind; integrating a provider means declaring exactly which events it emits and accepts in each family:
- Requirement statuses —
CounterpartyRequirements arms per (provider, status) (counterparty-requirements).
- Advance submissions —
submitCounterpartyRequirementsSchema arms, collectedData = the only PII channel (counterparty-requirements).
- Webhook events —
RampSettlementEvent kinds, plus provider-specific provisioning events that auto-advance requirements (integrate-webhook).
- Client session events —
POST /v1/payments/ramps/<id>/events kinds for session_widget providers.
Provider-side state is counterparty_provider_accounts rows discriminated by kind (customer_link, payout_account, funding_wallet, merchant_wallet). counterparties.provider_data is deprecated — never add a provider key to it; PII flows only through advance-submission collectedData, JIT to the provider, never persisted.
Reference selection
Pick the existing provider closest to yours by archetype — all live under packages/sdp-payments/src/ramps/providers/:
- manual instructions plus customer/payout provisioning;
- hosted quote with no provider-side counterparty provisioning;
- session-widget quote;
- multi-step onboarding and provider-specific API-side state.
Rules that aren't optional (shared by every step)
- Do not default required credentials or required upstream fields. Explicit product defaults and optional-field fallbacks are acceptable only when their semantics are deliberate and tested; never swallow an upstream failure.
- HTTP in the provider; DB in the route handler. Providers read creds from the passed
env keyed by mode and never touch the database.
- Secrets are environment variables, mode-keyed where the upstream separates sandbox and production; a missing one throws
providerNotConfigured → HTTP 503. Never commit credentials.
- Webhooks are fully typed — parse the raw body as
unknown only at the signature boundary, then narrow.
- Strong typing — no
any, no enum, finite sets are as const satisfies Record<…>.
- Public contract follows OpenAPI. When a new provider changes a public request or response shape, update
apps/sdp-api/src/openapi/** and regenerate owned artifacts rather than editing generated files.
- Verify the changed surfaces:
pnpm --filter @sdp/payments typecheck, pnpm --filter @sdp/payments lint, pnpm --filter @sdp/payments test, pnpm --filter @sdp/api typecheck, and focused API tests. Run web checks when the dashboard catalog or quote renderer changes.
Per-step detail lives in each linked skill.
1---2name: integrate-ramp-provider3description: Start here for a provider-owned fiat↔crypto ramp integration PR spanning @sdp/payments, shared contracts, sdp-api orchestration, webhooks, and dashboard presentation.4---56# Integrate a ramp provider78Use this router for a provider-owned integration PR. Keep the reusable provider adapter in `packages/sdp-payments`, shared public types in `packages/sdp-types`, and API/webhook/DB orchestration in `apps/sdp-api`.910## Inputs1112- **`docs`** — provider API documentation URL; use it as the source of truth for auth, endpoints, payloads, status semantics, and signatures.13- **`capabilities`** — on-ramp, off-ramp, or both; supported entity types and Solana asset rails; `manual_instructions`, `hosted`, or `session_widget` quote delivery.14- **`sandbox`** — credentials, test accounts, rate limits, webhook registration steps, and provider-specific sandbox limitations.1516Complete the [ramp intake](https://solanafoundation.typeform.com/to/sxTGbwXt) and follow `apps/sdp-docs/content/docs/reference/provider-onboarding.mdx` before opening the PR. The dashboard ramp flow is active; there is no Payments v2 override cookie.1718## Sequence1920Do them in this order. For unsupported directions, skip business-flow implementation but still satisfy the required `RampProvider` methods with empty rail/entity support and explicit typed rejection; only `createOnrampQuote` and `listExternalAccountDetails` are optional today.21221. **register-provider** — add the id, package client, API schemas/dispatch, availability, setup registry, mode-keyed config, webhook registration decision, and dashboard catalog. Make the skeleton compile.232. **rail-discovery** — declare which fiat/crypto rails you support.243. **integrate-estimate** — rate preview; the cheapest live end-to-end check (no DB, no KYC).254. **counterparty-requirements** — required readiness contract for every provider, including providers that immediately return `ready` or reject an unsupported direction.265. **integrate-onramp** / **integrate-offramp** — the quote flow(s) for the direction(s) you support.276. **integrate-webhook** — settlement events and reconciliation.2829Adding the id breaks exhaustive registries and switches. Fix those failures without adding fallbacks, then follow `register-provider` for the non-exhaustive schemas, public quote types, translations, and UI catalogs the compiler cannot discover from the new union member alone.3031## Everything is discriminated on events3233Every provider-facing surface is a closed union keyed by provider id + event kind; integrating a provider means declaring exactly which events it emits and accepts in each family:34351. **Requirement statuses** — `CounterpartyRequirements` arms per `(provider, status)` (`counterparty-requirements`).362. **Advance submissions** — `submitCounterpartyRequirementsSchema` arms, `collectedData` = the only PII channel (`counterparty-requirements`).373. **Webhook events** — `RampSettlementEvent` kinds, plus provider-specific provisioning events that auto-advance requirements (`integrate-webhook`).384. **Client session events** — `POST /v1/payments/ramps/<id>/events` kinds for `session_widget` providers.3940Provider-side state is `counterparty_provider_accounts` rows discriminated by `kind` (`customer_link`, `payout_account`, `funding_wallet`, `merchant_wallet`). **`counterparties.provider_data` is deprecated** — never add a provider key to it; PII flows only through advance-submission `collectedData`, JIT to the provider, never persisted.4142## Reference selection4344Pick the existing provider closest to yours by archetype — all live under `packages/sdp-payments/src/ramps/providers/`:4546- manual instructions plus customer/payout provisioning;47- hosted quote with no provider-side counterparty provisioning;48- session-widget quote;49- multi-step onboarding and provider-specific API-side state.5051## Rules that aren't optional (shared by every step)5253- **Do not default required credentials or required upstream fields.** Explicit product defaults and optional-field fallbacks are acceptable only when their semantics are deliberate and tested; never swallow an upstream failure.54- **HTTP in the provider; DB in the route handler.** Providers read creds from the passed `env` keyed by `mode` and never touch the database.55- **Secrets are environment variables**, mode-keyed where the upstream separates sandbox and production; a missing one throws `providerNotConfigured` → HTTP 503. Never commit credentials.56- **Webhooks are fully typed** — parse the raw body as `unknown` only at the signature boundary, then narrow.57- **Strong typing** — no `any`, no `enum`, finite sets are `as const satisfies Record<…>`.58- **Public contract follows OpenAPI.** When a new provider changes a public request or response shape, update `apps/sdp-api/src/openapi/**` and regenerate owned artifacts rather than editing generated files.59- **Verify the changed surfaces:** `pnpm --filter @sdp/payments typecheck`, `pnpm --filter @sdp/payments lint`, `pnpm --filter @sdp/payments test`, `pnpm --filter @sdp/api typecheck`, and focused API tests. Run web checks when the dashboard catalog or quote renderer changes.6061Per-step detail lives in each linked skill.