Skip to content
Zuuppa
Dashboard

Core Concepts

Core Concepts

Read this before integrating. These concepts appear throughout the API — how payments are represented, priced, settled, and accounted for.

Unique deposit addresses#

Zuuppa derives a unique Solana deposit address for every payment. You never reuse an address, and you never handle private keys — the platform holds one master seed and re-derives the signing key for any address on demand to sign sweeps and refunds.

  • Each payment intent is allocated a stable integer index (its derivation_index), which maps to exactly one deposit address.
  • You reference a payment by its index (to poll /status, sweep, or cancel).

You never see or handle private keys.

Payment intent#

A payment intent is the central object. It represents “I expect a payment to this address.” Creating one (POST /intents) does three things:

  1. Allocates the next index (atomic, no collisions even under high concurrency).
  2. Derives the unique deposit address for that index.
  3. Persists the intent.

An intent records:

FieldMeaning
derivation_indexThe stable integer id. Use this to poll status and reference the payment.
addressThe Solana deposit address to show the payer.
price_usd_centsThe USD price in integer cents. Every intent is USD-priced.
mintThe pay-in asset: null = native SOL, else an SPL mint address. null until the buyer selects a token.
mint_decimalsDecimals of the token (null for SOL, and null until selection).
expected_lamportsExpected amount in base units. null until the buyer selects a token, which is when the USD price is converted and the amount locks.
received_lamportsAmount received so far, in base units.
statusLifecycle state — see Payment lifecycle.
referenceYour own id (order id, user id, memo). Optional, opaque to the server.
expires_atWhen the payment window closes. Always 10 minutes after creation.
Warning

Fixed 10-minute window

Every intent expires exactly 10 minutes after it's created. This is a fixed product guarantee: the expires_in_secs request field is accepted for backward compatibility but ignored — you cannot lengthen or shorten the window. An intent that isn't paid in time is auto-cancelled (partial funds are refunded where possible; see the lifecycle).

Note

Naming note

The fields are called expected_lamports / received_lamports for historical reasons, but they hold base units of the intent's asset: lamports for SOL, token base units for an SPL token. Treat them as generic integer amounts.

Pricing#

Every intent is priced in USD: you set the amount in integer USD cents with amount_usd_cents — anything from a checkout total to a tip — and the buyer chooses which asset to pay in.

Creating an intent requires accepted_tokens: the list of assets the buyer may pay in. Each must be a token the platform accepts — see GET /allowed-tokens for the current set (SOL and USDC today).

At creation, mint, mint_decimals, and expected_lamports are all null. The buyer picks one of accepted_tokens (POST /intents/select-token, authorized by the intent's client_secret), the server converts the USD price to that asset's base units at spot, and the amount locks. From that point the intent is single-asset. GET /intents/quote previews every option's amount without locking anything.

Warning

The legacy fixed-token path was removed

The path where the caller set the amount directly in a token's base units (expected_lamports / mint at create) now returns 400. Every payment carries a USD price so it can be valued in stats and so an account can accept several assets for one payment.

Assets: the platform allowlist (SOL + USDC today)#

An intent may only accept tokens on the platform's curated allowlist — today SOL and USDC. The set is managed by the operator, so support for a new token is added centrally (no code change) and every merchant can then use it; read the live set from GET /allowed-tokens.

Each intent settles in exactly one asset, fixed once the buyer selects it:

  • No mint → native SOL. Amounts are in lamports (1 SOL = 1,000,000,000 lamports).
  • mint set → that SPL token. Its decimals come from the allowlist entry (captured on-chain when the token was added), and amounts are in the token's base units.

accepted_tokens may list several allowed assets; exactly one of them becomes the intent's asset at selection time.

Base units = the smallest indivisible unit. To convert:

Text
base_units = ui_amount × 10^decimals
ui_amount  = base_units ÷ 10^decimals

Examples:

  • 0.5 SOL (9 decimals) → 500000000 lamports.
  • 100 USDC (6 decimals) → 100000000 base units.
  • 100 units of a 9-decimal token → 100000000000 base units.

Both classic SPL Token and Token-2022 mints are supported automatically.

The settlement lifecycle#

Once a payment arrives, the server settles it automatically: it sweeps the funds to your destination wallet, refunds any excess, and so on. You don't trigger this. You just observe the status.

The full status table and every branch of the flow — happy path, overpayment, underpayment, expiry, and cancellation — live on their own page:

Tip

See the full lifecycle

Payment lifecycle → covers all status values, the action aliases, the visual flow diagrams, and cancellation behavior. The status you care about most is swept: the money is yours and settlement tells you exactly how much.

What “settled” means and the settlement object#

When an intent reaches swept, /status includes a settlement object: the exact on-chain amounts that were delivered. This is your source of truth for accounting:

200 OKsettlement (on swept)
JSON
"settlement": {
  "asset": "SOL",
  "decimals": 9,
  "destination_amount": 499995000,
  "destination_ui": 0.499995,
  "platform_fee_amount": 0,
  "platform_fee_ui": 0.0,
  "signatures": ["4bd..."]
}
destination_amountinteger
Base units that reached your destination wallet (net of platform fee, and for overpayments net of the refunded excess).
platform_fee_amountinteger
Base units sent to the platform wallet (0 if fees off).
asset / decimalsstring / integer
So you can format correctly ("SOL" or the mint).
signaturesstring[]
The on-chain sweep transaction signature(s) for verification.

A payment taken for a seller adds a third leg, marketplace_fee_amount, plus marketplace_fee_cents. See Businesses, sellers, and marketplaces.

Note

Use base units for accounting, not the floats

destination_amount can be less than received_lamports, and that's expected. received_lamports is the gross amount the payer sent; destination_amount is what you actually net after fees/refunds. For a ledger, prefer the integer base-unit fields or the exact decimal strings that webhooks carry — the _ui floats are for display only.

Fees, refunds, and edge cases#

The server handles these automatically:

  • Network fee. Every sweep pays the tiny Solana network fee (~5000 lamports). For SOL it comes out of the swept amount; for tokens the platform's fee-payer wallet fronts it (the deposit address holds no SOL).
  • Platform fee (optional). The platform's own cut: a percentage plus a flat amount of each sweep, diverted to the platform wallet in the same transaction (no extra network fee). You keep 100% of your revenue minus this fee. The fee in force is published at GET /config.
  • Marketplace fee (marketplace payments only). A third leg in the same sweep, paying the marketplace its cut of a seller's payment.
  • Overpayment. The expected amount is swept to the destination and the excess is refunded to the sender, in one atomic transaction, for SOL and SPL tokens. (SOL takes the network fee out of the excess; a token refund is fronted by the fee-payer, so a token overpayer gets the full excess back.)
  • Underpayment + expiry. SOL: the partial amount is refunded to the sender. Token: held for manual handling.
  • Wrong asset. A token sent to an intent expecting a different asset is automatically refunded to the sender on an independent track (reported in token_refunds). SOL sent to a token intent is held for manual review.
  • Funds sent after the order finished. A deposit address is used once and then goes dead; nothing keeps watching it. Don't reuse an address or show it again after checkout completes — generate a new intent. Anything that lands there is visible to the platform and can be returned on request.

Businesses, sellers, and marketplaces#

One login can own several businesses (accounts). Each business has its own deposit index space, API keys, sweep destination, platform-fee settings, webhook, and stats. On the API, the sk_ key you present decides which business you are acting as.

A business can be approved as a marketplace, which lets it create sellers: accounts it takes payments for and pays out to directly, without ever holding their funds. (You apply to become a marketplace from your dashboard.)

BusinessSeller
Created byThe owner, in the dashboardA marketplace, via POST /marketplace/sellers
Can sign inYesNo. No login, no dashboard, no API key.
Own webhookYesNo. Its events go to the marketplace's URL, signed with the marketplace's secret.
Payout addressIts ownIts own, set and changed by the marketplace

A payment for a seller is an ordinary POST /intents plus one header, X-Zuuppa-On-Behalf-Of: <seller_id>, and one extra requirement: customer.email is mandatory, because a seller has no login and the platform sends no email. Every failure mode for that header answers a uniform 404 seller not found, so a caller can never probe for the existence of accounts it does not own.

The sweep then splits three ways in one transaction:

LegWhere it lands
The seller's sharethe seller's sweep_destination
The marketplace's cutthe marketplace's fee_wallet, falling back to the marketplace account's own sweep_destination
The platform feethe platform wallet

The cut rate resolves once at create time and is frozen onto the intent as marketplace_fee_bps: the request's application_fee_bps, else the seller's marketplace_fee_bps override, else the marketplace's default_fee_bps. The cap is 5000 bps. If neither a fee_wallet nor a marketplace sweep_destination is set, the cut is forgone rather than the payment failing. A fee leg that would land below the rent-exempt minimum is dropped and rolled into the seller's leg, and marketplace_fee_cents records 0. Marketplace splitting is SOL only today.

Full endpoints are in the Marketplace API.

Idempotency & safety guarantees#

  • Idempotent creates. Pass a stable reference to POST /intents and a retry returns the same intent instead of a second deposit address.
  • Detection is idempotent. The same on-chain transaction is never counted twice (keyed by (address, signature)).
  • Crash-safe & race-safe settlement. No two workers ever sweep the same intent; a settlement interrupted mid-flight is retried automatically.
  • Retries with backoff. Transient failures retry with exponential backoff, then dead-letter to a *_failed status for manual review (never silently lost).
Note

Next

Continue to Authentication for the three credentials, or jump to the Payments API.

© 2026 Zuuppa. All rights reserved.