Portfolio
Portfolio balances, positions, and fee components for the authenticated user. Cash and token amounts held against open or settling orders appear as frozen (locked) balances. See also Locked balances.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/portfolio/ | Portfolio summary |
GET | /api/portfolio/component | Portfolio component |
GET | /api/portfolio/position/:tokenName/pnl-card | Position PnL share card (JPEG) |
Portfolio summary
GET /api/portfolio/
Returns portfolio page data for the authenticated user. The payload wraps a component object (same shape as the component endpoint below).
Request
import axios from 'axios';
const response = await axios.get('https://api.loafmarkets.com/api/portfolio/', {
headers: { Authorization: `Bearer ${process.env.LOAF_API_KEY}` },
});
console.log(response.data);Response
Type: PortfolioPageResponse
interface PortfolioPageResponse {
component: PortfolioComponentResponse;
}See Portfolio component for the full component shape.
Errors
| Status | Code | Description |
|---|---|---|
401 | unauthorized | Missing or invalid authentication. |
500 | internal_error | Unexpected server error. |
Requires API key authentication.
Get your starting point here, then switch to the live feed. Call this endpoint once to load current balances and positions, then subscribe to WebSocket portfolio:{userId} for updates — the live feed only sends changes, not the full picture.
Portfolio component
GET /api/portfolio/component
Returns balances, positions, fees, and recent activity lists used by the portfolio UI.
Request
import axios from 'axios';
const response = await axios.get('https://api.loafmarkets.com/api/portfolio/component', {
headers: { Authorization: `Bearer ${process.env.LOAF_API_KEY}` },
});
console.log(response.data);Response
Type: PortfolioComponentResponse
interface PortfolioComponentResponse {
cash: number; // available USDC (not frozen)
frozen: number; // locked USDC (open orders / settling)
portfolioValue: number;
portfolioPnl: number;
portfolioPnlPercent: number;
lifetimeVolume: number;
positions: PortfolioComponentPosition[];
applicableFees: ApplicableFees;
offeringOrders: OfferingOrder[];
openOrders: OrderHistoryItem[];
tradeHistory: TradeHistoryItem[];
orderHistory: OrderHistoryItem[];
transfers: TransferHistoryItem[];
}
interface PortfolioComponentPosition {
propertyId: number;
tokenName: string;
quantity: number; // tradeable (available) size
totalQuantity: number; // includes reserved/frozen tokens
totalTokens: number;
averageEntryPrice: number;
marketPrice: number;
percentChange: number;
percentOfPortfolio: number;
propertyPnl: number;
propertyPnlPercent: number;
isIpoAllocation: boolean;
imageUrl: string;
}
interface ApplicableFees {
takerFeeBps: number;
makerFeeBps: number;
}Balances: cash is spendable USDC. frozen is reserved against open or in-flight orders and cannot be used until the reservation clears (fill, cancel, or settlement complete). For positions, quantity is what you can trade; totalQuantity includes locked size.
Errors
| Status | Code | Description |
|---|---|---|
401 | unauthorized | Missing or invalid authentication. |
500 | internal_error | Unexpected server error. |
Get your starting point here, then switch to the live feed. Call this endpoint once to load current balances and positions, then subscribe to WebSocket portfolio:{userId} for updates — the live feed only sends changes, not the full picture.
Position PnL card
GET /api/portfolio/position/:tokenName/pnl-card
Authenticated. Returns a JPEG share image for a position identified by lowercase tokenName (e.g. opera, not the ticker).
Response headers include Content-Type: image/jpeg and Cache-Control: private, no-store.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenName | string | Yes | (path) Lowercase property tokenName (letters a-z only, max 20). |
Errors
| Status | Code | Description |
|---|---|---|
401 | unauthorized | Missing or invalid authentication. |
404 | not_found | No position for that tokenName. |
422 | — | Cannot render (e.g. no market price available). |
500 | internal_error | Unexpected server error. |