# 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"`
