Submit & Track
Submit an intent
POST /v2/trading/submit-intent?blockchain=MONAD
Content-Type: application/json{
"trader": "0x…",
"action": 0,
"nonce": "1785312000123",
"deadline": 1785312300,
"feeToken": "0x…",
"antiDdosFee": "500000",
"actionData": "0x…",
"signature": "0x…"
}Returns 200 with the intent hash as a JSON string, e.g. "0xabc123…".
WARNING
nonce and antiDdosFee must be strings. nonce is a millisecond timestamp that exceeds Number.MAX_SAFE_INTEGER; sending it as a JSON number silently corrupts it and the signature check fails.
const res = await fetch(`${baseUrl}/v2/trading/submit-intent?blockchain=MONAD`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
trader,
action,
nonce: nonce.toString(),
deadline,
feeToken,
antiDdosFee: antiDdosFee.toString(),
actionData,
signature,
}),
})
if (!res.ok) throw new Error(`submit-intent ${res.status}: ${await res.text()}`)
const intentHash = (await res.text()).replace(/^"|"$/g, '')The intent hash
Computed identically by the backend and the contract:
intentHash = keccak256(abi.encode(trader, action, nonce, deadline, actionData));It excludes feeToken, antiDdosFee and signature, and is not the EIP-712 digest. Use the value returned by the response rather than deriving it yourself.
Validation
The endpoint checks the following before queueing. Any failure returns 400, except the position lookup, which returns 404.
| Check | Error text contains |
|---|---|
deadline in the future | deadline already passed |
nonce not older than 2 days | nonce too old |
nonce not more than 5 minutes ahead | nonce too far in future |
action in [0, 13] | invalid action |
trader / feeToken are valid addresses | invalid … address |
actionData / signature non-empty | must not be empty |
(action, feeToken, antiDdosFee) matches current or previous config | antiDdosFee mismatch, feeToken not supported, anti-ddos not enabled |
actionData decodes under the action's ABI | decode error |
| For actions whose oracle feeds are resolved from a stored position, the position is known | Position not found (404) |
The signature is not checked here
Signature validity and agent permissions are enforced onchain, not at submission. An intent with a bad signature or an unauthorized agent is accepted with 200 and later skipped with skipReason = INVALID. Make sure the agent is authorized and holds the right permission bit before you rely on a 200.
Position not found
Actions whose oracle feeds the relayer resolves by looking the position up require that position to be indexed — MARKET_CLOSE, BATCH_MARKET_CLOSE, PARTIAL_CLOSE, ADD_MARGIN, REMOVE_MARGIN, BATCH_CREATE_DECREASE_ORDERS. Closing right after opening can hit this. Retry with backoff, or confirm the position exists via getPositionsV4 first.
Opens are unaffected — they carry the market in actionData. Order-only actions are also exempt, including CANCEL_ALL_DECREASE_ORDERS, which takes a positionHash but needs no lookup. See Actions Reference.
This is the only submission failure that is not a 400.
Poll status
GET /v2/trading/{intent_hash}/status{
"submitted": true,
"executed": true,
"success": true,
"skipped": false,
"skipReason": null,
"txnHash": "0x…",
"reason": "0x"
}State machine:
POST accepted
→ submitted = false queued, waiting to be relayed
→ submitted = true, executed = false broadcast, waiting for the block
→ executed = true, success = true|false settled onchain
or skipped = true, skipReason = INVALID|NONCE|FEE|UNKNOWNAn intent can be skipped either because pre-flight simulation rejected it — in which case it never goes onchain and costs nothing — or because onchain execution rejected it. The response fields look the same in both cases.
Polling strategy
A two-phase poll gives the best user experience:
Phase 1 — wait for `submitted` (300ms interval, 20s timeout)
- a terminal state during this window is returned immediately
- on submitted = true, report optimistic success and let the user continue
Phase 2 — keep polling for the terminal state in the background
(300ms interval, 5 minute timeout, matching the intent deadline)
- surface an error if it ends in failureexport async function pollIntentStatus(
baseUrl: string,
intentHash: string,
{ interval = 300, timeout = 300_000 } = {},
): Promise<IntentStatus> {
const until = Date.now() + timeout
while (Date.now() < until) {
const res = await fetch(`${baseUrl}/v2/trading/${intentHash}/status`)
if (res.ok) {
const status: IntentStatus = await res.json()
if (status.executed || status.skipped) return status
}
await new Promise((r) => setTimeout(r, interval))
}
throw new Error('Timed out waiting for intent execution')
}Interpreting the result
| State | Meaning | What to do |
|---|---|---|
submitted = false | Queued | Keep waiting |
submitted = true, executed = false | Broadcast | Keep waiting |
executed = true, success = true | Done | Refresh positions and orders |
executed = true, success = false | Executed but reverted; reason holds the revert data | Decode and show; retry with a new nonce |
skipped, skipReason = INVALID | Bad signature, unauthorized agent, missing permission bit, or expired deadline | Check authorization, deadline, EIP-712 structure and primaryType |
skipped, skipReason = NONCE | Nonce not increasing, or outside the window | Re-sign with a larger nonce |
skipped, skipReason = FEE | Fee could not be collected | Check the fee token balance and Diamond allowance |
skipped, skipReason = UNKNOWN | Unclassified | Report it |
Decoding reason
reason is the raw revert data from the chain. Decode in this order:
- A
skipReasonenum value → map directly to a message. - Standard
Error(string)/Panic(uint256). - Protocol custom errors — see Error Reference.
- Fall back to a generic message.
import { decodeErrorResult } from 'viem'
const SOLIDITY_ERROR_ABI = [
{ type: 'error', name: 'Error', inputs: [{ name: 'message', type: 'string' }] },
{ type: 'error', name: 'Panic', inputs: [{ name: 'code', type: 'uint256' }] },
] as const
function decodeReason(reason: string): string {
if (!reason || reason === '0x') return ''
const skip = SKIP_REASON_MESSAGES[reason.toUpperCase()]
if (skip) return skip
try {
const decoded = decodeErrorResult({ abi: SOLIDITY_ERROR_ABI, data: reason as `0x${string}` })
if (decoded.errorName === 'Error') return String(decoded.args?.[0] ?? '')
} catch {
// fall through to protocol custom errors
}
return decodeCustomError(reason) ?? 'The transaction could not be executed.'
}Retrying
A skipped or reverted intent is not retried for you. To retry:
- Sign again with a new nonce — never resubmit the same payload.
- Refresh the fee configuration if more than ~120 seconds have passed.
- Refresh the market price and slippage bound if the action is an open.
Common integration errors
| Symptom | Likely cause |
|---|---|
Persistent INVALID with a plausible signature | EIP-712 struct expands business fields instead of using actionDataHash; wrong primaryType; verifyingContract is not the Diamond |
| Static actions work, batch actions (8/11/12) decode as garbage | actionData missing the trader prefix — see Building actionData |
antiDdosFee mismatch | Fee config cache older than ~120 seconds, or a disabled fee token |
FEE skips despite sufficient balance | Missing Diamond approval, or the trade's own spend was not added to the required amount |
Occasional NONCE under load | Two intents signed in the same millisecond |
| Signature fails after a working prototype | nonce sent as a JSON number instead of a string |
Next: Reference Client.