DCA
Recurring swaps on a fixed schedule — config fields, progress tracking, and modification.
A dca order spends a fixed amountPerCycle of the input mint on a schedule until totalAmount is exhausted. The user funds the whole totalAmount up front in one deposit; Titan holds it in their manager and swaps a slice at each cycle.
DCA is the only order type that can be modified after creation, and the only one that produces more than one execution.
Config
{
"orderType": "dca",
"userPubkey": "<user's external Solana wallet>",
"config": {
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"totalAmount": "1000000000",
"amountPerCycle": "100000000",
"cycleFrequencySeconds": 86400,
"minOutputPerCycle": "0",
"maxOutputPerCycle": "0",
"startAt": 1735689600,
"expiresAt": 1767225600
}
}inputMint
✅
SPL mint address. Must differ from outputMint.
outputMint
✅
SPL mint address.
totalAmount
✅
Total input the user spends over the order's lifetime. Funded in a single deposit at creation.
amountPerCycle
✅
Input spent per cycle. Must be worth at least $10 USD by default — your tenant's configured floor.
cycleFrequencySeconds
✅
Seconds between cycles. Minimum 60.
minOutputPerCycle
❌
The cycle aborts if the quote comes in below this.
maxOutputPerCycle
❌
The cycle aborts if the quote comes in above this.
startAt
❌
Unix seconds. The first cycle waits until this time.
expiresAt
❌
Unix seconds. The order auto-expires at this time.
The top-level fields — userPubkey, outputRecipientAddress, platformFee, onboardIfNeeded — are shared by every order type and documented in Creating Orders.
amountPerCycle is priced at request time to enforce the minimum. USDC and USDT count as $1.00; everything else goes through the oracle. Below the floor returns 400 MIN_NOTIONAL_NOT_MET with details echoing mint, amount, usdCents, and requiredCents. If the oracle can't price the mint you get 503 PRICE_ORACLE_UNAVAILABLE — transient, retry shortly.
Titan sponsors the Solana network fees on recurring cycle executions, so the manager never needs SOL to keep the schedule running.
Read progress
GET /dca/{orderId} returns the order with its progress counters filled in:
nextExecutionAt is what makes DCA cheap to poll — you know when the next cycle lands, so you don't need a tight loop. GET /orders/{orderId}/executions gives you one row per cycle with executionType: "dca_cycle", most recent first, capped at 500 rows. The full field list is in Order & Execution Schema.
Returns 404 NOT_FOUND if the order doesn't exist, isn't a DCA order, or isn't this user's.
Modify
PATCH /dca/{orderId} edits an active or paused DCA order — this is the one order type that supports it. Editable fields: amountPerCycle, cycleFrequencySeconds, totalCycles, totalAmount, minOutputPerCycle, maxOutputPerCycle.
Most edits apply in place:
Changing totalAmount is different, because the difference has to be deposited or withdrawn on-chain. That turns modify into a two-step flow like creation, and the response carries an unsigned transaction:
Have the user sign it, then submit to POST /dca/{orderId}/modify/confirm with the same cyclesCompleted and config you got back.
Cycle executions pause while a modification is being signed — the user's signed totalAmount can't be applied to a different on-chain state than the one they reviewed. The lock auto-releases after 30 seconds; if it expires before confirm you get 409 LOCK_EXPIRED and re-run the modify intent. If a cycle lands mid-signature, cyclesCompleted won't match and confirm returns 400 MODIFICATION_ERROR — re-fetch the order and let the user re-confirm.
HTTP
error.code
When
400
VALIDATION_ERROR
Bad config.
400
MIN_NOTIONAL_NOT_MET
New amountPerCycle below your tenant's floor. Only checked when amountPerCycle changes.
400
MODIFICATION_ERROR
A server-side guard rejected the change (e.g. shrinking totalAmount below what's already been spent), or cyclesCompleted didn't match.
400
INVALID_REQUEST
/modify/confirm body shape invalid.
400
TRANSACTION_EXPIRED / TRANSACTION_TAMPERED
The 5-minute window elapsed, or the signed tx doesn't match the unsigned one.
404
NOT_FOUND
Order missing or not yours.
409
LOCK_FAILED
Another modification is already in progress.
409
LOCK_EXPIRED
The 30-second lock elapsed before confirm arrived.
503
PRICE_ORACLE_UNAVAILABLE
Couldn't price a non-stable inputMint. Retry shortly.
Lifecycle
Three behaviours are DCA-specific. A future config.startAt parks the order in pending until that time — it's the only way an order reaches that status, and without startAt confirm goes straight to active. POST /orders/{orderId}/retry can flip a failed order back to active, but only in the brief window before its unspent input is auto-returned. And an expired DCA order is not auto-returned — unlike an expired trigger order, you have to call order-level withdraw for it.
Related pages
Creating Orders — the two-step flow and the shared top-level fields
Managing Orders — pause, resume, retry, cancel
Lifecycle & Polling — statuses and polling cadence
Last updated

