POST Position State
/position-state
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)
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):
{
"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
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.
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
id
Notifications.id
Primary key (UUID) -> in some solver implementations this is also the quote ID.
create_time
Notifications.create_time
Epoch seconds UTC
modify_time
Notifications.modify_time
Epoch seconds UTC
quote_id
Notifications.quote_id
quote_id
.
temp_quote_id
Notifications.temp_quote_id
Used for instant trades or null
.
counterparty_address
Notifications.counterparty_address
partyA address
filled_amount_open
Notifications.filled_amount_open
Recorded open fill amount
avg_price_open
Notifications.avg_price_open
Recorded open fill price
filled_amount_close
Notifications.filled_amount_close
Recorded close fill amount
avg_price_close
Notifications.avg_price_close
Recorded close fill price
last_seen_action
Notifications.last_seen_action
e.g., SendQuote
, FillLimitOrderOpen
action_status
Notifications.action_status
success
, seen
, etc.
failure_type
Notifications.failure_type
If action failed
error_code
Notifications.error_code
Numeric code
order_type
Notifications.order_type
Limit = 0, Market = 1
state_type
Notifications.state_type
"alert"
or "report"
Implementation Details (Annotated)
This section explains how each piece fits together.
Router Layer (routers.py
)
routers.py
)@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
)
controllers.py
)# 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
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):
{
"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
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):
{
"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:
RFQ Observed
On-Chain Event Poller listens for
SendQuote
events.Creates a “alert” notification (
state_type = "alert"
) withaction_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.
Hedger Confirmation & Opening of the position
The hedger’s risk engine executes the quote on-chain.
Updates the
Notifications
table with actualfilled_amount_open
andavg_price_open
values.Emits a “report” notification (
state_type = "report"
) withaction_status = "success"
and real fill data.
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"
Last updated