# POST Position State

### /position-state

```json
position-state/{start}/{size}
```

**Example Query:**

```
https://base-hedger82.rasa.capital/position-state/{start}/{size}
```

### Overview

This endpoint gives insights into historical lifecycle updates for positions based on a quote ID or counterparty address. Results are sourced from the `Notifications`, `Positions`, and `Symbol` tables, then mapped into the response schema.

***

### Endpoint Specification

**URL**: `https://base-hedger82.rasa.capital/position-state/{start}/{size}`\
**Method**: `POST`

#### Headers

* `Content-Type: application/json`
* Optional: `App-Name:`(client identifier)

**cURL Example (Limit Open — Reserved but not yet Opened)**

```bash
curl -X POST "https://base-hedger82.rasa.capital/position-state/0/10" \
     -H "Content-Type: application/json" \
     -H "App-Name: Cloverfield" \
     -d '{"quote_id": "131388"}'
```

**Response** (HTTP 200):

```json
{
    "count": 1,
    "position_state": [
        {
            "id": "7d0c10a4-1d7e-44b9-8712-77c863bfab55",
            "create_time": 1745970777,
            "modify_time": 1745970777,
            "quote_id": 131388,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "SendQuote",
            "action_status": "seen",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        }
    ]
}
```

#### Path Parameters

| Name  | Type    | Description                           |
| ----- | ------- | ------------------------------------- |
| start | integer | 0-based pagination offset             |
| size  | integer | Number of records to return (max 100) |

#### Body Parameters (JSON)

Provide **at least `quote_id` or `address`** of the following to filter results; other fields are optional:

| Field             | Type      | Description                                                                             |
| ----------------- | --------- | --------------------------------------------------------------------------------------- |
| `quote_id`        | string    | Exact quote ID (string). One of `quote_id` or `address` is required.                    |
| `address`         | string    | Counterparty address (Ethereum hex string). One of `quote_id` or `address` is required. |
| `symbols`         | string\[] | Optional filter by trading symbols (e.g., `["ETHUSD"]`).                                |
| `states`          | string\[] | Optional filter by position state enums (e.g., `["alert"]`).                            |
| `create_time_gte` | integer   | Optional: include records with `Notifications.create_time >=` epoch seconds UTC.        |
| `modify_time_gte` | integer   | Optional: include records with `Notifications.modify_time >=` epoch seconds UTC.        |

***

### Request & Response Schemas

**Define Schemas**

Create `PositionsStateRequestSchema` and `PositionStateResponseSchema` + `PositionsStateOutputSchema` in the schema file.

```python
class PositionStateResponseSchema(BaseModel):
    id: UUID
    create_time: int
    modify_time: int
    quote_id: int
    temp_quote_id: Optional[int] = None
    counterparty_address: str
    filled_amount_open: Decimal = 0
    filled_amount_close: Decimal = 0
    avg_price_open: Decimal = 0
    avg_price_close: Decimal = 0
    last_seen_action: str
    action_status: str
    failure_type: Optional[str] = None
    error_code: Optional[int] = None
    order_type: int
    state_type: PositionStateType

class PositionsStateOutputSchema(BaseModel):
    count: int
    position_state: List[PositionStateResponseSchema]
```

***

### Data Sources & Field Mapping

<table><thead><tr><th>Response Field</th><th width="357.800048828125">Source Table.Column(s)</th><th>Notes</th></tr></thead><tbody><tr><td><code>id</code></td><td><code>Notifications.id</code></td><td>Primary key (UUID) -> in some solver implementations this is also the quote ID.</td></tr><tr><td><code>create_time</code></td><td><code>Notifications.create_time</code></td><td>Epoch seconds UTC</td></tr><tr><td><code>modify_time</code></td><td><code>Notifications.modify_time</code></td><td>Epoch seconds UTC</td></tr><tr><td><code>quote_id</code></td><td><code>Notifications.quote_id</code></td><td><code>quote_id</code>.</td></tr><tr><td><code>temp_quote_id</code></td><td><code>Notifications.temp_quote_id</code></td><td>Used for instant trades or <code>null</code>.</td></tr><tr><td><code>counterparty_address</code></td><td><code>Notifications.counterparty_address</code></td><td>partyA address</td></tr><tr><td><code>filled_amount_open</code></td><td><code>Notifications.filled_amount_open</code></td><td>Recorded open fill amount</td></tr><tr><td><code>avg_price_open</code></td><td><code>Notifications.avg_price_open</code></td><td>Recorded open fill price</td></tr><tr><td><code>filled_amount_close</code></td><td><code>Notifications.filled_amount_close</code></td><td>Recorded close fill amount</td></tr><tr><td><code>avg_price_close</code></td><td><code>Notifications.avg_price_close</code></td><td>Recorded close fill price</td></tr><tr><td><code>last_seen_action</code></td><td><code>Notifications.last_seen_action</code></td><td>e.g., <code>SendQuote</code>, <code>FillLimitOrderOpen</code></td></tr><tr><td><code>action_status</code></td><td><code>Notifications.action_status</code></td><td><code>success</code>, <code>seen</code>, etc.</td></tr><tr><td><code>failure_type</code></td><td><code>Notifications.failure_type</code></td><td>If action failed</td></tr><tr><td><code>error_code</code></td><td><code>Notifications.error_code</code></td><td>Numeric code</td></tr><tr><td><code>order_type</code></td><td><code>Notifications.order_type</code></td><td>Limit = 0, Market = 1</td></tr><tr><td><code>state_type</code></td><td><code>Notifications.state_type</code></td><td><code>"alert"</code> or <code>"report"</code></td></tr></tbody></table>

***

### Implementation Details (Annotated)

This section explains how each piece fits together.

#### Router Layer (`routers.py`)

```python
@common_router.post(
    '/position-state/{start}/{size}',
    responses={status.HTTP_200_OK: {"model": PositionsStateOutputSchema}},
    response_model=PositionsStateOutputSchema
)
@with_context_db_session
async def search_position_state(
    request: PositionsStateRequestSchema,
    start: NonNegativeInt = 0,
    size: PositiveInt = 100
):
    # Map incoming HTTP payload to internal search DTO
    new_request = NotificationsRequestSchema()
    new_request.counterparty_address = request.address
    new_request.quote_id = request.quote_id
    new_request.symbols = request.symbols
    new_request.states = request.states
    new_request.timestamp_gte = request.modify_time_gte

    # Call service layer to fetch and map results
    count, data = search_notification_by_counterparty(
        new_request, start, size, map_to_positions_state=True
    )

    # Return raw dict form; FastAPI serializes to schema
    return dict(count=count, position_state=data)
```

### Controller Layer (`controllers.py`)

```python
# Located in controllers.py
# Handles fetching and mapping notifications to position state

def search_notification_by_counterparty(
    request: NotificationsRequestSchema,
    start: int,
    size: int,
    map_to_positions_state: bool = False
):
    # a. Sanitize page size
    size = min(size, 100)

    # b. Handle relative timestamps (e.g., -3600 for last hour)
    if request.timestamp_gte and request.timestamp_gte < 0:
        request.timestamp_gte = get_now_epoch() + request.timestamp_gte

    # c. Build base SQLAlchemy query with necessary joins
    stmt = (
        Notifications.select()
        .outerjoin(
            Positions,
            or_(
                Positions.quote_id == Notifications.quote_id,
                Positions.temp_quote_id == Notifications.temp_quote_id
            )
        )
        .outerjoin(
            Symbol, Positions.symbol_id == Symbol.symbol_id
        )
        .where(Notifications.should_send == true())
    )

    # d. Apply filters based on request fields
    if request.quote_id:
        if is_quote_id_temp(request.quote_id):
            stmt = stmt.where(Notifications.temp_quote_id == request.quote_id)
        else:
            stmt = stmt.where(Notifications.quote_id == request.quote_id)
    if request.counterparty_address:
        stmt = stmt.where(
            Notifications.counterparty_address == request.counterparty_address
        )
    if request.states:
        stmt = stmt.where(Positions.state.in_(request.states))
    if request.symbols:
        stmt = stmt.where(Symbol.title.in_(request.symbols))
    if request.timestamp_gte:
        stmt = stmt.where(Notifications.create_time >= request.timestamp_gte)

    # e. Count and paginate
    count = session.count_star(stmt)
    records = (
        session.scalars_all(
            stmt.order_by(desc(Notifications.create_time))
                .offset(start)
                .limit(size)
        )
    )

    # f. Map to DTOs
    result = []
    for row in records:
        data = row.to_dict()
        if map_to_positions_state:
            result.append(map_notification_to_positions_state(data))
        else:
            result.append(prepare_notification_to_send(data))
    return count, result
```

***

### Example (Full Open Position -> Close Position Flow)

**cURL Query**

```bash
curl -X POST "https://base-hedger82.rasa.capital/position-state/0/10" \
     -H "Content-Type: application/json" \
     -H "App-Name: Cloverfield" \
     -d '{"quote_id": "131391"}'
```

**fResponse** (HTTP 200):

```json
{
    "count": 6,
    "position_state": [
        {
            "id": "000be762-2592-43b0-937f-32a75fa4587a", //last notification
            "create_time": 1745976098,
            "modify_time": 1745976098,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "FillLimitOrderClose",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        },
        {
            "id": "4763fe3d-e3ed-40f9-9821-2b501342579b",
            "create_time": 1745976092,
            "modify_time": 1745976092,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "6.70000000",
            "avg_price_open": "0",
            "avg_price_close": "2.2345",
            "last_seen_action": "RequestToClosePosition",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "report"
        },
        {
            "id": "c99b51b8-a18e-4271-af05-3ed37f3805b9",
            "create_time": 1745976091,
            "modify_time": 1745976091,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "RequestToClosePosition",
            "action_status": "seen",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        },
        {
            "id": "8ff29bbd-6ca6-42cd-973f-20b6f88bfdea",
            "create_time": 1745976010,
            "modify_time": 1745976010,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "FillLimitOrderOpen",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        },
        {
            "id": "98620b7c-c752-42ed-8611-34bde40a4b28",
            "create_time": 1745976004,
            "modify_time": 1745976004,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "6.70000000",
            "filled_amount_close": "0",
            "avg_price_open": "2.2367",
            "avg_price_close": "0",
            "last_seen_action": "SendQuote",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "report"
        },
        {
            "id": "2f383602-054e-46a2-96e6-b39a7c483c49", //first notification
            "create_time": 1745975999,
            "modify_time": 1745975999,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "SendQuote",
            "action_status": "seen",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        }
    ]
}
```

#### By Address

```bash
curl -X POST "https://base-hedger82.rasa.capital/position-state/0/50" \
     -H "Content-Type: application/json" \
     -H "App-Name: Cloverfield" \
     -d '{
       "address": "0x20F764F49bf8A2c653942dA29FeD1D7A7BAefD20"
     }'

```

**Response** (200 OK):

```json
{
  "count": 383,
  "position_state": [ /* items */ ]
}
```

***

### Full Lifecycle Flow

Below is the end-to-end sequence of events and how they map to the records you see in the `/position-state` response:

1. **RFQ Observed**
   * **On-Chain Event Poller** listens for `SendQuote` events.
   * Creates a **“alert”** notification (`state_type = "alert"`) with `action_status = "seen"` and zeros in all fill fields.
   * At this point the hedger can lock the quote. The quote is seen but not yet filled.
2. **Hedger Confirmation & Opening of the position**
   * The hedger’s risk engine **executes** the quote on-chain.
   * Updates the `Notifications`table with actual `filled_amount_open` and `avg_price_open` values.
   * Emits a **“report”** notification (`state_type = "report"`) with `action_status = "success"` and real fill data.
3. **On-Chain Fill Event**
   * Almost immediately thereafter, the contract itself emits an `OpenPosition`event when the transaction finally lands on-chain.
   * Poller picks it up and creates another **“alert”** (`state_type = "alert"`) with zeros in fill fields until the DB update completes.
   * The same poller picks up that on-chain event and writes a fresh Notification with
     * `last_seen_action = "FillLimitOrderOpen"`
     * `action_status = "success"`


---

# 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.symm.io/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/post-position-state.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.
