> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gains.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Non-custodial by construction: agents delegated on-chain, one EIP-712 request signature, scoped permissions, read-only keys.

## The model

| Actor                                        | Holds                                                                 | Does                                                                                                                                                                                                                             |
| -------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Trader** (master wallet)                   | Collateral, positions, the key that can move funds                    | Authorises and revokes agents. Never signs a trade.                                                                                                                                                                              |
| **Agent**                                    | A key you generate, held by the integrator, with a little ETH for gas | Signs every private API request and broadcasts the transactions the API prepares, as the trader's on-chain delegate. Cannot withdraw anything: the delegation only covers trading actions, and PnL is always paid to the trader. |
| **Relayer** (Gains, Hyperliquid facade only) | A gas wallet, address in `GET /v1/{chain}`                            | Delegate of the traders who come through the Hyperliquid facade, whose SDKs can only sign typed data. Native agents never use it: the native API sponsors no gas.                                                                |

The delegation is the existing gTrade one-click-trading primitive, `setTradingDelegate(agent)`: the Diamond only lets the trader's delegate call `delegatedTradingAction`, so the trader names the agent as delegate, once per chain. A trader who signs with its own wallet (the agent address is the trader address) needs no delegation at all. The delegation is all-or-nothing on-chain; the API adds an off-chain **scope** per agent, checked when a transaction is prepared. It is a guardrail, not a barrier: an agent that builds its own calldata is bound only by the chain.

## Authorising an agent

`POST /v1/{chain}/agents` with a body signed by the master wallet (EIP-712 `AgentAuthorization`, see the SDK's `agentAuthorizationTypedData`). The response contains the agent record and `delegationTx`, the `setTradingDelegate(agent)` transaction the master wallet must send to the Diamond (null when the agent is the trader itself).

```json theme={null}
{
  "agent": { "agent": "0xAgent", "trader": "0xTrader", "name": "quickstart-bot", "mode": "self", "status": "pending_delegation", "scope": { "..." : "..." } },
  "delegationTx": { "to": "0xDiamond", "data": "0x...", "chainId": 421614 }
}
```

`GET /v1/{chain}/agents/{address}` reports the status:

| Status               | Meaning                                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending_delegation` | The trader's on-chain delegate is not this agent (never set, or changed since). Requests answer `403 AGENT_NOT_DELEGATED` with the transaction to send. |
| `active`             | The trader's on-chain delegate is this agent, or the agent is the trader, and the scope is current.                                                     |
| `expired`            | `scope.expiresAt` is in the past.                                                                                                                       |
| `revoked`            | `DELETE /v1/{chain}/agents/{address}` was called.                                                                                                       |

### Scope

| Field              | Effect                                                                            |
| ------------------ | --------------------------------------------------------------------------------- |
| `markets`          | `"all"` or a list of symbols the agent may trade.                                 |
| `maxLeverage`      | Every open, increase and leverage change is checked against it.                   |
| `dailySpendCapUsd` | Rolling UTC-day sum of opened notional.                                           |
| `expiresAt`        | Epoch milliseconds; `null` for no expiry.                                         |
| `permissions`      | `trade` or `read`. A `read` agent can sign private reads and WebSocket auth only. |

Scope violations answer `403` with `SCOPE_MARKET_NOT_ALLOWED`, `SCOPE_LEVERAGE_EXCEEDED`, `SCOPE_SPEND_CAP_EXCEEDED` or `SCOPE_READ_ONLY`, before any transaction is sent.

### Revoking

`DELETE /v1/{chain}/agents/{address}`, signed by the master wallet (EIP-712 `AgentRevocation`), disables the agent in the API immediately and returns the `removeTradingDelegate` transaction. Send it to close the on-chain delegation too.

## Signing a request

Every private request carries four headers:

| Header              | Value                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `X-Gains-Agent`     | The agent address.                                                                                               |
| `X-Gains-Timestamp` | Unix milliseconds. Accepted within 30 seconds of server time.                                                    |
| `X-Gains-Nonce`     | Any integer never used before by this agent inside the replay window. The SDK uses `timestamp × 1000 + counter`. |
| `X-Gains-Signature` | The EIP-712 signature below.                                                                                     |

The signed struct binds the request, so a captured signature cannot be replayed on another path, body, chain or deployment:

```json theme={null}
{
  "domain": {
    "name": "Gains Trading API",
    "version": "1",
    "chainId": 42161,
    "verifyingContract": "0xFF162c694eAA571f685030649814282eA457f169"
  },
  "primaryType": "createOrder",
  "types": {
    "createOrder": [
      { "name": "method", "type": "string" },
      { "name": "path", "type": "string" },
      { "name": "bodyHash", "type": "bytes32" },
      { "name": "timestamp", "type": "uint256" },
      { "name": "nonce", "type": "uint256" }
    ]
  },
  "message": {
    "method": "POST",
    "path": "/v1/arbitrum/orders",
    "bodyHash": "0x...",
    "timestamp": 1700000000000,
    "nonce": 1700000000000001
  }
}
```

* `verifyingContract` is the Gains Diamond of the chain. `GET /v1/{chain}` publishes it together with the chain id, the field list and the empty-body hash, so nothing needs to be hard-coded; the SDK ships the public values as `CHAINS` and reads them from that endpoint whenever it is pointed at another `baseUrl` (a staging or preview deployment signs for its own Diamond).
* `primaryType` is the operation id from the OpenAPI document (`createOrder`, `cancelOrder`, `listPositions`, ...). The fields are the same for every operation; a wallet prompt shows the operation name.
* `path` is the path **with the query string**, exactly as sent, starting with `/v1`.
* `bodyHash` is `keccak256` of the exact bytes of the request body. For a request without a body (`GET`) it is `keccak256("")` = `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. Send the body bytes you hashed; the server hashes what it receives and never re-serialises it.

The WebSocket handshake signs the same struct with `primaryType: "wsAuth"`, `method: "WS"`, `path: "/v1/{chain}/ws"` and the empty-body hash.

Signature failures answer `401` with `INVALID_SIGNATURE`, `TIMESTAMP_OUT_OF_WINDOW` or `NONCE_REUSED`.

## Read-only API keys

For dashboards and monitoring you can read private data without signing. `POST /v1/{chain}/api-keys`, signed once by the master wallet (EIP-712 `ApiKeyIssue`), returns a key that is shown once and stored hashed. Send it as `X-Gains-Api-Key` on `GET /v1/{chain}/account/...` and `GET /v1/{chain}/orders/{id}`, or as `{"op":"auth","apiKey":"..."}` on the WebSocket. An API key can never place an order.

## What the API does not do

* Hold keys: the agent key lives with you, the master key never leaves the trader.
* Move funds: `delegatedTradingAction` cannot transfer collateral to the delegate; withdrawn PnL goes to the trader. The agent holds ETH for gas and nothing else.
* Hide the delegation: an agent is usable only after the trader's own `setTradingDelegate(agent)` transaction. This is the one on-chain setup step of the integration and it is intentional. `removeTradingDelegate` from the trader cuts the agent, whatever the API says.
