# Submit & Track

## Submit an intent

```http
POST /v2/trading/submit-intent?blockchain=MONAD
Content-Type: application/json
```

```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.
:::

```ts
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:

```solidity
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`) |

::: info 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`.
:::

::: warning 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`](/onchain/reading-data#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](/gasless/actions#actions-that-require-an-indexed-position).

This is the only submission failure that is not a `400`.
:::

## Poll status

```http
GET /v2/trading/{intent_hash}/status
```

```json
{
  "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|UNKNOWN
```

An 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 failure
```

```ts
export 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:

1. A `skipReason` enum value → map directly to a message.
2. Standard `Error(string)` / `Panic(uint256)`.
3. Protocol custom errors — see [Error Reference](/reference/errors#contract-errors).
4. Fall back to a generic message.

```ts
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:

1. Sign again with a **new nonce** — never resubmit the same payload.
2. Refresh the [fee configuration](/gasless/fees) if more than ~120 seconds have passed.
3. 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](/gasless/signing#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](/gasless/reference-client).
