# Integrate Onramp

> Implement a provider's fiat→crypto createOnrampQuote in @sdp/payments, extend the closed quote contract, and wire API persistence plus dashboard rendering.

- Skill: `solana-foundation/integrate-onramp` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add solana-foundation/integrate-onramp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/solana-foundation/integrate-onramp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: solana-foundation (https://skillmd.com/u/solana-foundation)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/solana-foundation/integrate-onramp

---


# Integrate on-ramp

On-ramp = a counterparty buys crypto with fiat, delivered to an SDP-known wallet. Implement `createOnrampQuote` on the package client and add a branch to the API quote dispatch. There is no `executeOnramp` method in the current provider contract.

`createOnrampQuote` is **optional** on `RampProvider` — implement it only if your provider has a lockable quote step.

Choose the closest package client by delivery mode: manual instructions, hosted URL, or session widget — all three are represented under `packages/sdp-payments/src/ramps/providers/`. Provider-specific DB helpers live in `apps/sdp-api/src/routes/payments/handlers/ramps/<id>.ts`.

## Contract

Read the current `RampOnrampQuoteInput` from `packages/sdp-payments/src/ramps/types.ts`. The handler resolves wallet, counterparty, identity/contact, and provider account ids before calling the package client; the client never reads the database.

`PaymentRampQuote` is a closed union discriminated by both `provider` and `deliveryMode` in `packages/sdp-types/src/payments.ts`. Add the provider-specific quote arm, and add a `PaymentRampInstruction` arm for new manual instruction fields:

- `deliveryMode: "manual_instructions"` — return `paymentInstructions` (bank/wire or on-chain funding details).
- `deliveryMode: "hosted"` — return a `hostedUrl` the client renders (widget/redirect).
- `deliveryMode: "session_widget"` — return the session fields required by an embedded SDK/frame.

Prefer the upstream quote/session id. If the upstream does not mint one, use `rampId("ramp_quote")` from `@sdp/payments/ramps/shared` and pass that reference upstream. The webhook or reconciliation path must return the same reference.

## Handler wiring (the DB side)

Add a branch to `apps/sdp-api/src/routes/payments/handlers/ramps.ts`. The handler owns all DB work:

- resolves the counterparty + destination wallet,
- ensures any provider-side customer/account exists (DB-touching `ensure*` helpers live in `apps/sdp-api/src/routes/payments/handlers/ramps/<id>.ts`),
- calls your HTTP-only `createOnrampQuote` with pre-resolved inputs,
- persists the transfer via `persistRampQuoteTransfer` (dedups by `(provider, providerReference)`; `rampQuoteTransferStatus` maps a `manual_instructions` + `pending` quote to `awaiting_payment`). A `reservedTransferId` is minted before the provider call so it can travel upstream as the reference; a provider whose failed calls must still be attributable to a row pre-creates the pending transfer instead and skips the post-quote persist.

Runtime route: `POST /v1/payments/ramps/onramp/quote`, gated by provider availability, metered quota, permissions, and `policyGate`. This route is public OpenAPI today; update `apps/sdp-api/src/openapi/**` when the new provider changes its request/response shape and regenerate owned artifacts.

For `hosted`, decide whether the upstream permits iframe embedding or requires a top-level redirect. Check the provider's CSP / `frame-ancestors` policy and return/render the URL accordingly; the dashboard's default hosted path assumes iframe embedding, so a redirect-only provider needs an explicit renderer instead of inheriting that path.

## Variety

| deliveryMode | On-ramp quote shape |
|---|---|
| `manual_instructions` | bank pay-in or funding instructions built from the provisioned customer/resource |
| `hosted` | signed provider widget `hostedUrl` |
| `session_widget` | embedded provider session credentials |

## Gating — throw, don't fallback

A provider that needs provisioning must fail loud when it's missing: a missing customer link, unverified customer, or unprovisioned funding resource throws `counterpartyNotProvisioned`. Getting the counterparty to a ready state is `counterparty-requirements` — never substitute a default.

## Rules + verify

Shared rules live in `integrate-ramp-provider`. Hot here:

- No fallbacks — missing customer/account/instructions throws; never default them.
- HTTP in the provider; DB (counterparty, wallet, customer, transfer row) in the handler.
- `deliveryMode` arms are a real discriminated union — return exactly one arm's fields; no `any`.
- Update `apps/sdp-web/src/app/dashboard/payments/ramps/` when the provider's quote arm or instruction shape is not already rendered by the chosen delivery mode.
- Verify `@sdp/payments`, focused API quote/persistence tests, OpenAPI generation when changed, and `sdp-web` typecheck/tests for the renderer.

