Integration details
Slippage floors, fees and sponsorship, swap-funded deposits, retries, errors, and polling for a production Earn integration.
These rules apply to every Earn integration. Read them before you take real deposits.
Slippage floors
A floor is the least a transaction may return: minSharesOut on a deposit,
minAmountOut on a withdrawal. A strategy's depositSlippage and
withdrawalSlippage say whether it takes one, as
Read the slippage policy
shows. When a policy is not null, preview first and send the floor: the build
refuses the transaction without it.
A null policy means the transaction cannot enforce a floor: a provider order is priced later, and some sandbox vaults lack the floor instruction. Omit the floor then, because a build refuses one it cannot enforce. In production, nearly every deposit takes a floor. A preview quotes the rate; it does not reserve it.
Compute the floor in integer base units, never with a JavaScript number:
Use the preview's shareDecimals for a deposit floor and assetDecimals for
a withdrawal floor. Start from the policy's defaultToleranceBps, and let
your customer choose another; this helper accepts 1 to 1,000 basis points.
The backend module
ships the same function whenever its strategy needs a floor.
Fees and sponsorship
By default the customer's wallet pays each transaction's network fee and the rent for any account it creates:
| Transaction | Accounts it can create |
|---|---|
| A first deposit into a strategy | The share account, about 0.002 SOL. Some providers also create an account for the wallet, for up to about 0.0035 SOL in all. |
| A withdrawal request | The request's own accounts, about 0.0027 SOL. |
Rent is separate from the deposit and its yield, and it is not part of
earned. Where a request's rent goes when it closes depends on the provider's
program; Hastra returns it to the owner. To pay these costs for your
customer, sponsor the transaction:
- Set
feePayerto a wallet your backend controls when you build. - Have the customer's wallet sign first.
- Check that the message is unchanged and that its fee payer is your wallet, then add your signature to the same transaction.
- Submit the transaction with both signatures.
With raw requests, run step 3 on your backend with your fee payer's key pair:
Pass it the build's transaction and the customer's signed transaction, then
submit what it returns. A missing sponsor signature fails the submit with
400, and changing the fee payer means a new build and new signatures. The
build's sponsored field stays false: it reports whether SDP paid, and SDP
never pays for an external wallet. In the
backend module,
signEarnTransaction runs steps 2 and 3, and createSponsorSigner checks
the fee payer before it signs.
- Sponsor only builds your backend started for an authenticated customer.
- Keep your sponsor's SOL balance small, rate-limit sponsored requests, and watch the spend.
- Store the sponsor's key in your secret manager.
The catalog's feeSponsored describes deposits and withdrawals from a Treasury custody
wallet. It never applies to a customer's wallet.
A full withdrawal from a Kamino strategy leaves the empty share account open. The customer's wallet can close it to recover the rent; do not promise an automatic refund.
Swap-funded deposits
Set sourceTokenMint to fund a deposit with a different supported
stablecoin. SDP adds a Jupiter swap to the deposit's deposit token in the
same transaction, so both happen or neither does. Swap routes need mainnet,
so you cannot test them in sandbox.
When the two do not fit in one transaction, the build returns
requiresSeparateSwap: true with a swap and a followUp instead of
transaction:
- Save the swap's signed bytes, its signature, and the follow-up body.
- Broadcast the swap yourself and confirm it.
- Build the follow-up deposit with its original floor and fee payer.
SDP does not record the standalone swap as a movement, so your app owns its recovery. Resolve an unfinished swap before you start another.
Retry safely
Every Earn submit takes an Idempotency-Key. Use a new random UUID for each
new deposit, withdrawal, request, or cancellation, and save it with the
signed transaction before the first send. Never put customer data in an
idempotency key.
| Situation | What SDP does | What you do |
|---|---|---|
A submit times out or returns 5xx. | Nothing twice. | Retry with the same idempotency key and body. |
You retry with the same idempotency key and transactionId. | Returns the original movement in its current state, with replayed: true, even if the signed bytes differ. | Track it as usual. |
| You reuse an idempotency key for a different build. | 409 CONFLICT. Nothing runs. | Retry the original build, or use a new idempotency key for a new action. |
| You submit the same build under a new idempotency key. | 409 CONFLICT. | Find the original action and its idempotency key. |
| The blockhash expired before you submitted. | Records the original signed bytes, which may already have landed. If they never did, the movement ends failed with Transaction blockhash expired before confirmation. | Submit the original and track it. Do not rebuild until it fails. |
A late withdrawal request returns 409 CONFLICT. | Waits to see the exact signed transaction finalized. | Retry the same bytes and key. |
| Recovery finds the transaction finalized as failed. | 400 TRANSACTION_FAILED. Nothing moved. | Build a new transaction. |
| An unsigned build expired. | Nothing has moved. | Build again before asking for a signature. |
| The preview has blocking issues. | Nothing has moved. | Resolve them and preview again. |
Earn's submits keep their own idempotency rules, which replace the shared
Idempotency page's. A submit without
a key returns 400 BAD_REQUEST, and a misused key returns 409 CONFLICT,
never 422. A key stays bound to its movement for good, with no 24-hour
window, so never reuse one for a new action.
Submit withdrawal requests and cancellations through SDP promptly. If a wallet broadcasts one itself and SDP first sees it after its build expires, SDP may be unable to track it. Keep unresolved actions visible, and never assume no funds moved.
Errors
Every error has the shape { "error": { "code", "message", "details" } }.
The codes Earn integrations meet most:
| Status | Code | Meaning | What to do |
|---|---|---|---|
400 | BAD_REQUEST | A field is invalid, the Idempotency-Key is missing, or the signed transaction differs from the build. | Read message and details, fix the request, and build again when the transaction changed. |
400 | TRANSACTION_FAILED | Recovery found the submitted transaction finalized as failed. | Build a new transaction. |
403 | INSUFFICIENT_PERMISSIONS | The API key lacks the grant in details.action. | Add the grant to the API key. |
403 | FORBIDDEN | details.reason is provider_not_entitled: your organization is not activated for the provider. With provider_not_offered, SDP takes no new deposits for it. | Ask your SDP contact to activate it. Withdrawals still work either way. |
404 | NOT_FOUND | No such strategy, position, movement, build, or request in your project. | Check the ID and the project. |
409 | CONFLICT | An idempotency key or a signed transaction was already used differently, a late withdrawal request awaits proof that it landed, or a request cannot be canceled yet. | See Retry safely. Retry a late request with its exact bytes and key; for a cancellation, wait for expiredCancelable. |
429 | RATE_LIMITED | You are over your rate limit. | Wait for Retry-After, then retry. |
501 | NOT_IMPLEMENTED | The provider cannot quote this operation. | Continue without the preview when the floor policy is null. |
503 | PROVIDER_UNAVAILABLE | SDP could not read the provider's live state or reach a dependency. | Retry with backoff, and show the action as unavailable meanwhile. Retry a submit with the same idempotency key and body. |
See Errors for the shared error model.
Poll politely
Track a movement by polling its GET route about once a second for the
first minute, then every five seconds. Stop at finalized or failed, and
treat 429 as a signal to slow down. Withdrawal requests can take hours or
days, so poll them every minute or two, and restore them on startup instead
of holding a loop open. The backend module's wait functions follow this
schedule. See
Rate limits.