Integrate on-ramp

SkillMedia

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

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Integrate on-ramp skill

What this skill tells your AI

The instructions your AI receives, as published by solana-foundation/solana-developer-platform in .agents/skills/integrate-onramp/SKILL.md and read by ahel’s review.

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

deliveryModeOn-ramp quote shape
manual_instructionsbank pay-in or funding instructions built from the provisioned customer/resource
hostedsigned provider widget hostedUrl
session_widgetembedded 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.

Signals

GitHub stars
53
Forks
23
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
integrate-onramp
Source
github.com/solana-foundation/solana-developer-platform