diff --git a/.claude/rules/product-learning.md b/.claude/rules/product-learning.md index 8d67a24a..e65dd1c9 100644 --- a/.claude/rules/product-learning.md +++ b/.claude/rules/product-learning.md @@ -208,6 +208,13 @@ Keep entries concise — one line if possible, a short paragraph if needed. # Trigger Order API +## Sources + +- **Live API:** `api.jup.ag/trigger/v2` (requires `x-api-key`; authenticated routes also need a JWT `Authorization: Bearer`). This is the gateway host used for all live verification. +- **OpenAPI spec:** `openapi-spec/trigger/v2/trigger.yaml` in this repo. The price-order surface was complete; the DCA paths were added 2026-06-26 from live + source. The hand-written source-repo doc (`docs/dca-api.md`) drifts from live (e.g. create response), so live wins. +- **Source repo:** `jup-ag/trigger-order`, cloned locally at `/Users/anmol/Dev/trigger-order` (corrects the older `Documents/Projects/...` path). The V2 API is `api-v2/` (Hono + `@hono/zod-openapi` on Cloudflare Workers). DCA route handlers: `api-v2/src/routes/v2/orders/dca/**`; schemas in `dca/schema.ts`; limits in `dca/validation.ts`; deposit discriminated union in `deposit/schema.ts`. The repo also ships its own integration docs (`docs/dca-api.md`, `dca-runbook.md`, `dca-keeper-internals.md`, `dca-system-design.md`) and a reference client (`api-v2/scripts/test-ui/dca/dca.html`). +- **SDK:** No public JS SDK for Trigger V2 DCA found in the repo or as a `@jup-ag/*` package. Integrators call the REST API directly. + ## Architecture - [2026-05-18] Trigger V2 API is the umbrella for both LOv2 (limit orders, price-triggered) and DCAv2 (recurring, time-triggered). They share the same API surface with different paths: `/orders/price/*` (LO) and `/orders/dca/*` (DCA). Internally both are "trigger orders" — DCA is just time-triggered. Source: Evan, owner of Trigger + Recurring APIs. Docs currently only expose the price (LO) paths; DCAv2 routes exist in source but are not yet documented. The standalone Recurring API in docs is V1 only. @@ -224,6 +231,43 @@ Keep entries concise — one line if possible, a short paragraph if needed. - [2026-04-28] No integrator fees on Trigger V2, no timeline. No atomic workaround: the keeper executes the tx and the order is opaque to integrators. Only option is a separate transfer transaction outside the order flow. (Confirmed by YY.) - [2026-05-08] Upcoming Trigger V2 craft-deposit enforcement: `POST /trigger/v2/deposit/craft` will require `orderType: "price"` and `orderSubType` (`single`, `oco`, or `otoco`) for price-order deposits. Create-order payloads do not receive these new craft-deposit fields. +## DCA v2 (live-verified 2026-06-26, mainnet lifecycle on `api.jup.ag/trigger/v2`) + +- [2026-06-26] **DCA v2 is live, well-adopted via the jup.ag frontend, and now documented for integrators** (`trigger/dca.mdx`, PR pending). The earlier 2026-03-10 "intentionally excluded from docs" framing was wrong: integrator docs simply had not been written yet (confirmed by user 2026-06-26), it was never a deliberate gate. Full lifecycle ran end-to-end with a funded test wallet: auth(JWT) → `GET /vault` → `POST /deposit/craft` (`orderType:"dca"`) → sign → `POST /orders/dca` → `POST /orders/dca/cancel/{id}` → sign → `POST /orders/dca/confirm-cancel/{id}`. Deposit sig `4R18eTrkDvmZFPq87pznBPpVCpk1G2FQapWVks4ELLNYxAQZdwoDaxnnAHN2ZvBFrRvbnJSAPTnhutgj8ZzLFwgV`, withdrawal sig `3fjoTu8AaSkzxPshgGUviUdnNy6rKRiyyuxA1A87SyDX6Z2NjQfVBEw8NptQVEeYNircD6oaZuunB7dUYR62QXLt`. Source of behaviour: live API > `jup-ag/trigger-order` `api-v2/src/routes/v2/orders/dca/**`. +- [2026-06-26] **Trailing-slash 404 footgun:** `POST /trigger/v2/orders/dca` works; `POST /trigger/v2/orders/dca/` returns `404 Not Found` at the gateway. Same on `/orders/price` vs `/orders/price/`. The canonical reference client (`scripts/test-ui/dca/dca.html`) uses no trailing slash. A trailing slash on create looks identical to a gateway gap — confirm route reachability by comparing the no-slash variant (returns 400 validation) before concluding an endpoint is unexposed. +- [2026-06-26] **Per-order detail path requires the `dca` segment:** `GET /trigger/v2/orders/history/dca/{id}` returns the order; `GET /trigger/v2/orders/history/{id}` returns `404 {"error":"Order not found"}` for DCA orders. The LIST endpoint works at both `/orders/history` and `/orders/history/dca` (both 200), but only `/orders/history/dca` is DCA-scoped/documented. +- [2026-06-26] **Create response includes `txSignature`** (the on-chain deposit signature): live returns `{id, txSignature}`. The source repo's `docs/dca-api.md` documents `{id}` only — source-doc drift, live wins. Matches the public price-order docs which already document `{id, txSignature}`. +- [2026-06-26] **`POST /orders/dca` submits the deposit synchronously** — creating a DCA order moves funds immediately (the signed deposit lands on-chain during the call), it does not just register intent. Cancelling before any round fills refunds the full deposit (`refundAmount` = `inputAmountInitial`, `roundsRemaining` = `orderCount`). +- [2026-06-26] **Limits (prod):** min **$10 per round** (`MINIMUM_ORDER_AMOUNT_USD=10` in `api-v2/wrangler.toml`, 0.20 USD tolerance, validated against `inputAmount/orderCount`); `orderCount` 2–100; `intervalSeconds` 60–31,536,000; `beginFillAt` ≤ 30 days out; max **10 active** DCA orders per wallet. `retryWindowSeconds = clamp(intervalSeconds*0.5, 30, 7200)` (observed 30 for interval 60). +- [2026-06-26] **`deposit/craft` for DCA:** `orderType:"dca"` with NO `orderSubType` (passing it → `400 "orderSubType must be omitted when orderType is 'dca'"`). Response has `inputTokenAccount` (deterministic NATA), no `outputTokenAccount` (that is OTOCO-only). `receiverAddress` = the vault pubkey. +- [2026-06-26] **Cost is not zero:** the deposit + withdrawal round-trip cost ~0.00208 SOL in fees/rent on the test wallet (USDC fully refunded, SOL slightly down). Worth noting for integrators expecting a free cancel. +- [2026-06-26] **`jlEnabled` (boolean) appears in the live order-detail response.** RESOLVED 2026-06-27: it is the Earn While You Wait (Jupiter Lend yield) opt-in, not a routing flag — now documented and in the public OpenAPI schema. See the 2026-06-27 entry below. +- [2026-06-26] **`inputAmountRemaining` is not zeroed after a cancelled withdrawal** — a cancelled order still reported `inputAmountRemaining: "20000000"` while `events` recorded the withdrawal. Treat `state`/`events`, not `inputAmountRemaining`, as the source of truth for whether funds were returned. +- [2026-06-26] **`price_conditional` verified live e2e** (second funded run): create + deposit `PPbp1QbbKDPjpHM3ScKyZBnCtLKKqmWwDeVj7USdBeEoNQsA1hkN35UQfrY2cQpyAB1pfVi9FdfciYdz7qUXBQA`, withdrawal `53CQkssz7oGmbUxp3W5tsjw8Vhpv8FKVXjfWb3ZyPW71gsAm4qHvK2FjEDKt9RNDVGesoNiMA7moGR1rKbzhEfqb`, USDC refunded in full. Order detail echoes `minPriceUsd`/`maxPriceUsd`/`triggerMint`; `retryWindowSeconds` = 7200 for `intervalSeconds` 86400 (confirms the upper clamp). Populated `GET /orders/history/dca` list verified (was only ever seen empty before). +- [2026-06-26] **`triggerMint` is REQUIRED for `price_conditional`** — contradicts the source repo's `docs/dca-api.md` claim that it "defaults to outputMint". Live: `price_conditional` without `triggerMint` → `400 {"orderType":"Price-conditional orders must have at least minPriceUsd or maxPriceUsd set","triggerMint":"Price-conditional orders must specify a triggerMint"}`. Public docs corrected to mark `triggerMint` required for price-conditional. +- [2026-06-26] Real DCA create validation errors (zod, body-level, fire before the deposit is consumed): `orderCount: 1` → `{"orderCount":"Too small: expected number to be >=2"}`; `time_based` + `minPriceUsd` → `{"orderType":"Time-based orders cannot set minPriceUsd or maxPriceUsd"}`. All under top-level `{"error":"Request validation failed","details":{...}}`. +- [2026-06-26] **Fill VERIFIED live e2e (third funded run).** A `time_based` order due now filled round 1 in ~3s: fill tx `HuNMpjFkTjVrCYMN6gTPWV9ay3GSxTxM7sZj4WNzjvzQoe4J5CR71MnTpuMKH9rtkDoDCTWu6rYRM12oR9cpbb4` (on-chain `err=null`), 10 USDC -> 137589159 lamports SOL. Confirmed: (1) **fill output is delivered straight to the taker wallet** (wallet SOL += outputAmount; the deposit only ever holds the unfilled remainder); (2) **the keeper pays the fill tx fee** (fill fee 1378618 lamports came off the keeper, taker only paid deposit+withdraw fees); (3) after a fill, `roundsFilled`/`fillPercent`/`outputAmountTotal`/`inputAmountUsed` update and `nextFillAt` advances by `intervalSeconds`; (4) `fill` event shape: `{type:"fill", roundNumber, inputAmount, outputAmount, txSignature, state:"success"}`; (5) `retryWindowSeconds` = 1800 for `intervalSeconds` 3600 (mid-range clamp confirmed). Cancelling after round 1 refunded only the remaining round (`refundAmount` 10000000, `roundsRemaining` 1). All DCA-doc response examples are now real captured responses, including the filled detail. + +### Edge cases (live-verified 2026-06-26, ~60 cases) + +- **`orderCount` has NO upper bound on the deployed API.** `orderCount` of 100/101/1,000/100,000/1,000,000 all passed zod and failed only the per-round `$10` min ("Each order must be at least 10 USD"), never a "Too big". The pr-461 source has `.max(100)` but the deployed worker does not enforce it. Only `min(2)` is a zod rule. Effective max = floor(total input USD / 10). **Docs corrected** from the wrong "2–100". +- **`triggerMint` must be a supported price mint, not just input-or-output.** The zod refinement accepts inputMint or outputMint, but the server then rejects an unsupported price leg: `triggerMint = inputMint` (USDC) → `400 "Trigger mint is not supported"`. Use the volatile leg (outputMint). Source's "defaults to outputMint" is wrong (it is required) AND the input leg is not accepted in practice. +- **DCA orders are NOT editable.** `PATCH /orders/dca/{id}` → `404` (no update endpoint; price orders have one). Change = cancel + recreate. +- **API key not enforced when a valid JWT is present:** `GET /vault` with NO `x-api-key` but a valid Bearer → `200`. Same gateway behavior as Prediction. Keep "API key required" in docs (gateway-side, can change). +- **`GET /vault/register` when a vault exists → `409`** with the existing vault in the body (resolves the long-standing 200-vs-201 open question: it's 409). +- **Dust:** `inputAmount 20000001`, `orderCount 2` → `amountPerRound: "10000000"` (floored); the extra unit is added to the last round, not reflected in `amountPerRound`. +- **`price_conditional` gate verified both directions:** out-of-band (`maxPriceUsd` 10 vs ~$73 market) stayed `active`/`roundsFilled 0` with no fill for 84s; in-band (`maxPriceUsd` 100000) filled in ~20s and delivered output to the wallet, identical to `time_based`. +- **Auth errors:** invalid signature → `400 "Challenge not found"` (not 401); missing/bad JWT → `401`; `userPubkey` ≠ JWT sub → `403 "Forbidden: userPubkey must match authenticated user"`. +- **Cancel state machine:** confirm with wrong `cancelRequestId` → `400 "Withdrawal transaction not found in cache"`; garbage tx → `400 "Invalid transaction format"`; a different valid signed tx → `400 "Invalid withdrawal transaction: Transaction signers modified"`; re-`cancel` while `withdrawing` → `200` (idempotent re-craft); cancel a cancelled order → `400 "Cannot cancel DCA order in 'cancelled' state"`; confirm a cancelled order → `400 "DCA order not in cancellation state"`. +- **Below-min create is rejected before any funds move** (USDC balance byte-identical across the 400). +- [2026-06-26] **Max-active cap VERIFIED live** (wallet topped up to >$200, rebalanced to USDC via Ultra): created 10 concurrent USDC→SOL orders (all `active`), and the 11th create returned `400 "Maximum of 10 active DCA orders allowed per user"`. Cancelling all 10 returned the active count to 0 and refunded the full $200. The cap is exactly 10, counting states `depositing|active|executing|withdrawing`. +- [2026-06-26] **Native-SOL-input DCA VERIFIED live** (SOL→USDC): the deposit accepts native SOL and wraps it into a deterministic token account (`inputTokenAccount` returned, same shape as SPL input); `amountPerRound` is in lamports (0.14 SOL/round for a 0.28 SOL, 2-round order); the full deposit is recovered on cancel (net −0.00008 SOL = fees only). Native SOL works as the input mint, not just SPL tokens. +- [2026-06-27] **DCA fills execute through Ultra (keeper source).** `keeper/internal/app/pkg/dcaexecutor/dcaexecutor.go` `prepareSwap` builds each round via `ultra.GenerateSwapTx` with `MinimumOutputAmount: ""`, `AutoSettle: true`, `Taker = vaultPubkey`, `Payer = gasPayer`, `User = userPubkey`, then `submitSwap` → `ultra.ExecuteTx` (Privy). Consequences for docs: (1) **slippage is Ultra/RTSE-managed per round, with no integrator slippage param anywhere in the DCA API** (grep of `orders/dca/**` + keeper = no create-side slippage); (2) **each round pays the standard Jupiter swap fee** (`feeBps := swapResp.FeeBps`; `fee = vol/10000 * FeeBps`), there is no separate DCA/integrator fee, and the fee is not surfaced in the public history response; (3) output `AutoSettle`s straight to the user wallet (confirms the live observation); (4) the keeper's `gasPayer` pays network/priority fees. Documented in `trigger/dca.mdx` "Fees and slippage". +- [2026-06-27] **Failed fills:** the executor has `handleFailedFill` + `recoverStuckOrder`; `ExecutionError.Retryable` drives retry within the round's retry window, then reschedule to the next interval. The `failed`/`rescheduled` `DcaEvent.state` values are from source/schema; an actual failed fill was NOT observed live (only successful fills and price-conditional out-of-band no-fills). Still a candidate for live verification. +- [2026-06-27] **⚠️ UNRELEASED — DO NOT PUBLISH.** Earn While You Wait is live on the API and verified e2e, but it is **not publicly released**: LO/DCA Linear project **LODCA-133 is still In Progress** (state "started", owner Evan Chng; backend PR #516 merged 2026-06-24, ~3 days before this note). All public docs for it are **commented out** in `trigger/dca.mdx` (`{/* */}`) and `openapi-spec/trigger/v2/trigger.yaml` (`#`). Uncomment ONLY when the DCA team confirms launch. (Caught because the user searched the org and questioned the release status.) +- [2026-06-27] **`jlEnabled` RESOLVED + verified live e2e: it is the "Earn While You Wait" opt-in** — idle stablecoin earns Jupiter Lend yield between rounds. (My earlier "not in the repo" note was wrong; see the stale-clone note below.) Rules, all verified live: `jlEnabled:true` requires `orderType: time_based` (price_conditional → `400`), a **stablecoin input** (SOL → `400 "jlEnabled is not supported for this input mint"`), and a **JL deposit craft carrying `jlMint`** (a normal deposit → `400 "JL orders require the JL deposit craft. Please re-craft the deposit with jlMint."`). `jlMint` = the Jupiter Lend earn token whose `assetAddress` is the input mint, from `GET /lend/v1/earn/tokens` (USDC → `9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D` jlUSDC; wrong value → `400 "jlMint does not match the JL token for this input mint"`). The JL craft response returns `jlTokenAccount` (the Lend position) instead of `inputTokenAccount`. Full lifecycle ran (order `019f0592-966e-736b-aedd-80fa56e2e399`); the public detail surfaces only `jlEnabled` (not jlMint/jlYieldAccrued); cancel unwound cleanly (USDC delta −0.000003 = fees). Documented in `trigger/dca.mdx` "Earn While You Wait". +- [2026-06-27] **The local `trigger-order` clone (branch `pr-461`) is STALE vs the deployed API + org default branch.** Two divergences proven live: the entire `jlEnabled`/Earn-While-You-Wait feature is absent from pr-461 but live; `orderCount.max(100)` exists in pr-461 but is not enforced live. **Always source-cite against the org default branch (or the live API), not this local checkout.** (The org-wide GitHub search the user ran found `jlEnabled` across 11 files; my local grep found none.) + ## Source Code - [2026-03-10] V2 API source: `Documents/Projects/trigger-order/api-v2/` — Hono.js on Cloudflare Workers with Zod OpenAPI validation. @@ -235,6 +279,7 @@ Keep entries concise — one line if possible, a short paragraph if needed. - [2026-03-10] Vault register: source returns 200 but docs say 201. Which is correct? - [2026-03-10] Are `/vault/link` endpoints needed in public docs? +- [2026-06-26] ~~DCA order detail returns `jlEnabled` — what does it control?~~ RESOLVED 2026-06-27: it is the Earn While You Wait (Jupiter Lend yield) opt-in, settable on create, now documented. --- diff --git a/.gitignore b/.gitignore index f37d460c..e9376463 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Local files .env audit/ +dx-findings/ manualmode.zip Private & Shared/ static/files/brand-kit/jupiter-brand-kit/ diff --git a/api-reference/trigger/cancel-dca.mdx b/api-reference/trigger/cancel-dca.mdx new file mode 100644 index 00000000..0c76ef07 --- /dev/null +++ b/api-reference/trigger/cancel-dca.mdx @@ -0,0 +1,5 @@ +--- +title: "Cancel DCA Order" +description: "Initiate cancellation of a DCA order and craft the withdrawal transaction" +openapi: /openapi-spec/trigger/v2/trigger.yaml post /orders/dca/cancel/{id} +--- diff --git a/api-reference/trigger/confirm-cancel-dca.mdx b/api-reference/trigger/confirm-cancel-dca.mdx new file mode 100644 index 00000000..0595fbe5 --- /dev/null +++ b/api-reference/trigger/confirm-cancel-dca.mdx @@ -0,0 +1,5 @@ +--- +title: "Confirm Cancel DCA Order" +description: "Submit the signed withdrawal transaction to finalize a DCA cancellation" +openapi: /openapi-spec/trigger/v2/trigger.yaml post /orders/dca/confirm-cancel/{id} +--- diff --git a/api-reference/trigger/create-dca.mdx b/api-reference/trigger/create-dca.mdx new file mode 100644 index 00000000..29943c06 --- /dev/null +++ b/api-reference/trigger/create-dca.mdx @@ -0,0 +1,5 @@ +--- +title: "Create DCA Order" +description: "Create a recurring dollar-cost averaging order" +openapi: /openapi-spec/trigger/v2/trigger.yaml post /orders/dca +--- diff --git a/api-reference/trigger/dca-history.mdx b/api-reference/trigger/dca-history.mdx new file mode 100644 index 00000000..50915d78 --- /dev/null +++ b/api-reference/trigger/dca-history.mdx @@ -0,0 +1,5 @@ +--- +title: "List DCA Orders" +description: "List your DCA orders with full event history" +openapi: /openapi-spec/trigger/v2/trigger.yaml get /orders/history/dca +--- diff --git a/api-reference/trigger/dca-order.mdx b/api-reference/trigger/dca-order.mdx new file mode 100644 index 00000000..dcfd358d --- /dev/null +++ b/api-reference/trigger/dca-order.mdx @@ -0,0 +1,5 @@ +--- +title: "Get DCA Order" +description: "Get a single DCA order by ID, including fills and events" +openapi: /openapi-spec/trigger/v2/trigger.yaml get /orders/history/dca/{id} +--- diff --git a/changelog/index.mdx b/changelog/index.mdx index 9be51792..5b00615b 100644 --- a/changelog/index.mdx +++ b/changelog/index.mdx @@ -21,6 +21,15 @@ llmsDescription: "Changelog for Jupiter developer APIs, SDKs, and docs. Tracks b +## Trigger API: Dollar-Cost Averaging (DCA) Orders + +The Trigger API now documents DCA orders. A DCA order splits one deposit into multiple swaps that run on a fixed schedule, using the same vault, authentication, and deposit flow as price orders. + +- Create with `POST /trigger/v2/orders/dca` (`time_based` or `price_conditional`) +- `orderCount` 2–100, `intervalSeconds` 60s–1 year, minimum 10 USD per round +- Cancel via the two-step withdrawal flow to refund the unfilled remainder +- [DCA guide](/trigger/dca) + ## Swap API: JupiterZ Integrator Fees Swap API `/order` supports integrator fees on JupiterZ routes. Requests using `referralAccount` and `referralFee` can receive RFQ quotes when JupiterZ is the best eligible route. diff --git a/docs.json b/docs.json index 6fb6cd9c..513d912e 100644 --- a/docs.json +++ b/docs.json @@ -331,6 +331,7 @@ "trigger/lifecycle", "trigger/authentication", "trigger/create-order", + "trigger/dca", "trigger/manage-orders", "trigger/order-history", "trigger/best-practices", @@ -359,6 +360,16 @@ "api-reference/trigger/confirm-cancel" ] }, + { + "group": "DCA", + "pages": [ + "api-reference/trigger/create-dca", + "api-reference/trigger/cancel-dca", + "api-reference/trigger/confirm-cancel-dca", + "api-reference/trigger/dca-history", + "api-reference/trigger/dca-order" + ] + }, { "group": "History", "pages": ["api-reference/trigger/order-history"] diff --git a/llms.txt b/llms.txt index b1f29fe8..6490b498 100644 --- a/llms.txt +++ b/llms.txt @@ -258,10 +258,11 @@ Vault-based limit orders with single, OCO (TP/SL), and OTOCO order types. ### Trigger -- [Trigger Order API Overview](https://developers.jup.ag/docs/trigger/index.md): Jupiter Trigger Order API V2 enables vault-based limit orders on Solana. Supports single price orders, OCO (one-cancels-other for TP/SL), and OTOCO (one-triggers-OCO) order types. Requires JWT authentication via challenge-response flow. Deposits go into vault accounts managed by Privy. +- [Trigger Order API Overview](https://developers.jup.ag/docs/trigger/index.md): Jupiter Trigger Order API V2 enables vault-based automated orders on Solana. Two order families: price orders (single limit, OCO for TP/SL, OTOCO conditional chains) and DCA (dollar-cost averaging, recurring time- or price-conditional swaps). Requires JWT authentication via challenge-response flow. Deposits go into Privy-managed vault accounts shared across all your orders. - [Lifecycle](https://developers.jup.ag/docs/trigger/lifecycle.md): Maps the full Jupiter Trigger Order V2 integration flow across endpoints: POST /trigger/v2/auth/challenge and /auth/verify to get a JWT, GET /trigger/v2/vault (or /vault/register) to resolve the vault, POST /trigger/v2/deposit/craft to build a deposit and get a requestId, sign the deposit client-side, POST /trigger/v2/orders/price to create the order (id/ocoId), GET /trigger/v2/orders/history to monitor state, PATCH /trigger/v2/orders/price/{id} to update, and POST /trigger/v2/orders/price/cancel/{id} + /confirm-cancel/{id} to cancel and withdraw. Shows the call order, the data dependencies between calls (token, vaultPubkey, requestId, depositSignedTx, id, cancelRequestId), the order state machine (open, executing, filled, expired, cancelled, pending_withdraw), how OCO and OTOCO orders branch, and how to recover a cancellation that was interrupted. Use this page to understand which endpoints to call in what order before integrating. - [Authentication](https://developers.jup.ag/docs/trigger/authentication.md): POST /trigger/v2/auth/challenge requests a challenge (message or transaction). POST /trigger/v2/auth/verify submits the signed challenge and returns a JWT token (24h TTL). Use the JWT in Authorization: Bearer header for all authenticated endpoints. - [Create Order](https://developers.jup.ag/docs/trigger/create-order.md): How to create Trigger V2 price orders using POST /trigger/v2/deposit/craft and POST /trigger/v2/orders/price. Covers the vault-based deposit flow, required craft-deposit fields including orderType: price and orderSubType (single, oco, otoco), client-side signing, create-order request bodies for single limit orders, OCO (one-cancels-other), and OTOCO (one-triggers-OCO), validation rules, and response handling. +- [Dollar-Cost Averaging (DCA)](https://developers.jup.ag/docs/trigger/dca.md): How to create Trigger V2 DCA (dollar-cost averaging) orders with POST /trigger/v2/deposit/craft (orderType: dca) and POST /trigger/v2/orders/dca. Covers the shared vault + JWT deposit flow, the time_based and price_conditional order types, all create parameters (orderCount min 2 with no fixed max, intervalSeconds 60s-1yr, minPriceUsd, maxPriceUsd, triggerMint, beginFillAt), validation (min 10 USD per round, max 10 active orders), how the keeper fills each round and delivers output to the wallet, tracking via GET /orders/history/dca and /orders/history/dca/{id}, and the two-step cancel/withdraw flow. Includes a complete runnable TypeScript example. - [Manage Orders](https://developers.jup.ag/docs/trigger/manage-orders.md): PATCH /trigger/v2/orders/price/{orderId} updates order parameters. POST /trigger/v2/orders/price/cancel/{orderId} initiates cancellation and returns a withdrawal transaction. POST /trigger/v2/orders/price/confirm-cancel/{orderId} confirms with the signed withdrawal transaction. - [Order History](https://developers.jup.ag/docs/trigger/order-history.md): GET /trigger/v2/orders/history returns paginated order history with state, events, and fill details. Filter by state (active/past), mint, and sort by updated_at/created_at/expires_at. - [Best Practices](https://developers.jup.ag/docs/trigger/best-practices.md): Best practices for Trigger Order V2 integrations: order expiry is required (TTL improves execution quality), slippage recommendations by order type, error handling patterns, reference jup.ag frontend for implementation guidance. @@ -284,6 +285,14 @@ Vault-based limit orders with single, OCO (TP/SL), and OTOCO order types. - [Cancel Order](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Cancel a pending trigger order - [Confirm Cancel](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Confirm and execute a pending order cancellation +#### DCA + +- [Create DCA Order](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Create a recurring dollar-cost averaging order +- [Cancel DCA Order](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Initiate cancellation of a DCA order and craft the withdrawal transaction +- [Confirm Cancel DCA Order](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Submit the signed withdrawal transaction to finalize a DCA cancellation +- [List DCA Orders](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): List your DCA orders with full event history +- [Get DCA Order](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Get a single DCA order by ID, including fills and events + #### History - [Order History](https://developers.jup.ag/docs/openapi-spec/trigger/v2/trigger.yaml): Get historical trigger orders for an account diff --git a/openapi-spec/trigger/v2/trigger.yaml b/openapi-spec/trigger/v2/trigger.yaml index 695e0532..7ce4b309 100644 --- a/openapi-spec/trigger/v2/trigger.yaml +++ b/openapi-spec/trigger/v2/trigger.yaml @@ -134,6 +134,117 @@ components: - userPubkey - inputMint - outputMint + DcaHistoryItem: + type: object + properties: + id: + type: string + description: DCA order UUID + requestId: + type: string + userPubkey: + type: string + vaultPubkey: + type: string + inputMint: + type: string + outputMint: + type: string + inputAmountInitial: + type: string + description: Total deposit amount in smallest units + inputAmountRemaining: + type: string + description: Amount still held in the vault + amountPerRound: + type: string + description: Amount swapped each round (the last round may include dust) + outputAmountTotal: + type: string + description: Total output received across all successful rounds + inputAmountUsed: + type: string + description: Total input consumed by successful rounds + orderType: + type: string + enum: [time_based, price_conditional] + minPriceUsd: + type: number + nullable: true + maxPriceUsd: + type: number + nullable: true + triggerMint: + type: string + nullable: true + retryWindowSeconds: + type: number + nullable: true + description: Retry window per round in seconds, clamp(intervalSeconds/2, 30, 7200) + # UNRELEASED (Earn While You Wait / LODCA-133) — uncomment when the feature launches: + # jlEnabled: + # type: boolean + # description: Whether Earn While You Wait is on for this order (idle funds earn Jupiter Lend yield between rounds). + numberOfRounds: + type: number + intervalSeconds: + type: number + beginFillAt: + type: string + description: ISO-8601 time the first round is scheduled + nextFillAt: + type: string + description: ISO-8601 time of the next scheduled round + lastFillAt: + type: string + nullable: true + roundsFilled: + type: number + fillPercent: + type: number + description: Progress from 0 to 1 + state: + type: string + enum: [depositing, deposit_failed, active, executing, withdrawing, completed, cancelled] + displayState: + type: string + enum: [pending, active, executing, pending_withdraw, completed, cancelled, failed] + createdAt: + type: string + updatedAt: + type: string + events: + type: array + items: + type: object + properties: + type: + type: string + enum: [deposit, fill, withdrawal, cancelled] + timestamp: + type: string + txSignature: + type: string + roundNumber: + type: number + inputAmount: + type: string + outputAmount: + type: string + state: + type: string + enum: [success, failed, rescheduled, pending] + attemptCount: + type: number + required: + - id + - userPubkey + - vaultPubkey + - inputMint + - outputMint + - orderType + - state + - displayState paths: /auth/challenge: @@ -371,21 +482,25 @@ paths: type: string enum: - price - description: Order type for the deposit. Use price when crafting a deposit for price trigger orders. + - dca + description: Order type for the deposit. Use price for price trigger orders, or dca for dollar-cost averaging orders. orderSubType: type: string enum: - single - oco - otoco - description: Required when orderType is price. Use single for standalone orders, oco for OCO orders, and otoco for OTOCO orders. + description: Required when orderType is price (single for standalone orders, oco for OCO orders, otoco for OTOCO orders). Must be omitted when orderType is dca. + # UNRELEASED (Earn While You Wait / LODCA-133) — uncomment when the feature launches: + # jlMint: + # type: string + # description: 'For Earn While You Wait DCA orders only. The Jupiter Lend earn token for the input stablecoin (from GET /lend/v1/earn/tokens). Crafts a deposit into Jupiter Lend; the response returns jlTokenAccount instead of inputTokenAccount.' required: - inputMint - outputMint - userAddress - amount - orderType - - orderSubType example: inputMint: "So11111111111111111111111111111111111111112" outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" @@ -425,6 +540,10 @@ paths: outputTokenAccount: type: string description: Deterministic per-order output token account. Returned only when orderSubType is otoco. + # UNRELEASED (Earn While You Wait / LODCA-133) — uncomment when the feature launches: + # jlTokenAccount: + # type: string + # description: Jupiter Lend position account. Returned instead of inputTokenAccount when the deposit is crafted with jlMint (Earn While You Wait). required: - transaction - requestId @@ -802,3 +921,403 @@ paths: description: Unauthorized '403': description: Forbidden + + /orders/dca: + post: + summary: Create DCA order + description: | + Create a dollar-cost averaging order that splits a single deposit into `orderCount` + equal rounds executed every `intervalSeconds`. Submit the signed deposit transaction + from `/deposit/craft` (crafted with `orderType: "dca"`). + + Call this path with no trailing slash. `POST /orders/dca/` returns 404. + + Rounds execute as Jupiter swaps with Ultra-managed (RTSE) slippage; there is no + slippage parameter. Each round pays the standard Jupiter swap fee (no separate DCA fee), + and the output settles directly to the taker's wallet. + security: + - ApiKeyAuth: [] + BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + depositRequestId: + type: string + description: requestId from the deposit craft response + depositSignedTx: + type: string + description: Base64-encoded signed deposit transaction + userPubkey: + type: string + description: Your wallet public key (must match the JWT) + inputMint: + type: string + description: Mint of the token to sell + outputMint: + type: string + description: Mint of the token to buy + inputAmount: + type: string + description: Total amount to DCA in smallest units. Dust from uneven division is added to the last round. + orderCount: + type: number + minimum: 2 + description: Number of rounds to split the deposit into (minimum 2). No fixed upper limit, but each round must be worth at least 10 USD, so the practical maximum is floor(total deposit USD / 10). + intervalSeconds: + type: number + minimum: 60 + maximum: 31536000 + description: Seconds between rounds (60 to 31,536,000, i.e. 1 minute to 1 year) + orderType: + type: string + enum: [time_based, price_conditional] + default: time_based + description: time_based runs every interval; price_conditional only fills while the trigger mint price is within the band. + minPriceUsd: + type: number + description: Lower USD price bound. Price-conditional only. + maxPriceUsd: + type: number + description: Upper USD price bound. Price-conditional only. + triggerMint: + type: string + description: Mint whose USD price is checked against the band. Required for price_conditional. Must be a mint with a supported USD price (in practice the volatile leg / outputMint); a stablecoin such as USDC returns "Trigger mint is not supported". Ignored for time_based. + beginFillAt: + type: string + description: ISO-8601 time to start the first round. Defaults to now, max 30 days in the future. + # UNRELEASED (Earn While You Wait / LODCA-133) — uncomment when the feature launches: + # jlEnabled: + # type: boolean + # default: false + # description: 'Earn While You Wait. When true, idle funds earn Jupiter Lend yield between rounds. Requires orderType time_based, a supported stablecoin input, and a deposit crafted with jlMint. Default false.' + # jlMint: + # type: string + # description: Jupiter Lend earn token for the input stablecoin (from GET /lend/v1/earn/tokens, the address whose assetAddress is the inputMint). Required when jlEnabled is true. + required: + - depositRequestId + - depositSignedTx + - userPubkey + - inputMint + - outputMint + - inputAmount + - orderCount + - intervalSeconds + example: + depositRequestId: "a8551844-fa46-41c7-af3a-70340c465bde" + depositSignedTx: "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAEN..." + userPubkey: "BQ72nSv9f3PRyRKCBnHLVrerrv37CYTHm5h3s9VSGQDV" + inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + outputMint: "So11111111111111111111111111111111111111112" + inputAmount: "20000000" + orderCount: 2 + intervalSeconds: 3600 + orderType: "time_based" + responses: + '200': + description: DCA order created + content: + application/json: + schema: + $ref: '#/components/schemas/OrderResponse' + example: + id: "019f0530-36a7-77da-a38f-ed9414bb996b" + txSignature: "PPbp1QbbKDPjpHM3ScKyZBnCtLKKqmWwDeVj7USdBeEoNQsA1hkN35UQfrY2cQpyAB1pfVi9FdfciYdz7qUXBQA" + '400': + description: 'Validation error, below minimum (10 USD per round), the max of 10 active DCA orders reached ("Maximum of 10 active DCA orders allowed per user"), or invalid deposit' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + '403': + description: userPubkey does not match the authenticated wallet + + /orders/dca/cancel/{id}: + post: + summary: Initiate DCA cancellation + description: | + Step 1 of the two-step cancellation. Moves the order to `withdrawing` and returns an + unsigned withdrawal transaction for the unfilled remainder. Already-filled rounds are + not reversed. + security: + - ApiKeyAuth: [] + BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: DCA order UUID + responses: + '200': + description: Withdrawal transaction for signing + content: + application/json: + schema: + type: object + properties: + id: + type: string + roundsRemaining: + type: number + description: Rounds that will not execute + refundAmount: + type: string + description: Amount to be refunded in smallest units + transaction: + type: string + description: Base64-encoded unsigned withdrawal transaction + requestId: + type: string + description: Pass to confirm-cancel as cancelRequestId + required: + - id + - roundsRemaining + - refundAmount + - transaction + - requestId + example: + id: "019f0530-36a7-77da-a38f-ed9414bb996b" + roundsRemaining: 2 + refundAmount: "20000000" + transaction: "AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBAgdyOCX48yeQcZusgzCLBJ+otTpXtR9YKzHDx3aLqYwC+0vQ4YrooVs15oSv6alpvSIyPs+Vo55usVe/6EytcDKmDUjHKsrOFPVwGfmQFwHHgWST75hdcLkGE1QZ/OgrO9V7X8BkIOtYGIrydcacmuIKcdfd6e/c7eONpxrCoSbqmM5Q+ul2G2Ootg7YYGSKJ3qW1hW2uKHAvmby4AdEXypFAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqSI5PGNkRpV42tULB0GGLK8o3q7WErcVy5goaCJGrByHBAUABQJQAgAABQAJA5DQAwAAAAAABgQDBAECCQMALTEBAAAAAAYDAwABAQk=" + requestId: "368645e8-7ac6-4c88-8b10-3a68812bdc1d" + '400': + description: Order not in active or withdrawing state, or no remaining funds + '404': + description: Order not found or not owned by the authenticated wallet + + /orders/dca/confirm-cancel/{id}: + post: + summary: Confirm DCA cancellation + description: | + Step 2 of the two-step cancellation. Submit the signed withdrawal transaction to return + the remaining funds and move the order to `cancelled`. + security: + - ApiKeyAuth: [] + BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: DCA order UUID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + signedTransaction: + type: string + description: Base64-encoded signed withdrawal transaction + cancelRequestId: + type: string + description: requestId from the cancel response + required: + - signedTransaction + - cancelRequestId + responses: + '200': + description: Cancellation confirmed + content: + application/json: + schema: + $ref: '#/components/schemas/OrderResponse' + example: + id: "019f0530-36a7-77da-a38f-ed9414bb996b" + txSignature: "53CQkssz7oGmbUxp3W5tsjw8Vhpv8FKVXjfWb3ZyPW71gsAm4qHvK2FjEDKt9RNDVGesoNiMA7moGR1rKbzhEfqb" + '400': + description: Order not in withdrawing state, transaction mismatch, or invalid signature + '401': + description: Unauthorized + '404': + description: Order not found or not owned by the authenticated wallet + + /orders/history/dca: + get: + summary: List DCA orders + description: List the authenticated wallet's DCA orders with full event history. + security: + - ApiKeyAuth: [] + BearerAuth: [] + parameters: + - name: state + in: query + schema: + type: string + enum: [active, past] + description: active = depositing/active/executing/withdrawing; past = completed/cancelled/deposit_failed + - name: mint + in: query + schema: + type: string + description: Filter by mint (matches inputMint or outputMint) + - name: limit + in: query + schema: + type: number + default: 20 + minimum: 1 + maximum: 100 + - name: offset + in: query + schema: + type: number + default: 0 + - name: sort + in: query + schema: + type: string + enum: [updated_at, created_at, next_fill_at] + default: updated_at + - name: dir + in: query + schema: + type: string + enum: [asc, desc] + default: desc + responses: + '200': + description: Paginated DCA order history + content: + application/json: + schema: + type: object + properties: + orders: + type: array + items: + $ref: '#/components/schemas/DcaHistoryItem' + pagination: + type: object + properties: + total: + type: number + limit: + type: number + offset: + type: number + required: + - orders + - pagination + example: + orders: + - id: "019f0530-36a7-77da-a38f-ed9414bb996b" + requestId: "b7d1b2a2-c8b3-4c21-a938-d1499260d218" + userPubkey: "8gs8rXavMQRqoAoMhCfo79r2wndgSn93FLSWErdHp82n" + vaultPubkey: "66xK9AE6oFpGAyn9eCWE3LtYwUZKkeUxbfjFSNmFnkBo" + inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + outputMint: "So11111111111111111111111111111111111111112" + inputAmountInitial: "20000000" + inputAmountRemaining: "20000000" + amountPerRound: "10000000" + outputAmountTotal: "0" + inputAmountUsed: "0" + orderType: "price_conditional" + minPriceUsd: 50 + maxPriceUsd: 1000 + triggerMint: "So11111111111111111111111111111111111111112" + retryWindowSeconds: 7200 + # jlEnabled: false # UNRELEASED (Earn While You Wait / LODCA-133) + numberOfRounds: 2 + intervalSeconds: 86400 + beginFillAt: "2026-06-26T20:27:46.015Z" + nextFillAt: "2026-06-26T20:27:46.015Z" + lastFillAt: null + roundsFilled: 0 + fillPercent: 0 + state: "active" + displayState: "active" + createdAt: "2026-06-26T18:27:53.665Z" + updatedAt: "2026-06-26T18:27:54.529Z" + events: + - type: "deposit" + timestamp: "2026-06-26T18:27:54.529Z" + txSignature: "PPbp1QbbKDPjpHM3ScKyZBnCtLKKqmWwDeVj7USdBeEoNQsA1hkN35UQfrY2cQpyAB1pfVi9FdfciYdz7qUXBQA" + inputAmount: "20000000" + state: "success" + pagination: + total: 1 + limit: 20 + offset: 0 + '401': + description: Unauthorized + + /orders/history/dca/{id}: + get: + summary: Get DCA order + description: | + Get a single DCA order by ID, including fill history and events. Use this DCA-specific + path; the price-order path `GET /orders/history/{id}` returns 404 for DCA orders. + security: + - ApiKeyAuth: [] + BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: DCA order UUID + responses: + '200': + description: DCA order details + content: + application/json: + schema: + $ref: '#/components/schemas/DcaHistoryItem' + example: + id: "019f053d-3f0c-70ad-bb5e-76d633291f80" + requestId: "1cc1ad82-866e-492d-a092-18859565945d" + userPubkey: "8gs8rXavMQRqoAoMhCfo79r2wndgSn93FLSWErdHp82n" + vaultPubkey: "66xK9AE6oFpGAyn9eCWE3LtYwUZKkeUxbfjFSNmFnkBo" + inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + outputMint: "So11111111111111111111111111111111111111112" + inputAmountInitial: "20000000" + inputAmountRemaining: "10000000" + amountPerRound: "10000000" + outputAmountTotal: "137589159" + inputAmountUsed: "10000000" + orderType: "time_based" + minPriceUsd: null + maxPriceUsd: null + triggerMint: null + retryWindowSeconds: 1800 + # jlEnabled: false # UNRELEASED (Earn While You Wait / LODCA-133) + numberOfRounds: 2 + intervalSeconds: 3600 + beginFillAt: "2026-06-26T18:42:07.750Z" + nextFillAt: "2026-06-26T19:44:57.469Z" + lastFillAt: "2026-06-26T18:42:11.469Z" + roundsFilled: 1 + fillPercent: 0.5 + state: "active" + displayState: "active" + createdAt: "2026-06-26T18:42:07.782Z" + updatedAt: "2026-06-26T18:42:11.469Z" + events: + - type: "fill" + timestamp: "2026-06-26T18:42:11.469Z" + txSignature: "HuNMpjFkTjVrCYMN6gTPWV9ay3GSxTxM7sZj4WNzjvzQoe4J5CR71MnTpuMKH9rtkDoDCTWu6rYRM12oR9cpbb4" + roundNumber: 1 + inputAmount: "10000000" + outputAmount: "137589159" + state: "success" + - type: "deposit" + timestamp: "2026-06-26T18:42:08.880Z" + txSignature: "32hAuVdZ57y5TUEx4vdV75zJyTqoCVqAby4eiayrmUW1TCuwoAvPRBcnhPMLN1YpfCp4Rc6HiLXKsi3NF5iWVcrn" + inputAmount: "20000000" + state: "success" + '401': + description: Unauthorized + '404': + description: Order not found or not owned by the authenticated wallet diff --git a/trigger/dca.mdx b/trigger/dca.mdx new file mode 100644 index 00000000..c101c209 --- /dev/null +++ b/trigger/dca.mdx @@ -0,0 +1,413 @@ +--- +title: "Dollar-Cost Averaging (DCA)" +description: "Split a single deposit into recurring swaps on a schedule, with optional price conditions." +llmsDescription: "How to create Trigger V2 DCA (dollar-cost averaging) orders with POST /trigger/v2/deposit/craft (orderType: dca) and POST /trigger/v2/orders/dca. Covers the shared vault + JWT deposit flow, the time_based and price_conditional order types, all create parameters (orderCount min 2 with no fixed max, intervalSeconds 60s-1yr, minPriceUsd, maxPriceUsd, triggerMint, beginFillAt), validation (min 10 USD per round, max 10 active orders), how the keeper fills each round and delivers output to the wallet, tracking via GET /orders/history/dca and /orders/history/dca/{id}, and the two-step cancel/withdraw flow. Includes a complete runnable TypeScript example." +--- + +A DCA order splits one deposit into a series of swaps that run automatically on a fixed schedule, so you buy a little at a time instead of all at once. You deposit your full budget, choose how many rounds and how often, and Jupiter's keeper executes each round and sends the output to your wallet. + +DCA is part of the Trigger API. It shares the same vault, [authentication](/trigger/authentication), and deposit endpoint as [price orders](/trigger/create-order), so if you have already integrated those, the only new endpoint is `POST /trigger/v2/orders/dca` and its cancel pair. + +## How it works + +Every wallet has one Privy-managed vault. You fund a DCA order by depositing into that vault, then the keeper draws from it each round: + +```mermaid +flowchart TD + S1["Step 1 · Deposit
You move your full budget once
(e.g. 20 USDC) into your vault"] + S2["Step 2 · Auto-buy one slice
The keeper swaps a slice
(e.g. 10 USDC into SOL) to your wallet"] + D{"Budget
remaining?"} + Done(["Done · all rounds filled"]) + Cancel(["Cancel anytime · unspent
balance returns to your wallet"]) + S1 --> S2 --> D + D -->|"yes · wait one interval, repeat"| S2 + D -->|"no"| Done + S2 -.->|"optional"| Cancel +``` + +You manage two moments: **creating** the order (deposit + schedule) and optionally **cancelling** it (withdraw the unfilled remainder). Everything in between, the per-round swaps, is handled by the keeper. + +## Order types + +Set `orderType` on create to choose how rounds execute. + +| Type | Behaviour | +| :--- | :--- | +| `time_based` (default) | Executes every round on schedule. If a round cannot execute within its retry window, it is rescheduled to the next interval. The completion time is predictable. | +| `price_conditional` | Executes a round only while the trigger mint's USD price is inside `[minPriceUsd, maxPriceUsd]`. While the price is out of band, the round waits and is rescheduled to the next interval. | + +Both types retry a stuck round within a window of `min(max(30, intervalSeconds / 2), 7200)` seconds before rescheduling it. + +## Quick start + +Creating a DCA order is four calls: authenticate, get your vault, craft and sign the deposit, then submit the order. + + + +Install the signing dependencies: + +```bash +npm install @solana/web3.js bs58 tweetnacl +``` + +You also need a [Jupiter API key](https://developers.jup.ag/portal) and a funded wallet. The example below loads the wallet from `BS58_PRIVATE_KEY` and the key from `JUPITER_API_KEY` in your `.env`. For the browser-wallet (`signMessage`) version of authentication, see [Authentication](/trigger/authentication). + + +Never commit private keys. Use environment variables for testing and a proper key-management solution in production. + + + +```typescript expandable +import { Keypair, VersionedTransaction } from "@solana/web3.js"; +import bs58 from "bs58"; +import nacl from "tweetnacl"; + +const BASE = "https://api.jup.ag/trigger/v2"; +const API_KEY = process.env.JUPITER_API_KEY!; +const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +const SOL = "So11111111111111111111111111111111111111112"; + +const wallet = Keypair.fromSecretKey(bs58.decode(process.env.BS58_PRIVATE_KEY!)); +const owner = wallet.publicKey.toBase58(); + +// 1. Authenticate: sign the challenge to get a 24h JWT (see /trigger/authentication) +async function authenticate(): Promise { + const { challenge } = await fetch(`${BASE}/auth/challenge`, { + method: "POST", + headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, + body: JSON.stringify({ walletPubkey: owner, type: "message" }), + }).then((r) => r.json()); + + const signature = nacl.sign.detached(new TextEncoder().encode(challenge), wallet.secretKey); + const { token } = await fetch(`${BASE}/auth/verify`, { + method: "POST", + headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, + body: JSON.stringify({ type: "message", walletPubkey: owner, signature: bs58.encode(signature) }), + }).then((r) => r.json()); + return token; +} + +async function createDca() { + const token = await authenticate(); + const headers = { "Content-Type": "application/json", "x-api-key": API_KEY, Authorization: `Bearer ${token}` }; + + // 2. Get your vault, registering one on first use + let vault = await fetch(`${BASE}/vault`, { headers }); + if (!vault.ok) vault = await fetch(`${BASE}/vault/register`, { headers }); + if (!vault.ok) throw new Error(`vault failed: ${vault.status}`); + + // 3. Craft the deposit (orderType: "dca", no orderSubType) + const deposit = await fetch(`${BASE}/deposit/craft`, { + method: "POST", + headers, + body: JSON.stringify({ inputMint: USDC, outputMint: SOL, userAddress: owner, amount: "20000000", orderType: "dca" }), + }).then((r) => r.json()); + if (!deposit.requestId) throw new Error(`craft failed: ${JSON.stringify(deposit)}`); + + // 4. Sign the deposit and submit the order (note: no trailing slash on /orders/dca) + const tx = VersionedTransaction.deserialize(Buffer.from(deposit.transaction, "base64")); + tx.sign([wallet]); + + const order = await fetch(`${BASE}/orders/dca`, { + method: "POST", + headers, + body: JSON.stringify({ + depositRequestId: deposit.requestId, + depositSignedTx: Buffer.from(tx.serialize()).toString("base64"), + userPubkey: owner, + inputMint: USDC, + outputMint: SOL, + inputAmount: "20000000", // 20 USDC total + orderCount: 2, // 2 rounds of 10 USDC + intervalSeconds: 3600, // one round per hour + orderType: "time_based", + }), + }).then((r) => r.json()); + if (!order.id) throw new Error(`create failed: ${JSON.stringify(order)}`); + + console.log(`DCA order ${order.id}, deposit tx https://solscan.io/tx/${order.txSignature}`); + return order.id as string; +} + +createDca().catch(console.error); +``` + +The deposit lands on-chain during the create call, so a `200` means the order is live. The response is `{ id, txSignature }`, where `txSignature` is the on-chain deposit signature: + +```json +{ + "id": "019f02b5-0400-72cc-8943-007c4310a3de", + "txSignature": "4R18eTrkDvmZFPq87pznBPpVCpk1G2FQapWVks4ELLNYxAQZdwoDaxnnAHN2ZvBFrRvbnJSAPTnhutgj8ZzLFwgV" +} +``` + + +The vault address is resolved from your JWT, so you never pass it. The deposit `requestId` is single-use and is consumed by the create call as `depositRequestId`. + + +## Request parameters + +The body of `POST /trigger/v2/orders/dca`: + +| Parameter | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `depositRequestId` | `string` | Yes | The `requestId` returned by `/deposit/craft`. | +| `depositSignedTx` | `string` | Yes | Base64-encoded signed deposit transaction. | +| `userPubkey` | `string` | Yes | Your wallet public key. Must match the JWT. | +| `inputMint` | `string` | Yes | Mint of the token to sell. Native SOL is supported (wrapped automatically). Tokens with transfer-fee or transfer-hook extensions are rejected at the deposit step unless whitelisted. | +| `outputMint` | `string` | Yes | Mint of the token to buy. | +| `inputAmount` | `string` | Yes | Total amount to DCA, in the input token's smallest unit. Any remainder from uneven division is added to the last round. | +| `orderCount` | `number` | Yes | Number of rounds. Minimum 2, no fixed maximum (the per-round \$10 minimum effectively caps it at your deposit in USD ÷ 10). | +| `intervalSeconds` | `number` | Yes | Seconds between rounds, from 60 (1 minute) to 31,536,000 (1 year). | +| `orderType` | `string` | No | `time_based` (default) or `price_conditional`. | +| `minPriceUsd` | `number` | No | Lower price bound. Price-conditional only. | +| `maxPriceUsd` | `number` | No | Upper price bound. Price-conditional only. | +| `triggerMint` | `string` | Cond. | Mint whose USD price is checked against the band. **Required** for `price_conditional`, and must be a mint with a supported price feed (the volatile leg, usually `outputMint`). Ignored for `time_based`. | +| `beginFillAt` | `string` | No | ISO-8601 time to start the first round. Defaults to now, up to 30 days out. | + +## Validation + +The API validates the request before any funds move, so a rejected order costs nothing. + +| Rule | Error on failure | +| :--- | :--- | +| Each round worth ≥ \$10 (`inputAmount ÷ orderCount`, \$0.20 tolerance) | `Each order must be at least 10 USD (current value: X USD)` | +| `orderCount` ≥ 2 | `Too small: expected number to be >=2` | +| `intervalSeconds` between 60 and 31,536,000 | `Too small/big: expected number to be >=60 / <=31536000` | +| `inputMint` ≠ `outputMint` | `Input mint and output mint cannot be the same` | +| `beginFillAt` in the future, ≤ 30 days out | `Begin fill time must be in the future` | +| At most 10 active orders per wallet | `Maximum of 10 active DCA orders allowed per user` | + +Active orders are those in `depositing`, `active`, `executing`, or `withdrawing`. Cancel orders you no longer need to free up slots. + +## Price-conditional orders + +To accumulate only within a target price range, set `orderType: "price_conditional"`, at least one of `minPriceUsd` / `maxPriceUsd`, and a `triggerMint`: + +```typescript +{ + // ...deposit and mints as above... + inputAmount: "40000000", // 40 USDC + orderCount: 4, + intervalSeconds: 86400, // daily + orderType: "price_conditional", + triggerMint: SOL, // check SOL's USD price + minPriceUsd: 120, // only buy while SOL is between + maxPriceUsd: 180, // $120 and $180 +} +``` + +`triggerMint` must be the input or output mint *and* have a supported price feed. In practice that is the volatile leg (here, SOL). A stablecoin trigger such as USDC is rejected with `400 "Trigger mint is not supported"`. Omitting the bounds or the trigger returns a `400` naming both missing fields, and a `time_based` order that sets price bounds is rejected. + +{/* UNRELEASED — Earn While You Wait (Jupiter Lend yield on idle DCA capital). + Backend merged in trigger-order PR #516 (2026-06-24) and live on the API, but the feature + is NOT publicly released: the LO/DCA Linear project LODCA-133 is still In Progress. + Do NOT uncomment until the DCA team confirms public launch. When it ships, restore this + section, the jlEnabled/jlMint create params, the jlEnabled response field, and the + matching jl* fields commented out in openapi-spec/trigger/v2/trigger.yaml. + + Additional create parameters: + - jlEnabled (boolean, default false): opt in to Earn While You Wait. time_based + + stablecoin input only; requires the JL deposit craft. + - jlMint (string): the Jupiter Lend earn token for your input stablecoin (from + GET /lend/v1/earn/tokens). Required on the deposit craft and create when jlEnabled is true. + + ## Earn While You Wait + + Set jlEnabled: true to earn yield on the budget that has not been swapped yet. While the + stablecoin sits in the vault between rounds it is supplied to Jupiter Lend (/lend) and earns + yield until each round draws from it. Requirements: + - The order must be time_based (price_conditional + jlEnabled returns 400). + - The input mint must be a supported stablecoin (else 400 "jlEnabled is not supported for this input mint"). + + Flow: + 1. Find the JL token: GET https://api.jup.ag/lend/v1/earn/tokens, match assetAddress to your + inputMint, use its address as jlMint. USDC -> 9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D (jlUSDC). + 2. Craft the deposit with jlMint (POST /deposit/craft + orderType dca). The response returns + jlTokenAccount instead of inputTokenAccount. Wrong jlMint -> 400 "jlMint does not match the JL token for this input mint". + 3. Create with jlEnabled: true (and jlMint) on POST /orders/dca. + + The order response carries jlEnabled: true. Cancelling unwinds the Lend position and returns + the remaining stablecoin (with yield) to the wallet. Verified live e2e on 2026-06-27 + (order 019f0592-966e-736b-aedd-80fa56e2e399). +*/} + +## Tracking an order + +List your DCA orders, or fetch one by ID. Both require the JWT. + +```typescript +const list = await fetch(`${BASE}/orders/history/dca?state=active&limit=20`, { headers }).then((r) => r.json()); +const detail = await fetch(`${BASE}/orders/history/dca/${orderId}`, { headers }).then((r) => r.json()); +``` + + +The per-order path includes the `dca` segment: `GET /trigger/v2/orders/history/dca/{id}`. The price-order path `GET /trigger/v2/orders/history/{id}` returns `404` for DCA orders. + + +List query parameters: `state` (`active` or `past`), `mint` (matches input or output), `limit` (1–100, default 20), `offset`, `sort` (`updated_at`, `created_at`, `next_fill_at`), and `dir` (`asc` / `desc`). + +Each order reports its schedule, progress, and event history: + +| Field | Description | +| :--- | :--- | +| `inputAmountInitial` / `inputAmountRemaining` | Total deposited, and amount still in the vault. | +| `amountPerRound` | Input swapped each round (the last round absorbs any dust). | +| `outputAmountTotal` / `inputAmountUsed` | Output received and input consumed across filled rounds. | +| `numberOfRounds` / `roundsFilled` / `fillPercent` | Total rounds, rounds completed, and progress from 0 to 1. | +| `nextFillAt` / `lastFillAt` | Next scheduled round, and the most recent fill. | +| `state` / `displayState` | Raw and human-friendly status (see below). | +| `events` | Ordered history of `deposit`, `fill`, `withdrawal`, and `cancelled` events. | + + +```json +{ + "id": "019f02b5-0400-72cc-8943-007c4310a3de", + "requestId": "2ea435dc-1068-466c-8fff-c9ad74d076e8", + "userPubkey": "8gs8rXavMQRqoAoMhCfo79r2wndgSn93FLSWErdHp82n", + "vaultPubkey": "66xK9AE6oFpGAyn9eCWE3LtYwUZKkeUxbfjFSNmFnkBo", + "inputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "outputMint": "So11111111111111111111111111111111111111112", + "inputAmountInitial": "20000000", + "inputAmountRemaining": "20000000", + "amountPerRound": "10000000", + "outputAmountTotal": "0", + "inputAmountUsed": "0", + "orderType": "time_based", + "minPriceUsd": null, + "maxPriceUsd": null, + "triggerMint": null, + "retryWindowSeconds": 30, + "numberOfRounds": 2, + "intervalSeconds": 60, + "beginFillAt": "2026-06-26T07:53:57.767Z", + "nextFillAt": "2026-06-26T07:53:57.767Z", + "lastFillAt": null, + "roundsFilled": 0, + "fillPercent": 0, + "state": "active", + "displayState": "active", + "createdAt": "2026-06-26T06:54:05.327Z", + "updatedAt": "2026-06-26T06:54:05.924Z", + "events": [ + { + "type": "deposit", + "timestamp": "2026-06-26T06:54:05.924Z", + "txSignature": "4R18eTrkDvmZFPq87pznBPpVCpk1G2FQapWVks4ELLNYxAQZdwoDaxnnAHN2ZvBFrRvbnJSAPTnhutgj8ZzLFwgV", + "inputAmount": "20000000", + "state": "success" + } + ] +} +``` +Reconcile against `state` and `events`, not `inputAmountRemaining`, which is not zeroed after a cancellation. + + +### Order states + +The response carries two status fields: `state` is the raw internal status, and `displayState` is the human-friendly version most integrations show. + +| `state` | `displayState` | Meaning | +| :--- | :--- | :--- | +| `depositing` | `pending` | Deposit in progress. | +| `active` | `active` | Scheduled and waiting for the next round. | +| `executing` | `executing` | A round is currently filling. | +| `withdrawing` | `pending_withdraw` | A cancellation is in progress. | +| `completed` | `completed` | All rounds filled. | +| `cancelled` | `cancelled` | Cancelled by you. | +| `deposit_failed` | `failed` | The deposit failed on-chain. | + +## How rounds fill + +When a round comes due, the keeper swaps `amountPerRound` and **delivers the output straight to your wallet**, and pays the swap's transaction fee itself. You never withdraw filled output manually; only the unfilled remainder is returned, and only when you cancel. A round that is due now typically fills within a few seconds. + +Each fill appends an event and advances `roundsFilled`, `outputAmountTotal`, `inputAmountUsed`, and `nextFillAt`: + +```json +{ + "type": "fill", + "timestamp": "2026-06-26T18:42:11.469Z", + "txSignature": "HuNMpjFkTjVrCYMN6gTPWV9ay3GSxTxM7sZj4WNzjvzQoe4J5CR71MnTpuMKH9rtkDoDCTWu6rYRM12oR9cpbb4", + "roundNumber": 1, + "inputAmount": "10000000", + "outputAmount": "137589159", + "state": "success" +} +``` + +A `price_conditional` round that is out of band does not fill: the order stays `active` with `roundsFilled` unchanged until the price re-enters the range, then resumes on the next interval. + + +DCA orders cannot be edited after creation; there is no update endpoint (`PATCH /trigger/v2/orders/dca/{id}` returns `404`). To change the schedule or amounts, cancel the order and create a new one. + + +## Fees and slippage + +Each round executes as a standard Jupiter swap, routed through Jupiter Ultra. Two things follow from that: + +- **Slippage is automatic and not configurable.** DCA orders have no slippage parameter. Every round uses Ultra's [Real-Time Slippage Estimator (RTSE)](/swap/advanced/slippage), which sets slippage per round from live market conditions. You cannot set or cap it. +- **The standard swap fee applies; there is no separate DCA fee.** Each round pays Jupiter's normal swap fee, already reflected in the output you receive. Trigger V2 adds no integrator fee on top. + +The keeper pays the network and priority fees for each fill, and the output settles directly to your wallet. + +## Cancelling an order + +Cancelling returns the unfilled remainder to your wallet through the same two-step withdrawal flow as price orders. Rounds that have already filled are not reversed. + +**Step 1** moves the order to `withdrawing` and returns an unsigned withdrawal transaction, along with `refundAmount` and `roundsRemaining`: + +```typescript +const cancel = await fetch(`${BASE}/orders/dca/cancel/${orderId}`, { method: "POST", headers }).then((r) => r.json()); +// { id, roundsRemaining, refundAmount, transaction, requestId } +``` + +**Step 2** signs that transaction and confirms the withdrawal: + +```typescript +const tx = VersionedTransaction.deserialize(Buffer.from(cancel.transaction, "base64")); +tx.sign([wallet]); + +const confirmed = await fetch(`${BASE}/orders/dca/confirm-cancel/${orderId}`, { + method: "POST", + headers, + body: JSON.stringify({ + signedTransaction: Buffer.from(tx.serialize()).toString("base64"), + cancelRequestId: cancel.requestId, + }), +}).then((r) => r.json()); +// { id, txSignature } +``` + +If the withdrawal does not land, call `cancel` again to re-craft it; the order stays in `withdrawing` and the request is idempotent. If `confirm-cancel` fails mid-execution, the order rolls back to `active` so you can retry. + +## Errors + +Beyond the create-time [validation](#validation) above, these are the runtime errors you are most likely to hit: + +| When | Status | Error | +| :--- | :--- | :--- | +| Missing or expired JWT | `401` | `Unauthorized` | +| `userPubkey` does not match the authenticated wallet | `403` | `Forbidden: userPubkey must match authenticated user` | +| `depositRequestId` not found or already used | `400` | `Deposit transaction not found in cache. Please craft a new deposit first.` | +| `triggerMint` is a stablecoin or has no supported price | `400` | `Trigger mint is not supported` | +| Create called with a trailing slash | `404` | `Not Found` (use `POST /orders/dca`, no trailing slash) | +| Fetching a DCA order at the price-order path | `404` | `Order not found` (use `GET /orders/history/dca/{id}`) | +| Cancelling an order that is not `active` or `withdrawing` | `400` | `Cannot cancel DCA order in '' state` | +| Confirm-cancel with a stale or wrong `cancelRequestId` | `400` | `Withdrawal transaction not found in cache. Please initiate cancel again.` | + +## Related + + + + The challenge-response JWT flow every request needs. + + + Single limit, OCO, and OTOCO orders on the same vault. + + + Full request and response schema in the API reference. + + + How Trigger orders move through their states. + + diff --git a/trigger/index.mdx b/trigger/index.mdx index b9505d84..153d6823 100644 --- a/trigger/index.mdx +++ b/trigger/index.mdx @@ -1,11 +1,11 @@ --- title: "Trigger Order API Overview" sidebarTitle: "About Trigger API" -description: "Create advanced trigger orders on Solana with vault-based execution, including limit orders, take-profit/stop-loss (OCO), and conditional chains (OTOCO)." -llmsDescription: "Jupiter Trigger Order API V2 enables vault-based limit orders on Solana. Supports single price orders, OCO (one-cancels-other for TP/SL), and OTOCO (one-triggers-OCO) order types. Requires JWT authentication via challenge-response flow. Deposits go into vault accounts managed by Privy." +description: "Create advanced trigger orders on Solana with vault-based execution: limit orders, take-profit/stop-loss (OCO), conditional chains (OTOCO), and recurring DCA." +llmsDescription: "Jupiter Trigger Order API V2 enables vault-based automated orders on Solana. Two order families: price orders (single limit, OCO for TP/SL, OTOCO conditional chains) and DCA (dollar-cost averaging, recurring time- or price-conditional swaps). Requires JWT authentication via challenge-response flow. Deposits go into Privy-managed vault accounts shared across all your orders." --- -The Jupiter Trigger Order API enables advanced order types on Solana, allowing users to set price conditions for token swaps that execute automatically when market conditions are met. +The Jupiter Trigger Order API enables automated order types on Solana. **Price orders** execute a swap when a USD price condition is met (limit orders, take-profit, stop-loss). **DCA** orders split a deposit into recurring swaps on a schedule. Both share one vault, authentication, and deposit flow. V1 required traders to think in pool ratios — calculating how many tokens per SOL a pool needed to reach before an order would trigger. V2 replaces this with USD price triggers: set a dollar price and Jupiter handles conversion, routing, and execution. Orders are stored off-chain and private by default, closing the most common MEV attack vector of visible pending orders. @@ -28,7 +28,13 @@ V1 required traders to think in pool ratios — calculating how many tokens per There are no current plans to deprecate V1. However, all development efforts, research, and maintenance are focused entirely on V2. V1 will only receive updates for critical issues. -## Order Types +## Order Families + +The API has two order families that share the same vault, authentication, and deposit flow. + +### Price Orders + +Execute a swap when the USD price of a token crosses a threshold. See [Create Order](/trigger/create-order). | Type | Description | Use case | | :--- | :--- | :--- | @@ -36,6 +42,15 @@ There are no current plans to deprecate V1. However, all development efforts, re | **OCO** | Two orders sharing one deposit: one take-profit, one stop-loss. When one fills, the other cancels automatically | Risk management with TP/SL brackets (e.g. sell SOL at \$300 TP or \$200 SL while market is \$250) | | **OTOCO** | A parent order triggers first, then activates a TP/SL pair (OCO) on the output | Conditional entry with automatic exit strategy | +### DCA (Dollar-Cost Averaging) + +Split a single deposit into multiple swaps that execute on a fixed schedule. See [DCA](/trigger/dca). + +| Type | Description | Use case | +| :--- | :--- | :--- | +| **Time-based** | Buys a fixed amount every interval until the deposit is spent | Recurring accumulation regardless of price | +| **Price-conditional** | Same schedule, but each round only fills while the price is within a `[min, max]` band | Recurring accumulation limited to a target price range | + ## How It Works ``` @@ -48,7 +63,7 @@ Each wallet gets a single vault (a Privy-managed custodial account). When you cr 1. [**Authenticate**](/trigger/authentication): Sign a challenge with your wallet to receive a JWT token 2. [**Get your vault**](/trigger/create-order#step-1-get-your-vault): Retrieve your vault, or register one on first use -3. [**Create an order**](/trigger/create-order): Deposit tokens into your vault and submit order parameters +3. **Create an order**: Deposit tokens into your vault and submit a [price order](/trigger/create-order) or a [DCA order](/trigger/dca) 4. **Monitor**: Track order state and history via the [history endpoint](/trigger/order-history) 5. [**Manage**](/trigger/manage-orders): Update order parameters, or cancel with a two-step withdrawal flow @@ -94,6 +109,10 @@ A One-Cancels-Other order creates a take-profit and stop-loss pair that share on A One-Triggers-One-Cancels-Other order has a parent trigger that executes first. Once the parent fills, it automatically activates a TP/SL pair (OCO) on the output tokens. If the parent expires or fails, the child orders are never created. + +A DCA (dollar-cost averaging) order splits one deposit into multiple swaps that run on a schedule. You set the total amount, the number of rounds (2–100), and the interval (1 minute to 1 year). Rounds can run unconditionally (`time_based`) or only within a price band (`price_conditional`). Each round must be worth at least 10 USD. See [DCA](/trigger/dca). + + Take-profit and buy-below orders use auto slippage via RTSE (Real-Time Slippage Estimator). Stop-loss and buy-above orders default to 20% (2000 bps) because execution certainty is more important when cutting losses. You can always set a custom slippage.