Complete REST API for the ETF portfolio optimizer. Tax-aware allocation, risk metrics, semantic ETF search, and factor analysis.
Join the waitlist at /api-access. We'll provision a key like etf_live_<32 hex chars>. Keys are per-tenant and can be rotated or revoked.
Send a POST to /portfolio/optimize with your investment constraints. The API returns optimized ETF weights, risk metrics, and tax analysis.
combined_weights is a {ticker: weight} map. equity_pct and bond_pct give the sleeve split. Risk metrics include volatility, Sortino ratio, and efficient frontier points.
Use /portfolio/search for semantic ETF discovery, /instruments for the ETF universe, and /portfolio/etf-factor-scorecard/{ticker} for factor analysis.
All protected endpoints require an X-API-Key header. Keys are per-tenant, formatted as etf_live_<32 hex chars>, and can be rotated or revoked at any time.
Rate limiting is applied per tenant via a Redis token bucket. The X-Tenant-Id response header confirms which tenant was billed for the request.
curl https://myetfportfolio.com/v1/portfolio/optimize \ -H "X-API-Key: etf_live_yourkeyhere" \ -H "Content-Type: application/json"
| Code | Meaning | How to fix |
|---|---|---|
| 401 | Invalid or missing X-API-Key header. | Verify your API key is correct and hasn't been revoked. |
| 422 | Request body failed Pydantic validation. | Check field types and required fields against the schema. |
| 429 | Rate limit exceeded (fair-use limit for your API key). | Wait for the Retry-After period, then retry. No paid upgrade path is published today. |
| 500 | Internal server error. | Retry with exponential backoff. Contact support if persistent. |
Fair-use limits apply per API key (typically a modest RPM). When exceeded, the API returns 429 with a Retry-After header (in seconds). There are no published paid tiers or upgrade SKUs — limits exist to keep the service healthy.
Responses are portfolio ideas and scorecards, not advice or order routing. See disclaimer on AllocatorResponse and methodology at /how-we-model.
Core optimization endpoints — send constraints, get optimized ETF allocations with risk and tax metrics.
/portfolio/optimizeKEYCore decision-support endpoint. Send constraints (capital, risk, tax rates, horizon) and receive a multi-ETF target allocation with after-tax metrics, parametric tail risk, FoF scorecard vs AOR-class funds, TLH notes, and illustrative stress. Always implements the multi-ETF book — FoF is scorecard only, never a substitute CALL.
AllocatorRequest → Response: AllocatorResponsecurl -X POST https://myetfportfolio.com/v1/portfolio/optimize \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"policy_id": "taxable_low_tax",
"capital": 100000,
"risk_tolerance": "MODERATE",
"creativity": "balanced",
"account_tax_type": "taxable",
"federal": 0.24,
"state": 0.05,
"ltcg": 0.15,
"investment_horizon_years": 10
}'{
"disclaimer": "Decision support / ideas — not investment advice. ...",
"equity_pct": 60,
"bond_pct": 40,
"combined_weights": {
"SCHB": 0.27, "SCHF": 0.15, "SCHV": 0.10, "SPLV": 0.08,
"BND": 0.20, "BIV": 0.10, "BSV": 0.05, "VTIP": 0.05
},
"estimated_after_tax_return_pct": 6.85,
"estimated_portfolio_volatility_pct": 12.4,
"estimated_sortino_ratio": 0.82,
"blended_expense_ratio": 0.004,
"bond_weighted_duration": 6.2,
"tax_savings_bps": 47.0,
"efficient_frontier": [
{"equity_pct": 0.10, "volatility": 3.2, "after_tax_return": 3.1},
{"equity_pct": 0.60, "volatility": 12.4, "after_tax_return": 6.85}
],
"benchmark_comparison": {
"benchmark_symbol": "AOR",
"recommendation": "custom_portfolio",
"implementation_call": "custom_multi_etf",
"fof_scorecard": "beats_simple_fof",
"custom_portfolio_justified": true,
"matched_risk_edge_bps": 42.0,
"fee_edge_bps": 12.0
},
"tail_risk": {
"cvar_method": "parametric_multivariate_student_t_df6_portfolio_risk_simulation",
"var_95": 0.12,
"max_drawdown_kind": "path_wealth_simulation"
},
"acceptance_test_results": {"tests_passed": true}
}/allocator/sensitivityKEYRe-run the allocator with expected returns shocked ±10% and tax rates shocked (federal ±7pp, state ±2pp). Reports weight L1 distance and stability scores at both role level (the headline — did the asset allocation move?) and ticker level (which counts benign same-role ETF swaps), plus muni weight deltas.
AllocatorRequest → Response: objectcurl -X POST https://myetfportfolio.com/v1/allocator/sensitivity \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"policy_id":"taxable_low_tax","capital":100000,"risk_tolerance":"MODERATE","federal":0.24,"state":0.05}'{
"shock_method": "scale_live_yields_or_snapshot_±10pct; tax federal±7pp and state±2pp",
"shock_return_up": {"weights": {...}, "distance_from_base": {"l1_distance": 0.04}},
"shock_return_down": {"weights": {...}, "distance_from_base": {"l1_distance": 0.03}},
"shock_tax_high": {"federal": 0.31, "muni_weight": 0.12, "distance_from_base": {...}},
"shock_tax_low": {"federal": 0.17, "muni_weight": 0.02, "distance_from_base": {...}},
"stability_score": {"role_average_l1_distance": 0.036, "ticker_average_l1_distance": 0.156, "interpretation": "stable", "ticker_interpretation": "unstable"},
"tax_stability_score": {"role_average_l1_distance": 0.02, "ticker_average_l1_distance": 0.08, "interpretation": "stable"}
}/portfolio/greeksKEYCompute risk-factor exposures, sleeve breakdown, concentration metrics, credit quality, distribution risk, and tax metrics for a given set of weights.
PortfolioGreeksRequest → Response: PortfolioGreekscurl -X POST https://myetfportfolio.com/v1/portfolio/greeks \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"weights": {"SCHB": 0.35, "SCHF": 0.15, "BND": 0.30, "VTIP": 0.10, "SPLV": 0.10},
"federal": 0.37, "state": 0.05
}'{
"risk_exposures": {"equity_beta": 0.82, "duration": 5.1, "credit_spread": 0.34},
"sleeve_breakdown": {"equity": 0.60, "fixed_income": 0.40},
"concentration": {"max_position": 0.35, "hhi": 0.091},
"tax_metrics": {"effective_rate": 0.28, "after_tax_yield": 0.031},
"suitability_flags": []
}Search, filter, and analyze the ETF universe. Semantic search, factor scorecards, eligibility, and discovery themes.
/portfolio/searchKEYSearch ETFs by natural-language query. Uses embeddings + pgvector cosine similarity to find ETFs whose provider description semantically matches. Results are re-ranked by semantic similarity and liquidity.
SemanticSearchRequest → Response: SemanticSearchResponsecurl -X POST https://myetfportfolio.com/v1/portfolio/search \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "low cost broad market index", "limit": 5}'{
"query": "low cost broad market index",
"total": 5,
"results": [
{
"ticker": "SCHB",
"fund_name": "Schwab US Broad Market ETF",
"semantic_similarity": 0.91,
"liquidity_score": 0.98,
"blended_score": 0.94,
"expense_ratio": 0.0003,
"aum": 32000000000,
"asset_class": "equity"
}
]
}/portfolio/etf-factor-scorecard/{ticker}KEYReturns factor betas (Mkt-RF, SMB, HML, RMW, CMA, Mom), alpha, R-squared, volatility, and max drawdown for a given ETF ticker.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| ticker | string | Yes | — | ETF ticker symbol (e.g. SCHB) |
| lookback_years | integer | No | 5 | Regression lookback period |
curl https://myetfportfolio.com/v1/portfolio/etf-factor-scorecard/SCHB?lookback_years=5 \ -H "X-API-Key: YOUR_KEY"
{
"ticker": "SCHB",
"status": "ok",
"source": "fama_french_5_plus_mom",
"lookback_years": 5,
"observations": 260,
"alpha_annualized": 0.0012,
"r_squared": 0.98,
"volatility_annualized": 0.16,
"max_drawdown": -0.24,
"factor_betas": {"Mkt-RF": 0.99, "SMB": -0.05, "HML": 0.02, "RMW": 0.03, "CMA": 0.01, "Mom": 0.01}
}/portfolio/etf-masterKEYBrowse the full ETF universe with filters for theme, account fit, review status, universe role, expense ratio, and liquidity.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| theme | string | No | — | Filter by theme (e.g. core_equity, muni_bonds) |
| account_fit | string | No | — | Filter by account type fit |
| review_status | string | No | — | Filter by review status |
| universe_role | string | No | — | Filter by universe role |
| max_expense_ratio | number | No | — | Maximum expense ratio filter |
| min_liquidity_score | number | No | — | Minimum liquidity score |
| limit | integer | No | 250 | Max results |
| offset | integer | No | 0 | Pagination offset |
curl "https://myetfportfolio.com/v1/portfolio/etf-master?theme=core_equity&limit=10" \ -H "X-API-Key: YOUR_KEY"
[
{"symbol": "SCHB", "name": "Schwab US Broad Market ETF", "expense_ratio": 0.0003, "aum": 32000000000},
{"symbol": "VTI", "name": "Vanguard Total Stock Market ETF", "expense_ratio": 0.0003, "aum": 41000000000}
]/portfolio/etf-discoveryKEYList ETF discovery candidates filtered by minimum liquidity score and theme.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| min_liquidity_score | number | No | 0.0 | Minimum liquidity score threshold |
| theme | string | No | — | Filter by discovery theme |
/portfolio/etf-discovery-themesKEYReturns all available ETF discovery themes with candidate counts, seed counts, and metadata.
curl https://myetfportfolio.com/v1/portfolio/etf-discovery-themes \ -H "X-API-Key: YOUR_KEY"
{
"themes": [
{"theme": "core_equity", "account_fit": "general", "candidate_count": 45, "innovation_score": 0.10},
{"theme": "tax_aware_muni", "account_fit": "taxable", "candidate_count": 22, "innovation_score": 0.15}
]
}/portfolio/etf-universe-summaryKEYHigh-level summary of the ETF universe: total count, breakdown by asset class, theme, and review status.
/portfolio/etf-provider-healthKEYHealth metrics for ETF providers (AUM trends, closure risk, fund family concentration).
/portfolio/etf-eligibilityKEYList ETFs eligible for specific optimization models, with filters for model ID, eligibility status, theme, and account fit.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| model_id | string | No | — | Filter by model ID |
| eligible | boolean | No | — | Filter by eligibility |
| theme | string | No | — | Filter by theme |
| account_fit | string | No | — | Filter by account fit |
| limit | integer | No | 250 | Max results |
| offset | integer | No | 0 | Pagination offset |
/portfolio/etf-eligibility-modelsKEYReturns all available optimization model IDs with labels, descriptions, and default account types.
Treasury curves, snapshot yields, yield comparisons, and instrument metadata.
/fred/curveKEYFetch the current Treasury yield curve, SOFR rate, and equity indices from FRED. Requires FRED_API_KEY on the server.
curl https://myetfportfolio.com/v1/fred/curve \ -H "X-API-Key: YOUR_KEY"
{
"curve": {"1M": 0.052, "3M": 0.048, "6M": 0.045, "1Y": 0.042, "2Y": 0.041, "5Y": 0.040, "10Y": 0.041, "30Y": 0.043},
"sofr": 0.053,
"equity_indices": {"SPX": 5432.10, "NDX": 19234.50}
}/snapshot-yieldsKEYCurrent snapshot of yields for all tracked instruments (money market, T-bills, muni, BOXX, etc.).
curl https://myetfportfolio.com/v1/snapshot-yields \ -H "X-API-Key: YOUR_KEY"
{
"yields": {"SGOV": 0.052, "USFR": 0.051, "BOXX": 0.048, "BND": 0.041},
"date": "2026-07-06"
}/yield-comparisonKEYCompare gross yield, after-tax yield, and tax-equivalent yield for key cash/fixed-income ETFs given a tax profile.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| federal | number | No | 0.37 | Federal marginal rate |
| niit | number | No | 0.038 | Net investment income tax |
| state | number | No | 0.0399 | State marginal rate |
| muni_instate | number | No | 0.0 | In-state muni fraction |
curl "https://myetfportfolio.com/v1/yield-comparison?federal=0.37&state=0.05" \ -H "X-API-Key: YOUR_KEY"
[
{"symbol": "SGOV", "type": "treasury", "gross": 5.20, "after_tax": 3.28, "tax_equiv": 5.20},
{"symbol": "BOXX", "type": "etf_boxx", "gross": 4.80, "after_tax": 4.80, "tax_equiv": null}
]/instrumentsKEYList all instruments in the fixed-income universe with type, duration, true-liquidity flag, and default cap.
/portfolio/instruments-6040KEYList all ETFs in the 60/40 equity universe with full metadata: expense ratio, dividend yield, qualified %, TLH partner, AUM, bid-ask spread.
/tax-optionsKEYReturns available tax years, federal rates, NIIT rates, and state rate presets.
Stress testing and multi-account household asset-location optimization.
/stressKEYRun a parametric stress battery on a set of portfolio weights: parallel rate shocks (±100bp), curve twists (steepener/flattener), muni credit/liquidity gaps, and one 2008-style equity-crash-plus-Treasury-rally mark. Returns a mark-to-market pnl_pct (decimal, before income) and per-symbol detail for each scenario. These are illustrative parametric shocks, not historical replays.
StressRequest → Response: StressResponsecurl -X POST https://myetfportfolio.com/v1/stress \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"weights": {"SCHB": 0.35, "SCHF": 0.15, "BND": 0.30, "VTIP": 0.10, "SPLV": 0.10}
}'{
"shocks": [
{"label": "Parallel +100bp", "pnl_pct": -0.031, "detail": {"BND": -0.062, "VTIP": -0.021}},
{"label": "Parallel -100bp", "pnl_pct": 0.031, "detail": {"BND": 0.062, "VTIP": 0.021}},
{"label": "Steepener (long +100bp)", "pnl_pct": -0.018, "detail": {"BND": -0.036}},
{"label": "Flattener (short +100bp)", "pnl_pct": -0.009, "detail": {"BSV": -0.018}},
{"label": "Muni +50bp / 2% NAV gap", "pnl_pct": -0.004, "detail": {"MEAR": -0.02}},
{"label": "Muni +150bp / 5% NAV gap", "pnl_pct": -0.011, "detail": {"MEAR": -0.05}},
{"label": "2008-style crisis (equity crash + Treasury rally)", "pnl_pct": -0.184, "detail": {"SCHB": -0.37, "BND": 0.05}}
]
}/household/optimizeKEYTwo-stage household asset-location optimization. Stage 1 runs the standard allocator on household total capital to determine target weights. Stage 2 assigns assets across account wrappers (taxable, IRA, Roth, 401k) by after-tax priority to maximize tax efficiency. Returns per-wrapper allocations and location benefit in basis points.
HouseholdAllocatorRequest → Response: HouseholdAllocatorResponsecurl -X POST https://myetfportfolio.com/v1/household/optimize \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"wrappers": [
{"account_tax_treatment": "taxable", "capital": 150000},
{"account_tax_treatment": "ira", "capital": 200000},
{"account_tax_treatment": "roth", "capital": 100000}
],
"policy_id": "taxable_low_tax",
"risk_tolerance": "MODERATE",
"federal": 0.37,
"state": 0.05,
"ltcg": 0.20
}'{
"household_weights": {"SCHB": 0.27, "SCHF": 0.15, "BND": 0.20, "SPLV": 0.08, "VTIP": 0.05},
"per_wrapper": [
{"wrapper": "taxable", "weights": {"SCHB": 0.30, "SPLV": 0.12, "VTIP": 0.08}},
{"wrapper": "ira", "weights": {"SCHF": 0.25, "BND": 0.25}},
{"wrapper": "roth", "weights": {"SCHB": 0.20, "SCHV": 0.10}}
],
"location_benefit_bps": 23.5,
"household_equity_pct": 0.60,
"household_bond_pct": 0.40,
"notes": ["Tax-exempt bonds placed in taxable account", "High-growth equities tilted to Roth"]
}Multi-model orchestration and validation endpoints for running multiple optimization models in one call.
/portfolio/orchestrateKEYRun the orchestration pipeline across multiple models for a given account type and objective. Returns per-model results.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| account_type | string | No | taxable | Account type |
| objective | string | No | research | Objective |
| limit_per_model | integer | No | 10 | Results per model |
/portfolio/orchestrate/optimizeKEYOptimize across multiple models in one call. Takes account type, risk level, capital, and tax parameters.
PortfolioOrchestrateOptimizeRequest → Response: objectcurl -X POST https://myetfportfolio.com/v1/portfolio/orchestrate/optimize \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_type": "taxable",
"risk_level": "moderate",
"capital": 100000,
"federal": 0.37,
"state": 0.05
}'{
"models": {
"taxable_low_tax": {"equity_pct": 0.60, "combined_weights": {...}},
"taxable_high_tax": {"equity_pct": 0.50, "combined_weights": {...}}
}
}/portfolio/validate-6040KEYValidate the taxable 60/40 model for a given capital, risk tolerance, and covariance mode.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| capital | number | No | 100000 | Portfolio capital |
| risk_tolerance | string | No | MODERATE | Risk tolerance |
| covariance_mode | string | No | heuristic | Covariance mode |
Unauthenticated endpoints — waitlist signup and health check.
/waitlist/signupPUBLICPublic endpoint — no API key required. Rate-limited by client IP (5 req/min). Honeypot field (website) silently succeeds without storing. Duplicate emails are idempotent. Always returns {"status": "ok"} to avoid leaking internal details.
WaitlistSignupRequestcurl -X POST https://myetfportfolio.com/v1/waitlist/signup \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "company": "Acme Capital", "use_case": "Robo-advisor backend"}'{"status": "ok"}/healthPUBLICReturns service health status. No authentication required.
curl https://myetfportfolio.com/v1/health
{"status": "ok", "plane": "research", "time": "2026-07-14T12:00:00", "database": {"ok": true}}Key fields for the main request and response models. The full OpenAPI 3.1 spec (75+ fields on AllocatorRequest, 39 on AllocatorResponse) is available as openapi.json.
| Field | Type | Default |
|---|---|---|
| policy_id | string | taxable_low_tax |
| capital | number | 100000 |
| risk_tolerance | string | MODERATE |
| creativity | string | balanced |
| account_tax_type | string | taxable |
| federal | number | 0.37 |
| state | number | 0.0399 |
| ltcg | number | 0.20 |
| niit | number | 0.038 |
| investment_horizon_years | integer | 10 |
| bond_duration_ceiling | number | 5.0 |
| bond_style | string | any |
| factor_model_enabled | boolean | false |
| black_litterman_enabled | boolean | false |
| expected_return_shrinkage | number | 0.5 |
| max_positions | integer | null |
| excluded_symbols | string[] | null |
| Field | Type | Description |
|---|---|---|
| disclaimer | string | Decision-support / ideas — not investment advice and not order routing. Present on every response. |
| equity_pct | number | Equity sleeve allocation (decimal, e.g. 0.60 = 60%) |
| bond_pct | number | Bond sleeve allocation (decimal) |
| combined_weights | object | {ticker: weight} map of the full portfolio |
| equity_weights | object | {ticker: weight} map of equity sleeve only |
| bond_weights | object | {ticker: weight} map of bond sleeve only |
| estimated_after_tax_return_pct | number | Estimated annual after-tax return (%) |
| estimated_portfolio_volatility_pct | number | Estimated annual volatility (%) |
| estimated_sortino_ratio | number | Sortino ratio (risk-adjusted return) |
| blended_expense_ratio | number | Weighted-average ETF expense ratio |
| bond_weighted_duration | number | Weighted-average bond duration (years) |
| tax_savings_bps | number | Tax savings vs naive allocation (basis points) |
| efficient_frontier | array | Efficient frontier points: {equity_pct, volatility, after_tax_return} |
| benchmark_comparison | object | Benchmark frontier comparison data |
| tail_risk | object | Tail risk metrics: VaR/CVaR from a multivariate Student-t (df=6) simulation plus a path-wealth max drawdown |
| tlh_opportunities | array | Tax-loss harvesting opportunities |
| stress_scenarios | array | Parametric stress results (rate shocks, curve twists, muni gaps, 2008-style crisis) |
| acceptance_test_results | object | Internal validation checks (turnover, duration, concentration) |
| proposed_orders | array | Concrete buy/sell orders to reach target weights |
import requests
API_BASE = "https://myetfportfolio.com/v1"
API_KEY = "YOUR_KEY"
# Optimize a portfolio
response = requests.post(
f"{API_BASE}/portfolio/optimize",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
json={
"policy_id": "taxable_low_tax",
"capital": 100000,
"risk_tolerance": "MODERATE",
"account_tax_type": "taxable",
"federal": 0.37,
"state": 0.05,
"ltcg": 0.20,
},
)
data = response.json()
print(f"Equity: {data['equity_pct']:.0%}")
print(f"Bonds: {data['bond_pct']:.0%}")
print(f"After-tax return: {data['estimated_after_tax_return_pct']:.2f}%")
print(f"Volatility: {data['estimated_portfolio_volatility_pct']:.2f}%")
print(f"Sortino: {data['estimated_sortino_ratio']:.2f}")
for ticker, weight in data["combined_weights"].items():
print(f" {ticker}: {weight:.1%}")
/portfolio/social-searchKEYSocial media ETF search
Search social media (Reddit, YouTube) for ETF-related discussions and analysis by topic. Returns curated posts with relevance scoring.
{ "topic": "SCHB vs VTI", "results": [ {"source": "reddit", "title": "SCHB vs VTI - virtually identical?", "url": "...", "sentiment": "neutral"}, {"source": "youtube", "title": "Best Total Market ETFs 2026", "url": "...", "sentiment": "positive"} ] }