# End To End Example

Two complete walk-throughs — one per signing method. **Method A (ActionHash)** is used by trades, leverage changes, and most writes; **Method B (Typed Data)** is used by the four account-relationship endpoints. See [Signing Method A](broken://pages/d2130f996d95713bdd27db8c056dc20bfb86db5d) and [Signing Method B](broken://pages/1b294f6690c8101dac23707ef1ffe599bc1d8479) for the full specification of each.

|                   | Method A                                                  | Method B                                                                |
| ----------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
| Example below     | Place a limit order                                       | Approve an Agent Key                                                    |
| Business params → | Canonical JSON → `actionHash` → wrapped in `Agent` struct | Encoded **directly** as a typed struct (no `actionHash`, no action tag) |
| Signer            | Your Agent Key (or private key)                           | Your **own private key** (Agent Keys cannot manage Agent Keys)          |

Both methods share the same EIP-712 Domain, the same `nonce` / `expires_after` semantics, and the same ECDSA signature format. The only difference is how `structHash` is built.

***

## Method A — Place A Limit Order

> **Scenario:** Place a limit buy of 1 BTC at 67,500 USDT on the BTC-USDT perpetual.\
> **Signer:** your Agent Key (already approved on your main account — see [Agent Keys](broken://pages/c3ae66845b4dc85b10b48cf29fd0002d51821a96)).\
> **Action tag:** `7` (PlaceOrder — see [Action Tags](file:///1548985/reference/action-tags.md)).

{% stepper %}
{% step %}

## Step 1 — Choose Your Business Parameters

Look up `symbol_id` via `GET /v1/market/symbols` and find that BTC-USDT has ID `100001`.

```json
{
  "symbol_id": 100001,
  "is_buy": true,
  "order_type": "limit",
  "time_in_force": "gtc",
  "quantity": "1.0",
  "price": "67500.00",
  "position_side": "both",
  "margin_mode": "cross"
}
```

Optional fields (`algo_order_type`, `flags`, `tpsl`, …) are omitted — they will not appear in the canonical JSON.
{% endstep %}

{% step %}

## Step 2 — Compute `actionHash`

PlaceOrder tag = **7**. Serialize the business parameters as canonical JSON, prepend the tag byte, take `keccak256`.

```python
import json
from web3 import Web3

action = {
    "is_buy": True,
    "margin_mode": "cross",
    "order_type": "limit",
    "position_side": "both",
    "price": "67500.00",
    "quantity": "1.0",
    "symbol_id": 100001,
    "time_in_force": "gtc",
}

# Canonical JSON: Alphabetical Keys, Compact
canonical = json.dumps(action, sort_keys=True, separators=(",", ":")).encode()
# Tag Byte + Canonical JSON Bytes
payload = bytes([7]) + canonical
action_hash = Web3.keccak(payload)
```

{% endstep %}

{% step %}

## Step 3 — Wrap In EIP-712 `Agent` And Sign

```python
from eth_account import Account
from eth_account.messages import encode_typed_data

domain = {"name": "UniX", "version": "1", "chainId": 1}
types = {"Agent": [
    {"name": "sender", "type": "address"},
    {"name": "actionHash", "type": "bytes32"},
    {"name": "nonce", "type": "uint64"},
    {"name": "expiresAfter", "type": "uint64"},
]}

message = {
    "sender": "0xYOUR_ADDRESS",
    "actionHash": action_hash,
    "nonce": 1719500000000,
    "expiresAfter": 1719500600000,
}

signable = encode_typed_data(domain, types, "Agent", message)
signed = Account.sign_message(signable, private_key="0xYOUR_PRIVATE_KEY")
```

{% endstep %}

{% step %}

## Step 4 — Submit The HTTP Request

```python
import requests

resp = requests.post("http://10.34.8.77:8481/v1/trade/orders", json={
    **action,
    "address": "0xYOUR_ADDRESS",
    "nonce": 1719500000000,
    "expires_after": 1719500600000,
    "signature": {"r": hex(signed.r), "s": hex(signed.s), "v": signed.v},
})
print(resp.json())
```

> **Required**: `address` must equal the recovered signer from `signature`. The node verifies both — if they differ, the request is rejected with error `10001` (Signature verification failed).
> {% endstep %}

{% step %}

## Step 5 — Response

```json
{
  "code": "0",
  "msg": "",
  "data": {
    "tx_hash": "0x9e581b06f35cb6c44f4b8e3f7e2d1c0b9a8f7e6d5c4b3a2918273645f6e5d4c3",
    "order_id": "144115188075855872",
    "client_order_id": null
  },
  "trace_code": ""
}
```

> `tx_hash` is **exactly** the `signing_hash` you computed at the end of Step 3 — you knew the transaction's hash before submitting it.
> {% endstep %}
> {% endstepper %}

### What The Node Does

1. Extract `address`, `nonce`, `expires_after`, `signature`, and the business parameters from the body.
2. Recompute the canonical JSON, prepend tag `7`, hash → `actionHash`.
3. Build `Agent{sender = address, actionHash, nonce, expiresAfter}`, compute `signing_hash`.
4. `ecrecover(signing_hash, signature)` → `recovered_address`. **Reject** unless it equals `address`.
5. Check `nonce` is in the per-signer window and unused.
6. Resolve permissions: `signer` operating on `target_address` (or itself).
7. Execute, include in next block, return.

The node's signing-hash construction is **identical** to yours. Any divergence means a rejected transaction with `code = "10001"`.

***

## Method B — Approve An Agent Key

> **Scenario:** Authorize a new Agent Key on your main account so a trading bot can sign orders without your private key.\
> **Signer:** your **own private key** — `approve-agent` (like all account-relationship operations) cannot be signed by an Agent Key.\
> **No action tag, no `actionHash`:** the business fields are encoded directly into the `ApproveAgent` typed struct, so the wallet prompt shows them in clear text.

{% stepper %}
{% step %}

## Step 1 — Choose Your Business Parameters

```json
{
  "agent_address": "0xAGENT_ADDRESS",
  "authorized_address": "0xYOUR_ADDRESS",
  "valid_days": 30,
  "label": "mm-bot-prod"
}
```

`authorized_address` set to your own main address grants the Agent authority over your main account plus all of its sub-accounts. `label` must be unique within the authorized account — reusing an existing label replaces the prior Agent.
{% endstep %}

{% step %}

## Step 2 — Build The EIP-712 Typed Struct And Sign

There is **no** canonical JSON and **no** action tag. Declare the `ApproveAgent` type, fill in the fields, and sign — the field order must match the type declaration exactly.

```python
from eth_account import Account
from eth_account.messages import encode_typed_data

domain = {"name": "UniX", "version": "1", "chainId": 1}
types = {"ApproveAgent": [
    {"name": "sender", "type": "address"},
    {"name": "agentAddress", "type": "address"},
    {"name": "authorizedAddress", "type": "address"},
    {"name": "validDays", "type": "uint32"},
    {"name": "label", "type": "string"},
    {"name": "nonce", "type": "uint64"},
    {"name": "expiresAfter", "type": "uint64"},
]}

message = {
    "sender": "0xYOUR_ADDRESS",
    "agentAddress": "0xAGENT_ADDRESS",
    "authorizedAddress": "0xYOUR_ADDRESS",
    "validDays": 30,
    "label": "mm-bot-prod",
    "nonce": 1719600000000,
    "expiresAfter": 1719600600000,
}

signable = encode_typed_data(domain, types, "ApproveAgent", message)
signed = Account.sign_message(signable, private_key="0xYOUR_PRIVATE_KEY")
```

{% endstep %}

{% step %}

## Step 3 — Submit The HTTP Request

```python
import requests

resp = requests.post("http://10.34.8.77:8481/v1/account/approve-agent", json={
    "agent_address": "0xAGENT_ADDRESS",
    "authorized_address": "0xYOUR_ADDRESS",
    "valid_days": 30,
    "label": "mm-bot-prod",
    "address": "0xYOUR_ADDRESS",
    "nonce": 1719600000000,
    "expires_after": 1719600600000,
    "signature": {"r": hex(signed.r), "s": hex(signed.s), "v": signed.v},
})
print(resp.json())
```

> The `address` field carries the same value as `sender` in the typed struct and must equal the recovered signer. A mismatch is rejected with `10001`.
> {% endstep %}

{% step %}

## Step 4 — Response

```json
{
  "code": "0",
  "msg": "",
  "data": {
    "tx_hash": "0x4f3c2b1a09f8e7d6c5b4a3928170655443322110ffeeddccbbaa99887766554",
    "agent_address": "0xAGENT_ADDRESS",
    "label": "mm-bot-prod",
    "authorized_address": "0xYOUR_ADDRESS",
    "valid_days": 30,
    "expires_at": 1722192000000,
    "replaced_agent_address": null
  },
  "trace_code": ""
}
```

> `replaced_agent_address` is non-null only when the `label` already existed — the old Agent is revoked and its address returned here. `expires_at = approval_block_time + valid_days × 86400000`.
> {% endstep %}
> {% endstepper %}

### What The Node Does

1. Extract `address`, `nonce`, `expires_after`, `signature`, and the business fields from the body.
2. Rebuild the `ApproveAgent` typed struct (**no** canonical JSON, **no** action tag) and compute `signing_hash`.
3. `ecrecover(signing_hash, signature)` → `recovered_address`. **Reject** unless it equals `address`.
4. Confirm the signer is operating on its own account — an Agent Key signing this is rejected (`10003`).
5. Check `nonce` is in the per-signer window and unused.
6. Apply business rules: `label` unique within the authorized account (reuse revokes the old Agent), at most 4 Agents per authorized account (`10007`), `agent_address` not already approved (`10009`), and `agent_address` must not already have a TradeVM account (`10011`).
7. Record the authorization on-chain, include in next block, return `tx_hash` + `expires_at`.

As with Method A, `tx_hash` equals the `signing_hash` you computed in Step 2.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.lynxtrade.world/authentication/end-to-end-example.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
