GET https://api.ria.us/openapi.jsonALLOCATOR℠ is a portfolio-optimization API that applies Merton continuous-time portfolio theory with constant-relative-risk-aversion (CRRA) utility to compute a household’s optimal allocation between the Empirical Global Market Portfolio (EGMP-14) and a risk-free asset.
The unit of analysis is the household — either a single individual (client)
or a married couple (client + spouse). Every household asset is modelled:
Social Security, Medicare, pensions, human capital (remaining labor earnings),
businesses, real estate, vehicles, bank CDs, annuities, life-insurance cash
values, and any other illiquid background wealth. Present values are computed
using CAPM-based discount rates and sex-specific actuarial life tables, and
expected returns / covariances are iteratively adjusted to an after-tax basis.
A single POST /allocator call returns:
max_premium).| Mode | Who it is for | Authentication | Billing |
|---|---|---|---|
| RIA Client | Registered investment advisers (B2B integrations) | X-User-Token |
Monthly in arrears, auto-charge |
| Individual Client | A California resident using the API for their own household | X-Payment-Intent (via Stripe) |
Pay-per-call, captured on success |
The underlying computation is identical for both. The only differences are how you authenticate and how you pay. See §4 — Authentication.
Advisory API Systems LLC is registered only in the State of California. Every
/allocator request must represent a household where:
client.personal_info.address.state == "CA"client.personal_info.address.zip_code is a 5-digit ZIP in the range
90000–96199Requests from non-California households are rejected with a 400 response and
rule: california_residency.
X-User-Token issued by Advisory API Systems
(email [email protected] to request one).pm_...). In
practice this means a browser and a Stripe Elements form — 3-D Secure cannot
be satisfied from the command line.curl, httpie, Postman, any
modern HTTP library.By making any API request you agree to the User Agreement, Privacy Policy, Data Processing Agreement, and Investment Advice Disclaimer. Relevant documents:
If you are submitting a spouse block, you must have the spouse’s authority to provide their data — see §7.3 — Spouse block and User Agreement §2.7.
All endpoints are served from:
https://api.ria.us
application/json.The API version is returned on GET /health as version: "3.2.0". Breaking
changes will produce a new major version path; backward-compatible additions
are rolled forward in-place. The OpenAPI specification is authoritative: if
anything in this guide disagrees with GET /openapi.json, the spec wins.
Authentication is via a single HTTP request header. Each request carries exactly one of the following:
| Header | Mode | Who uses it |
|---|---|---|
X-User-Token |
RIA Client | RIAs, integrators |
X-Payment-Intent |
Individual Client | Pay-per-call, one request |
X-Admin-Token |
Administrative | Reserved; not public |
Unauthenticated endpoints (/health, /openapi.json, /docs, /spec,
/create-payment-intent) accept requests with no auth header at all.
X-User-Token — RIA ClientsThis is a static token issued to you by Advisory API Systems. You present it
on every /allocator request:
X-User-Token: asys_sk_live_your_token_here
On each successful /allocator call, a Stripe metered-usage event is
recorded. At calendar month-end, Stripe automatically charges the payment
method on file with your firm’s Stripe customer record on a graduated
monthly tier schedule: $34.95 per call for the first 25 calls in a
calendar month, $27.95 per call for calls 26–100, and $21.95 per
call thereafter (see §11 — Pricing).
Per-token rate limit: 20 requests per rolling 60-minute window. Requests in excess of this limit receive a 429 response and are not billed.
Failed requests are never billed. Specifically:
X-Payment-Intent — Individual Clients (pay-per-call)Pay-per-call is a two-step flow. First authorize a Stripe PaymentIntent
for $79.95; then call /allocator with the PaymentIntent ID on the header.
The card is captured only if /allocator returns 2xx.
The authorize step requires a Stripe-tokenized payment method (pm_...),
which in turn requires a browser capable of completing 3-D Secure. curl
alone cannot complete the authorize step for any card that triggers 3DS —
use a browser or Stripe’s own SDKs for that step, then use curl for the
/allocator step if you like.
The full flow is:
[Browser / Stripe.js] → POST /create-payment-intent → pi_... (authorized)
[Your client] → POST /allocator (X-Payment-Intent: pi_...)
[Server] → Captures card on 200 / Cancels hold on 4xx/5xx
See §5.5 — POST /create-payment-intent
and §10.1 — Pay-per-call worked example.
From the Security Practices Documentation (incorporated into the User Agreement):
-k / --insecure
to curl in production.Six endpoints, grouped below. For each, the authoritative contract is in
openapi.yaml; the summaries here are just the integrator-facing view with
copy-pasteable curl.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health |
none | Liveness probe |
| GET | /openapi.json |
none | OpenAPI 3.1 specification (JSON) |
| GET | /docs |
none | Swagger UI |
| GET | /spec |
none | Input-validation spec (frontend-consumable) |
| POST | /create-payment-intent |
none | Authorize a pay-per-call Stripe PaymentIntent |
| POST | /allocator |
X-User-Token or X-Payment-Intent |
Run the household optimization |
GET /healthReturns the API’s liveness status, server timestamp (Pacific Time), developer identifier, and running version. Suitable for uptime probes.
curl -sS https://api.ria.us/health | jq
Response:
{
"status": "healthy",
"timestamp": "2026-04-20 11:42:37 PDT",
"api_developer": "Advisory API Systems LLC",
"version": "3.2.0"
}
Note: throughout this guide, jq is used just to pretty-print. You can omit it.
GET /openapi.jsonReturns the full OpenAPI 3.1 specification as JSON, generated server-side from
the canonical openapi.yaml on each request. Import this URL directly into
Postman (“Import → Link”), Swagger UI, or any OpenAPI-aware client-generator.
curl -sS https://api.ria.us/openapi.json -o openapi.json
To import into Postman:
Postman → Import → Link → https://api.ria.us/openapi.json → Continue
GET /docsServes Swagger UI, loading /openapi.json. Paste an X-User-Token into the
auth field at the top of the page to use “Try it out” interactively.
Open https://api.ria.us/docs in a browser.
GET /specReturns input_spec.yaml serialized as JSON. This is the same rules file the
backend compiles into its JSON Schema validator. The web interface fetches
this endpoint on load so its dropdowns, numeric min/max values, and field
hints stay synchronized with validation. Useful for building your own UI.
curl -sS https://api.ria.us/spec | jq '.enums | keys'
Top-level keys: meta, enums, ranges, required, array_items,
conditional, error_response.
POST /create-payment-intentAuthorizes a Stripe PaymentIntent in manual-capture mode for $79.95. The
card is captured only on a successful /allocator response; any other outcome
cancels the hold.
Request body:
{ "payment_method_id": "pm_1PExAmPlEtOkEn123" }
curl:
curl -sS -X POST https://api.ria.us/create-payment-intent \
-H "Content-Type: application/json" \
-d '{"payment_method_id":"pm_1PExAmPlEtOkEn123"}' | jq
Success response (200):
{
"status": "success",
"payment_intent_id": "pi_3NXyZ1AbCdEfGhIj0KlMnOpQ",
"client_secret": "pi_3NXyZ1AbCdEfGhIj0KlMnOpQ_secret_AbCdEfGh",
"amount": 79.95
}
After receiving this response, the browser must confirm the client_secret
with stripe.confirmCardPayment() (this is where 3-D Secure happens). Then
present the payment_intent_id on the X-Payment-Intent header of the
/allocator request.
Failure modes:
| HTTP | error |
Meaning |
|---|---|---|
| 400 | — | Missing body or payment_method_id not starting pm_ |
| 402 | Payment Failed |
Stripe declined the card |
| 500 | — | Internal Stripe / server error |
POST /allocatorThe main endpoint. Accepts a complete household profile and returns the allocation, portfolio statistics, balance sheet, income statement, and strategy recommendations.
Request body: see §6 — Request payload: structure.
curl — RIA Client (X-User-Token):
curl -sS -X POST https://api.ria.us/allocator \
-H "Content-Type: application/json" \
-H "X-User-Token: $ALLOCATOR℠_TOKEN" \
-d @household.json | jq
curl — Individual Client (X-Payment-Intent):
curl -sS -X POST https://api.ria.us/allocator \
-H "Content-Type: application/json" \
-H "X-Payment-Intent: $ALLOCATOR℠_PI" \
-d @household.json | jq
Response (200): see §9 — Response payload: field-by-field.
Failure modes (common):
| HTTP | error |
Cause |
|---|---|---|
| 400 | validation_failed |
Schema / array-item / cross-field rule violation |
| 400 | Asset validation failed |
Real-estate address or VIN failed external validation |
| 401 | Unauthorized |
Invalid X-User-Token (only when that header was supplied) |
| 402 | Payment Required |
Missing / invalid / expired / captured X-Payment-Intent |
| 500 | internal_error |
Server-side exception; retry with exponential backoff |
The top-level shape is:
{
"client": {
"personal_info": { ... },
"financial_info": { ... }
},
"spouse": {
"personal_info": { ... },
"financial_info": { ... }
},
"household_total_annual_expenses": 12000
}
client is always required.spouse is required if and only if client.personal_info.married == true.household_total_annual_expenses is optional and applies to the whole
household (not per-person).The smallest request that can succeed is a single, unmarried client with one SSA earnings record. Everything else — employments, businesses, retirement accounts, real estate, vehicles, bank CDs, life insurance — is optional.
{
"client": {
"personal_info": {
"name": { "first_name": "Henry", "last_name": "Mason" },
"address": {
"street_number": "4248",
"street_name": "La Salle Ave",
"city": "Culver City",
"state": "CA",
"zip_code": "90232"
},
"email_address": "[email protected]",
"dob": "1930-06-18",
"gender": "male",
"married": false
},
"financial_info": {
"max_premium": 150000,
"household_allocatable_wealth": 2131650.27,
"tax_filing_status": "single",
"quarters_of_coverage": 88,
"ssa_earnings_record": [
{ "year": 1974, "earnings": 2044 }
]
}
}
}
Add a spouse block with personal_info and financial_info. married is
implicit on the spouse side (submitting a spouse block is a representation of
spousal authority — User Agreement §2.7).
{
"client": {
"personal_info": {
"name": { "first_name": "John", "last_name": "Smith" },
"address": { "street_number": "4240", "street_name": "Baldwin Ave",
"city": "Culver City", "state": "CA", "zip_code": "90232" },
"email_address": "[email protected]",
"dob": "1980-02-10",
"gender": "male",
"married": true
},
"financial_info": {
"max_premium": 142000,
"household_allocatable_wealth": 1000000,
"tax_filing_status": "married filing jointly",
"quarters_of_coverage": 92,
"ssa_earnings_record": [ { "year": 2023, "earnings": 180610 } ]
}
},
"spouse": {
"personal_info": {
"name": { "first_name": "Jane", "last_name": "Smith" },
"address": { "street_number": "4240", "street_name": "Baldwin Ave",
"city": "Culver City", "state": "CA", "zip_code": "90232" },
"email_address": "[email protected]",
"dob": "1982-02-04",
"gender": "female"
},
"financial_info": {
"max_premium": 144000,
"tax_filing_status": "married filing jointly",
"quarters_of_coverage": 16,
"ssa_earnings_record": [ { "year": 2017, "earnings": 80000 } ]
}
},
"household_total_annual_expenses": 12000
}
The household profile maps onto several categories of input:
| Category | Location in payload | Example fields |
|---|---|---|
| Identity | .personal_info.{name,address,dob,gender,email_address,phone} |
|
| Marital status | client.personal_info.married |
boolean |
| Wealth to allocate | client.financial_info.household_allocatable_wealth |
number ≥ 0 |
| Risk tolerance | client.financial_info.max_premium and, if married, spouse.financial_info.max_premium |
($50k, $300k) exclusive |
| Tax filing | .financial_info.tax_filing_status |
enum |
| Social Security history | .financial_info.ssa_earnings_record[] |
array of {year, earnings} |
| Social Security claiming | .financial_info — claiming-age / benefit / optimization fields |
see §7.2 |
| Human capital | .financial_info.employments[] |
employer, income, retire age, occupation category |
| Business capital | .financial_info.businesses[] |
EIN, net income, category, ownership |
| DC plans (IRAs) | .financial_info.retirement.defined_contribution_plans.accounts_IRA[] |
|
| DC plans (401(k)) | .financial_info.retirement.defined_contribution_plans.accounts_401k[] |
|
| DB plans | .financial_info.retirement.defined_benefit_plans.pensions[] |
|
| Annuities | .financial_info.retirement.annuities.contracts[] |
|
| Life insurance | .financial_info.life_insurance.policies[] |
|
| Real estate | .financial_info.real_estate[] |
|
| Vehicles | .financial_info.vehicles[] |
|
| Bank CDs | .financial_info.bank_CDs[] |
|
| Miscellaneous income | .financial_info.other_annual_income (+ other_income_ownership) |
|
| Miscellaneous wealth | .financial_info.other_background_wealth (+ other_background_wealth_ownership) |
|
| Household expenses | top-level household_total_annual_expenses |
This section is organized by location in the JSON tree. Ranges and enums
shown below come from input_spec.yaml — the single source of truth for
validation — and are exactly what GET /spec returns.
client.personal_infoRequired on every request.
name.first_name — string, required.name.last_name — string, required.address.street_number — string, required.address.street_name — string, required.address.unit_number — string, optional. Apartment, suite, or unit designation.address.city — string, required.address.state — string, required. Must equal "CA" — the service is California-only for now.address.zip_code — string, required. A 5-digit ZIP code in the California range 90000–96199.email_address — string, required. RFC-5322 email format.dob — string, required. Date of birth in YYYY-MM-DD format.gender — enum, required. One of male or female. Used to select the correct sex-specific actuarial life table for present-value calculations.married — boolean, required. If true, the top-level spouse block is required.is_disabled_for_ss_purposes — boolean, optional. Defaults to false. Set true for clients (or spouses) who meet the SSA’s medical definition of disability under 42 U.S.C. §423(d) and have enough quarters of coverage to qualify for Social Security Disability Insurance under the SSA duration-of-work test (age-graduated minimums: 6 QC at age <24, scaling to 20 QC at age 31+; see SSA POMS DI 25201.005). When true, the model treats SS retirement-benefit and Medicare Part A entitlement as starting at current_age rather than at the normal retirement age: no future FICA contributions are projected, AIME is rescaled over the SSA’s elapsed-years window (not 35), and Medicare entitlement begins at current_age (the SSDI 24-month waiting period is approximated by gating the Medicare path on current_age ≥ 26). Surfacing in the response: social_security_details.{client,spouse}.calculation_approach will contain "ssdi_...", and the SS benefit appears in income_statement.income[] as currently received (not in future_income[]).phone.mobile_phone — string, optional.phone.work_phone — string, optional.social_security_number — string, optional. Not used in the optimization itself; retained only when supplied, for client-file completeness.client.financial_infoRequired fields: max_premium, tax_filing_status,
household_allocatable_wealth, quarters_of_coverage. ssa_earnings_record
is optional — omit it for clients with no SSA-covered employment
history (e.g., living on inheritance, expecting only spousal benefits).
max_premium — number, required. Must be strictly greater than 50,000 and strictly less than 300,000 (both endpoints excluded).
The dollar amount the household would pay in premium to avoid a 10% chance of a $400,000 loss on $1,000,000 of wealth. Used to derive the CRRA risk-aversion coefficient γ via revealed preference.
household_allocatable_wealth — number, required. Must be ≥ 0.
Liquid wealth available to allocate (cash, brokerage balances, any other liquid funds). Excludes retirement accounts, real estate, vehicles, life-insurance cash values, bank CDs, annuities, and other background wealth — all of those are captured in their own fields and must not be double-counted here.
client_separate_allocatable_wealth — number, optional. Must be ≥ 0 when present.
The portion of household_allocatable_wealth that is the client’s
separate property (inheritance, gifts, pre-marital assets). Only
permitted when filing status is MFS, or HoH-while-married with the spouse
filing MFS. See §7.6 — Separate property.
tax_filing_status — enum, required. One of: single, married filing jointly, married filing separately, or head of household. Must align with marital status — see §8.2 — Rule families for the cross-field constraint.
self_employed — boolean, optional. Defaults to false. Affects SECA vs FICA and Medicare surcharge calculations.
quarters_of_coverage — integer, required. Range: 0 to 160 inclusive.
Social Security quarters of coverage (CURRENT snapshot at input).
Typically auto-computed from the ssa_earnings_record (if supplied)
against SSA’s indexed earnings thresholds (42 U.S.C. §213(a)(2)).
40 quarters are required both for Social Security retirement benefits
on the client’s own record (42 U.S.C. §414(a) “fully insured” status)
and for premium-free Medicare Part A (42 U.S.C. §426).
For eligibility purposes the model PROJECTS forward from the current snapshot using the client’s current salary: each year of work earns up to 4 QC, capped at 160 total. A 35-year-old at 20 QC currently earning $60K will be modeled as 140 QC at retirement and treated as fully insured. A client with no current earnings won’t accrue projected QC and will remain ineligible if the snapshot is below 40.
Medicare eligibility additionally honors the spousal-record pathway:
a married client whose spouse projects to 40+ QC is treated as
eligible for premium-free Part A (42 U.S.C. §426(b)). The Social
Security mirror pathway — a not-fully-insured client (or spouse)
claiming 50% of an insured partner’s PIA under 42 U.S.C. §402(b) —
is now implemented in both directions, client-on-spouse and
spouse-on-client. The benefit equals 0.5 × insured-partner PIA,
claimed at the not-insured side’s full retirement age (or later if
the insured partner delays claiming past that point), and is
surfaced via social_security_details with
calculation_approach: "client_spousal_on_spouse_record" or
the symmetric "spousal_benefit_qc_below_40" for the reverse
direction. SSDI is also modeled via is_disabled_for_ss_purposes
(see §7.1). ESRD is not modeled.
ssa_earnings_record[] — array, optional. May be omitted entirely
or supplied as an empty array for clients with no SSA-covered
employment history.
Each entry is an object with year (integer, 1937–2100) and earnings
(number, ≥ 0). Supply values exactly as they appear on the client’s
official SSA statement at ssa.gov; the statement already excludes
non-covered earnings. When omitted, quarters_of_coverage is taken
directly from the value supplied in financial_info (which must still
be present, even if zero).
actual_monthly_benefit — number, optional. Supply only if the client is already claiming Social Security.
actual_claiming_age — number, optional. Range: 62–70. The age at which Social Security was actually claimed.
fixed_claiming_age — number, optional. Range: 62–70. A planned Social Security claiming age; if set, overrides assume_optimal_claiming and optimize_claiming. At most one of actual_claiming_age and fixed_claiming_age may be supplied.
expected_retirement_age — number, optional. Range: 50–85. Age at which the client plans to stop working. Defaults to Full Retirement Age when not supplied.
assume_optimal_claiming — boolean, optional. Defaults to true. For clients age 62+ who have not yet claimed: assume claiming at age 70 to maximize delayed retirement credits. Ignored when fixed_claiming_age is set or optimize_claiming is true.
optimize_claiming — boolean, optional. Defaults to true. Maximize benefits by delaying to age 70. Ignored when fixed_claiming_age is set.
Tip. Supply the SSA earnings record exactly as it appears on the client’s official SSA statement (ssa.gov). The SSA statement already excludes non-covered earnings (from 2025 onward the Windfall Elimination Provision and Government Pension Offset are gone — the Social Security Fairness Act was signed January 5, 2025).
other_annual_income — number, optional. Must be ≥ 0 when present.
Taxable income not captured elsewhere: trust distributions, royalties,
alimony received, non-real-estate rental income, disability payments,
etc. Do not include gifts or inheritances (IRC §102(a) exclusion) —
roll those into household_allocatable_wealth or
other_background_wealth instead. Do not double-count income already
reported via employments, businesses, pensions, annuities, or Social
Security.
other_income_ownership — enum, optional. One of individual, joint, or community (see §7.5 — Community property and ownership).
other_background_wealth — number, optional. Must be ≥ 0 when present.
Net value (value minus related debt) of any other background assets not captured elsewhere: collectibles, precious metals, private equity, hedge funds, LP interests, crypto, mineral/water rights, IP, unvested RSUs or options, deferred-compensation balances, structured-settlement receivables, tax-lien certificates, etc.
other_background_wealth_ownership — enum, optional. One of individual, joint, or community.
rents_residence — boolean, optional. Defaults to false.
If true, the person does not own the home they live in. No real_estate
entry for this person may have owner_occupied = true, and the annual
rent should be included in the top-level
household_total_annual_expenses.
Required when client.personal_info.married == true.
spouse.personal_info mirrors client.personal_info without the
married field (submitting a spouse block is itself a representation of
spousal authority).spouse.financial_info mirrors client.financial_info with two differences:
household_allocatable_wealth — that’s a household-level value and
lives only on the client side.spouse_separate_allocatable_wealth replaces the client’s
client_separate_allocatable_wealth.spouse.financial_info.tax_filing_status is auto-synced:
User Agreement §2.7 — by submitting a spouse block you represent that you have legal authority to provide the spouse’s data, to receive the Form ADV and disclosures on their behalf, and to enter into the User Agreement on their behalf. The spouse is a Client of the Company and must receive the User Agreement, Privacy Policy, and ADV disclosures and be informed of the five-business-day right to terminate.
Every array item has its own required/optional field set. The rules below
match input_spec.yaml → array_items.
ssa_earnings_record[]Required: year, earnings.
{ "year": 2024, "earnings": 183730 }
year integer, 1937 to 2100earnings number ≥ 0employments[]Required: employer_name, employee_id.
{
"employer_name": "Acme Software Company",
"employee_id": "328",
"location": { "city": "Culver City", "state": "CA" },
"job_title": "Software Engineer",
"occupation_category": 6,
"income": 184000,
"expected_retirement_age": 65
}
occupation_category enum: 6 High-skill, 7 Mid-skill, 8 Public sector,
9 Low-skill, 10 Manual labor. Drives human-capital β.employer_name + employee_id.
Duplicates are not fatal — they surface in data_quality_warnings.businesses[]Required: business_name, ein, business_category, net_income,
ownership.
{
"business_name": "John's Hardware LLC",
"ein": "78-5566676",
"business_category": 13,
"net_income": 130000,
"ownership": "community"
}
business_category enum: 11 Tech, 12 Manufacturing, 13 Retail,
14 Services, 15 Agriculture. Drives business-equity β.net_income may be negative.ein.retirement.defined_contribution_plans.accounts_IRA[]Required: account_id, account_type, balance.
{
"account_id": "IRA-123456789",
"account_type": "traditional",
"custodian": "Fidelity Investments",
"balance": 177500
}
account_type enum: traditional, roth, sep, simple.ownership field.retirement.defined_contribution_plans.accounts_401k[]Required: account_id, account_type, balance.
{
"account_id": "778899",
"account_type": "traditional",
"employer": "Acme Software Company",
"custodian": "Custodial Company of America",
"balance": 284365.15
}
account_type enum: traditional, roth, solo.solo is only valid when the household has business income, not
merely employment income.account_id.retirement.defined_benefit_plans.pensions[]Required: pension_id, plan_type, type, status, annual_amount.
{
"pension_id": "P8863760",
"plan_type": "defined_benefit",
"type": "government",
"employer": "City of Eureka",
"administrator": "HHTR",
"status": "vested",
"annual_amount": 68000
}
plan_type enum: defined_benefit, cash_balance.type enum: government, private — government pensions get a lower β
(closer to risk-free).status enum: active, vested, deferred, retired.retirement.annuities.contracts[]Required: issuer, contract_number, base_payment_amount, ownership.
{
"issuer": "Pac Life",
"contract_number": "ANN-66780",
"base_payment_amount": 1000,
"life": true,
"term_years": 0,
"ownership": "community",
"investment_in_contract": 10000,
"is_qualified": false,
"deferral_period": 10,
"benefit_growth_rate": 0.06,
"has_cola": true,
"cola_rate": 0.035,
"is_joint_life": true,
"survivor_percentage": 1
}
Conditional rules:
life == false → term_years required and > 0has_cola == true → cola_rate required (0.00–0.10)is_joint_life == true → survivor_percentage required (0–1)Important modelling note: deferred annuity income does not appear in
current-income totals. It goes into income_statement.future_income[] with
starts_in_years. Only immediate annuity payments count as current income.
life_insurance.policies[]Required: insurer, policy_number, policy_type, face_amount, ownership.
{
"insurer": "NW Life",
"policy_number": "INS-5548",
"policy_type": "whole",
"face_amount": 100000,
"net_surrender_value": 25000,
"annual_premium": 1000,
"ownership": "community"
}
policy_type enum: whole, universal, variable_universal,
indexed_universal, term.net_surrender_value counts toward background wealth. face_amount
is retained for estate-planning context but is not added to current wealth.annual_premium surfaces in the Household Income & Expense Summary as the
household-aggregated Total Annual Life Insurance Premiums line — same
cash-flow treatment as mortgage and vehicle loan payments. NSV remains in
background assets; the premium is shown as a Recurring outflow without
reducing NSV or any optimization input.real_estate[]Required: address, city, state, zip, type, purchase_price,
purchase_year, purchase_month.
{
"address": "4240 Baldwin Ave",
"city": "Culver City",
"state": "CA",
"zip": "90232",
"type": "Multi-Family",
"bedrooms": 4,
"bathrooms": 2,
"sqft": 2000,
"units": 2,
"mortgage_balance": 180000,
"purchase_price": 340000,
"purchase_year": 2008,
"purchase_month": 1,
"monthly_payment": 1400,
"ownership": "community",
"owner_occupied": true
}
type enum: Single Family, Condo, Townhouse, Manufactured,
Multi-Family, Apartment, Land.type != Land → bedrooms, bathrooms, sqft required and > 0type == Land → owner_occupied must not be truetype == Multi-Family → units required, 2–4type == Apartment → units required, 5–999purchase_price must be strictly > 0 (a zero price would break
cost-basis / capital-gains / depreciation math downstream).monthly_payment. Those are estimated automatically from
property type and current market value. monthly_payment is principal +
interest only.rents_xor_owner_occupied_per_person: each person may
have at most one owner_occupied = true property, and cannot have any
if rents_residence = true.error: "Asset validation failed".vehicles[]Required: vin, mileage, purchase_price, purchase_year, purchase_month.
{
"vin": "1N4AL3AP4EC144655",
"mileage": 70382,
"purchase_price": 17000,
"purchase_year": 2015,
"purchase_month": 3,
"amount_owed": 4500,
"monthly_payment": 350,
"ownership": "community"
}
vin must be exactly 17 chars, exclude the letters I, O, Q, and pass
the 49 CFR 565 check-digit. Server-side the VIN is also verified against a
VIN-decoder service; unknown VINs surface as Asset validation failed.purchase_price must be strictly > 0.monthly_payment is principal + interest only. Excludes vehicle
insurance, registration, fuel, and maintenance.Valuation methodology. Vehicle valuation derives from a comparable-listings analysis. We aggregate price statistics over active private-party listings matching the user’s vehicle by year, make, model, and trim, weighting toward listings with similar mileage where the data supports it, and falling back to exponential depreciation when no comparable listings are available.
bank_CDs[]Required: account_balance, current_apy.
{
"institution": { "name": "Chase", "branch": "5th and Spring" },
"account_number": "CD-66580",
"title": "36 Month CD",
"principal": 36000,
"account_balance": 36000,
"currency": "USD",
"issue_date": "2025-04-12",
"maturity_date": "2029-04-12",
"current_apy": 0.046,
"auto_renew": false,
"compounding_method": "daily",
"ownership": "community"
}
current_apy is a decimal (e.g., 0.046 for 4.6%), strictly > 0, ≤ 1.account_balance strictly > 0.compounding_method enum: daily, monthly, quarterly, annually.ownership on every asset that supports it is one of:
| Value | Meaning |
|---|---|
individual |
Titled in one person’s name only |
joint |
Jointly titled (e.g., JTWROS). Only meaningful when married |
community |
Community property under state law. Only meaningful when married AND in a community-property state (AZ, CA, ID, LA, NV, NM, TX, WA, WI) |
When client.personal_info.married == false, every asset’s ownership MUST
be individual. joint and community are rejected for unmarried clients.
For married couples with ownership: community or ownership: joint, the API
attributes the asset’s contribution symmetrically — 50/50 between spouses —
across all asset types and income categories. This matters for Social Security
calculations, tax filing, and the income statement.
client_separate_allocatable_wealth and spouse_separate_allocatable_wealth
are only permitted when:
married filing separately (MFS), or head of household
while married with the spouse filing MFS.For MFJ or unmarried clients, both separate-wealth fields must be absent or
zero. Their sum must not exceed household_allocatable_wealth. Violation
surfaces as rule: separate_property_only_when_separate_returns.
Every non-2xx response uses the same envelope:
{
"status": "error",
"error": "validation_failed",
"message": "Request failed validation (1 issue). Your card has NOT been charged.",
"details": [
{
"field": "client.financial_info.max_premium",
"message": "must be > 50000",
"rule": "schema.exclusiveMinimum"
}
]
}
status is always the string "error" on non-2xx responses.error is a stable machine-readable identifier. Branch on this — do
not parse message.message is a human-readable string intended for display.details[] is an array of per-field validation problems. Empty when the
body itself was malformed (e.g., invalid JSON).trace[] is present only when the server’s DEBUG environment variable
is TRUE. Not present in production.| Code | When it is returned |
|---|---|
| 400 | Malformed JSON, schema violation, array-item rule violation, cross-field rule violation, or RentCast/VIN-decoder asset-validation failure |
| 401 | Invalid X-User-Token (only when that header was supplied) |
| 402 | Missing / invalid / expired / already-captured X-Payment-Intent |
| 500 | Internal server error during optimization |
A missing auth header on /allocator surfaces as 402, not 401, because
pay-per-call is the default mode.
The rule field in details[] falls into exactly one of three families.
schema.<keyword> — JSON Schema violation.
<keyword> is the schema keyword that failed: required, type,
minimum, maximum, exclusiveMinimum, exclusiveMaximum, enum,
pattern, minItems, minLength, etc.
array_item_rules.<array_name> — conditional rule on an array item.
Examples: array_item_rules.real_estate (a Multi-Family property with
units outside 2–4), array_item_rules.annuity (a life: false annuity
without term_years).
Named cross-field rules:
marital_status_consistencycalifornia_residencyclaiming_age_consistencyrents_xor_owner_occupied_per_personseparate_property_only_when_separate_returnsspouse_tax_filing_status_mirrors_clientssa_earnings_min_one_entryvehicle_vin_validation (external VIN check)real_estate_address_validation (external address check)Missing required field:
{
"status": "error",
"error": "validation_failed",
"message": "Request failed validation (1 issue). Your card has NOT been charged.",
"details": [
{ "field": "client.financial_info",
"message": "must have required property 'max_premium'",
"rule": "schema.required" }
]
}
Out-of-range:
{
"status": "error",
"error": "validation_failed",
"message": "Request failed validation (1 issue). Your card has NOT been charged.",
"details": [
{ "field": "client.financial_info.max_premium",
"message": "must be > 50000",
"rule": "schema.exclusiveMinimum" }
]
}
Conditional array rule:
{
"status": "error",
"error": "validation_failed",
"message": "Request failed validation (1 issue). Your card has NOT been charged.",
"details": [
{ "field": "client.financial_info.real_estate[0].units",
"message": "units must be between 2 and 4 for Multi-Family",
"rule": "array_item_rules.real_estate" }
]
}
California residency:
{
"status": "error",
"error": "validation_failed",
"message": "Request failed validation (1 issue). Your card has NOT been charged.",
"details": [
{ "field": "client.personal_info.address.state",
"message": "Client state must be 'CA' (service is California-only).",
"rule": "california_residency" }
]
}
Invalid token:
{
"status": "error",
"error": "Unauthorized",
"message": "Invalid or missing X-User-Token"
}
Missing payment intent:
{
"status": "error",
"error": "Payment Required",
"message": "X-Payment-Intent header required for pay-per-call requests."
}
/allocator returns 4xx, the pay-per-call Stripe authorization is
cancelled immediately. No hold remains on the card./allocator returns 5xx, the authorization is cancelled as well./allocator returns 200 but the subsequent Stripe card-capture fails
twice in a row, the response still includes the full allocation but
adds a payment_warning field explaining that no charge was made. The
user is directed to email [email protected] if they wish to be billed manually.This design ensures a pay-per-call user is never left with a pending hold on their card, and never sees a charge they did not receive results for.
A successful /allocator response is a single JSON object with the following
top-level keys.
Null values. Any field that is not applicable to a particular response —
spouse-side fields when the client is unmarried, total-portfolio statistics
when the household is insolvent (see §9.3), or any computed quantity the
model declines to report because it would be misleading at a boundary — is
returned as JSON null. Treat null as “not applicable” and guard before
arithmetic or formatting (e.g., (value ?? 0).toFixed(...) rather than
value.toFixed(...)).
client_personal_informationEcho of identifying fields so callers (and their logs) can correlate requests and responses without retaining the full request body.
{
"client_first_name": "John",
"client_last_name": "Smith",
"client_email_address": "[email protected]"
}
household_max_premium, household_gamma{
"household_max_premium": 143000,
"household_gamma": 5.93
}
household_max_premium — for a single-client household this equals the
client’s max_premium; for a married household it is the combined figure.household_gamma — the CRRA risk-aversion coefficient γ derived from
household_max_premium via revealed-preference calibration. Higher γ means
more risk-averse.household{
"total_allocatable_wealth": 1000000,
"total_background_wealth": 10828578.74,
"total_wealth": 11828578.74,
"optimal_allocation": {
"empirical_global_market_portfolio": 1,
"risk_free_asset": 0
},
"optimal_portfolio": {
"allocatable": {
"before_tax": {
"expected_return": 0.213,
"std_dev": 0.1182,
"sharpe_ratio": 1.4975
},
"after_tax": {
"expected_return": 0.189,
"std_dev": 0.1113,
"sharpe_ratio": 1.4987
}
},
"total": {
"before_tax": {
"expected_return": 0.1012,
"std_dev": 0.0695,
"sharpe_ratio": 0.939
},
"after_tax": {
"expected_return": 0.0624,
"std_dev": 0.0688,
"sharpe_ratio": 0.5864
}
}
}
}
total_allocatable_wealth — echoed from household_allocatable_wealth in
the request, rounded to cents.total_background_wealth — present value of all background assets.total_wealth = the sum.optimal_allocation.empirical_global_market_portfolio (α*) — the fraction
of allocatable wealth to invest in the EGMP-14 market portfolio.optimal_allocation.risk_free_asset = 1 − α*.optimal_portfolio is nested by scope then by tax treatment:
allocatable.before_tax, allocatable.after_tax, total.before_tax,
total.after_tax. Each leaf object holds expected_return,
std_dev, and sharpe_ratio. ALLOCATOR℠ optimizes for the
after-tax case; before-tax is shown for comparison.std_dev fields are standard deviations (NOT variances).sharpe_ratio uses the same-basis risk-free rate (before-tax Sharpe
uses risk_free_rate.before_tax; after-tax Sharpe uses
risk_free_rate.after_tax).total_wealth ≤ 0 (PV of expenses
dominates PV of income streams + allocatable wealth), the four leaves
under optimal_portfolio.total return expected_return, std_dev,
and sharpe_ratio as JSON null. Under negative total wealth the
standard w_portfolio = w_dollar / W_total transformation produces
sign-flipped weights that make these statistics non-physical (e.g.,
the after-tax total expected return would exceed the before-tax
total). The optimal_portfolio.allocatable subtree is computed from
α* alone and remains valid in this case.std_dev on the total-portfolio subtree falls
below 0.005 (the optimization sits at a boundary corner — typically
α = 0 or α = 1), sharpe_ratio is returned as null rather than an
arbitrarily large quotient. The liquid-portfolio Sharpe is unaffected:
at α = 0 it correctly returns 0 (no excess return, no risk).risk_free_rate block exposes both rates as decimals
(e.g. {"before_tax": 0.036, "after_tax": 0.0221}).empirical_global_market_portfolio_contentsAn array of exactly 14 items (the EGMP-14 components), ordered by descending weight. Each item has these fields:
ticker — ETF symbol.name — ETF full name.proxy_for — the asset class this ticker represents within EGMP-14.weight — fraction of the market portfolio this ticker represents
(a decimal in [0, 1]; the 14 weights sum to 1).household_amount_to_invest — dollar amount the household should invest
in this ticker. Equals
total_allocatable_wealth × optimal_allocation.empirical_global_market_portfolio × weight,
rounded to cents.The specific tickers, asset-class labels, and weights are returned at runtime
in the /allocator response and are not enumerated here.
optimal_risk_free_allocation{
"household": {
"name": "Risk-Free Asset",
"household_allocation": 0,
"amount_to_invest": 0,
"description": "Optimal household allocation to risk-free asset"
}
}
The implementation choice (short-dated Treasury, money-market fund, savings account, CD ladder, etc.) is left to the user. The Methodology Disclosure describes the assumptions in detail.
strategy_recommendationsA map of strategy-display-name → strategy object. Which strategies appear depends on the household profile: a household with no business income will not receive Solo 401(k) or Mega Backdoor Roth recommendations; a household over the MAGI phase-out will receive Backdoor Roth instead of Roth IRA; and so on.
"strategy_recommendations": {
"Roth IRA": {
"short_description": "Individual retirement account funded with after-tax dollars; qualified withdrawals of earnings are tax-free. 2026 contribution limit is $7,500 ($8,600 with catch-up for age 50+).",
"long_description": "https://www.ria.us/strategies/Roth_IRA.html",
"rationale": "Provides tax-free growth and tax-free withdrawals in retirement. No RMDs during the original owner's lifetime.",
"risk_factors": [
"Income phase-out for 2026: $153,000-$168,000 (single), $242,000-$252,000 (MFJ)",
"5-year holding period required for qualified withdrawals"
],
"irs_citation": "IRC §408A; SECURE 2.0 Act (catch-up now indexed to inflation)"
},
"Tax Loss Harvesting": { ... },
"1031 Exchange": { ... }
}
Each strategy object has:
| Field | Type | Notes |
|---|---|---|
short_description |
string | Summary with 2026 statutory limits |
long_description |
string (URL) | Link to the long-form article on ria.us |
rationale |
string | Why it applies to this household |
risk_factors |
string OR array | May be a single string or an array of strings |
irs_citation |
string (optional) | Code section, regulation, etc. |
balance_sheetA Merton-framework economic balance sheet (not GAAP). Assets include the present value of future income streams; the single liability is the present value of future expenses.
"balance_sheet": {
"assets": [
{ "name": "Market", "type": "Allocatable Wealth", "value": 1000000 },
{ "name": "Riskless", "type": "Allocatable Wealth", "value": 0 },
{ "name": "Total Allocatable Wealth", "type": "Allocatable Wealth", "value": 1000000, "is_subtotal": true },
{ "name": "Traditional 401(k)s", "type": "Retirement Savings", "value": 284365.15 },
...
{ "name": "Social Security Net PV", "type": "Capitalized Income Streams",
"subcategory": "Social Security & Medicare", "value": 320406.95 },
...
],
"liabilities": [
{ "name": "Total Annual Household Expenses PV", "type": "Expenses", "value": 464886.59 }
],
"net_worth": 11724078.74
}
Asset type values:
| Type | Contents |
|---|---|
Allocatable Wealth |
Market, Riskless, and a total |
Retirement Savings |
IRA and 401(k) balances, by sub-type, plus a total |
Capitalized Income Streams |
Human capital PV, business capital PV, SS & Medicare & SSI, pensions, annuities, other income PV |
Tangible Background Assets |
Real-estate equity, vehicle equity, and totals |
Other Background Assets |
Bank CDs, life-insurance net surrender value, other_background_wealth |
net_worth equals total_wealth from the household block, to rounding.
Subtotal rows carry "is_subtotal": true. Any entry whose name begins
with “Total” — in either balance_sheet.assets[] or income_statement.income[]
(see §9.9) — has is_subtotal: true. Examples include Total Allocatable Wealth, Total Human Capital, Total Social Security & Medicare, Total Retirement Accounts, Total Human Capital Income, and Total Earned Income. These rows are duplicates of the within-category sum and are
included for display convenience. Consumers computing
sum(assets[*].value) (or sum(income[*].annual_amount)) should skip rows
where is_subtotal === true to avoid double-counting; net_worth and
net_income are computed without them.
retirement_accounts_by_typeHousehold totals by retirement-account type, summed across client and spouse.
All seven keys are always present; types the household does not have are
returned as 0.
{
"traditional_ira": 187500,
"roth_ira": 134000,
"sep_ira": 40000,
"simple_ira": 22000,
"traditional_401k": 284365.15,
"roth_401k": 28000,
"solo_401k": 12000
}
income_statement"income_statement": {
"income": [
{ "name": "High-skill Employment Income", "type": "Earned Income",
"subcategory": "Human Capital", "annual_amount": 184000 },
...
{ "name": "Pension Income", "type": "Retirement Income", "annual_amount": 173739.60 },
{ "name": "Social Security Income", "type": "Retirement Income", "annual_amount": 4380 },
{ "name": "RMD Income", "type": "Retirement Income", "annual_amount": 8228.23 },
{ "name": "SSI Income", "type": "Public Benefits",
"annual_amount": 14844,
"note": "Supplemental Security Income (federal FBR + CA SSP); non-taxable per 42 U.S.C. § 1382a" },
{ "name": "Investment Income", "type": "Asset-Based Income","annual_amount": 62518.78 },
{ "name": "Imputed Rent (Owner-Occupied)", "type": "Asset-Based Income",
"annual_amount": 81960, "note": "Non-taxable economic benefit of owner-occupied housing" }
],
"expenses": [
{ "name": "Total Annual Real Estate Expenses", "type": "Recurring",
"annual_amount": 119551.50 },
{ "name": "Total Annual Mortgage Payments", "type": "Recurring",
"annual_amount": 16800 },
{ "name": "Total Annual Vehicle Loan Payments", "type": "Recurring",
"annual_amount": 4200 },
{ "name": "Total Annual Life Insurance Premiums", "type": "Recurring",
"annual_amount": 1000 },
{ "name": "Total Annual Household Expenses", "type": "Recurring",
"annual_amount": 12000 }
],
"net_income": 189275.12,
"future_income": [
{
"name": "Deferred Annuity Income (Future)",
"type": "Deferred",
"annual_amount": 1790.85,
"deferred_cola_portion": 1790.85,
"deferred_joint_survivor_portion": 1790.85,
"starts_in_years": 10,
"note": "Not included in current income; will begin after deferral period"
},
{
"name": "Anna Social Security",
"type": "Future Retirement Income",
"annual_amount": 9938.70,
"starts_at_age": 67,
"starts_in_years": 32,
"starts_in_year": 2058,
"benefit_type": "spousal",
"note": "Projected Social Security benefit; capitalized PV is on balance sheet"
},
{
"name": "Ben Social Security",
"type": "Future Retirement Income",
"annual_amount": 24648.03,
"starts_at_age": 67,
"starts_in_years": 30,
"starts_in_year": 2056,
"benefit_type": "own_worker",
"note": "Projected Social Security benefit; capitalized PV is on balance sheet"
}
]
}
Income type values: Earned Income, Asset-Based Income, Retirement Income, Public Benefits, Other Income. Expense type: Recurring.
Future-income type values: Deferred (deferred annuities) and
Future Retirement Income (projected SS benefits for pre-retirement
clients/spouses).
Public Benefits covers means-tested cash assistance — Supplemental
Security Income (SSI) per 42 U.S.C. § 1381 et seq., including the
California State Supplementary Payment (SSP) for aged / blind /
disabled recipients per CA W&I Code § 12000 et seq. SSI is non-taxable
per 42 U.S.C. § 1382a and therefore does not enter the tax-rate
calculation, but does enter net_income (it is real economic income).
Eligibility is gated on the SSI resource limit ($2,000 individual /
$3,000 couple as of 2025, unindexed since 1989) and on counted income
after the $20/month general disregard and the $65/month + ½-remainder
earned-income disregard. The line is omitted by nonzero_filter when
the client does not qualify; the entire Public Benefits section
disappears in that case.
For each pre-retirement client or spouse who has a positive projected SS
benefit but is not yet claiming, a Future Retirement Income entry is
emitted with the projected annual_amount, the starts_at_age (the
claiming age), the implied starts_in_years and starts_in_year, and a
benefit_type of either own_worker or spousal. SSDI cases
(is_disabled_for_ss_purposes: true) are excluded because their claiming
age equals current age — they appear in income[] as currently received
Social Security Income. The Social Security present value remains on
balance_sheet.assets[] under Social Security Net PV in either case;
the future_income entries are explanatory.
future_income[] is intentionally not summed into net_income —
deferred annuity and similar future streams have not started yet.
data_quality_warningsPresent only when the API detected non-fatal issues worth surfacing (e.g.,
two employments with the same employer_name + employee_id across client
and spouse — the duplicate is deduplicated server-side, but the user should
know). When no issues are found, this field is absent (not {}).
{
"employment_business": {
"warnings": [
"Duplicate employment detected across client and spouse and deduplicated."
],
"duplicate_businesses": false,
"duplicate_employments": true
}
}
payment_warningPresent only in the rare successful-computation / failed-capture case for pay-per-call:
"Payment capture failed after two attempts. You have NOT been charged for this request; your card authorization has been cancelled. Your allocation results are included in full. If you wish to be billed for these results, please contact [email protected] and reference your request timestamp."
The full flow from a single, unmarried California client’s perspective.
Step 1 — (browser) create a PaymentMethod.
In the browser, using Stripe Elements and your publishable key, call
stripe.createPaymentMethod({ type: 'card', card, billing_details: {...} })
and keep the resulting paymentMethod.id (pm_...).
Step 2 — authorize the PaymentIntent.
Send the pm_... to /create-payment-intent:
PM="pm_1PExAmPlEtOkEn123"
curl -sS -X POST https://api.ria.us/create-payment-intent \
-H "Content-Type: application/json" \
-d "{\"payment_method_id\":\"${PM}\"}" \
| tee /tmp/pi_response.json | jq
Response:
{
"status": "success",
"payment_intent_id": "pi_3NXyZ1AbCdEfGhIj0KlMnOpQ",
"client_secret": "pi_3NXyZ1AbCdEfGhIj0KlMnOpQ_secret_AbCdEfGh",
"amount": 79.95
}
Step 3 — (browser) confirm the PaymentIntent.
Call stripe.confirmCardPayment(clientSecret) in the browser. If the card
triggers 3-D Secure, Stripe.js shows the challenge UI; otherwise, the
confirmation resolves immediately. Extract the payment_intent_id for the
next step.
Step 4 — build the household JSON.
Save this as henry.json:
{
"client": {
"personal_info": {
"name": { "first_name": "Henry", "last_name": "Mason" },
"address": {
"street_number": "4248",
"street_name": "La Salle Ave",
"city": "Culver City",
"state": "CA",
"zip_code": "90232"
},
"email_address": "[email protected]",
"dob": "1930-06-18",
"gender": "male",
"married": false,
"phone": { "home_phone": "310-839-0358", "mobile_phone": "310-776-2415" }
},
"financial_info": {
"max_premium": 150000,
"household_allocatable_wealth": 2131650.27,
"tax_filing_status": "single",
"quarters_of_coverage": 88,
"ssa_earnings_record": [
{ "year": 1974, "earnings": 2044 },
{ "year": 1975, "earnings": 1407 },
{ "year": 1980, "earnings": 1306 },
{ "year": 1981, "earnings": 1796 }
],
"actual_monthly_benefit": 365,
"actual_claiming_age": 65,
"optimize_claiming": false,
"retirement": {
"defined_contribution_plans": {
"accounts_401k": [
{
"account_id": "401K-778899",
"account_type": "traditional",
"employer": "County of Podunk",
"custodian": "Podunk Insurance Company",
"balance": 73231.29
}
]
},
"defined_benefit_plans": {
"pensions": [
{
"pension_id": "RETPEN",
"plan_type": "defined_benefit",
"type": "government",
"employer": "County of Coconut",
"administrator": "Coconut Administrator",
"status": "retired",
"annual_amount": 126600.48
},
{
"pension_id": "MAN",
"plan_type": "defined_benefit",
"type": "government",
"employer": "U.S. Armed Forces",
"administrator": "Uncle Sam",
"status": "retired",
"annual_amount": 24998.40
},
{
"pension_id": "Chain Gang",
"plan_type": "defined_benefit",
"type": "government",
"employer": "State of Happiness",
"administrator": "Happy Administration",
"status": "retired",
"annual_amount": 22140.72
}
]
}
},
"real_estate": [
{
"address": "4248 La Salle Ave",
"city": "Culver City",
"state": "CA",
"zip": "90232",
"type": "Single Family",
"bedrooms": 5,
"bathrooms": 5,
"sqft": 2627,
"mortgage_balance": 100000,
"purchase_price": 60000,
"purchase_year": 1974,
"purchase_month": 3,
"monthly_payment": 850,
"ownership": "individual",
"owner_occupied": true
}
],
"vehicles": [
{
"vin": "1N4AL3AP4EC144655",
"mileage": 70978,
"purchase_price": 17000,
"purchase_year": 2015,
"purchase_month": 3,
"amount_owed": 4500,
"monthly_payment": 350,
"ownership": "individual"
}
]
}
}
}
Step 5 — call /allocator:
PI="pi_3NXyZ1AbCdEfGhIj0KlMnOpQ"
curl -sS -X POST https://api.ria.us/allocator \
-H "Content-Type: application/json" \
-H "X-Payment-Intent: ${PI}" \
-d @henry.json \
| tee henry_response.json \
| jq '{ gamma: .household_gamma,
alloc: .household.optimal_allocation,
total_wealth: .household.total_wealth }'
Condensed response:
{
"gamma": 5.93,
"alloc": {
"empirical_global_market_portfolio": 1,
"risk_free_asset": 0
},
"total_wealth": 11828578.74
}
The card is captured ($79.95) automatically on the 200 response.
export ALLOCATOR℠_TOKEN="asys_sk_live_your_token_here"
curl -sS -X POST https://api.ria.us/allocator \
-H "Content-Type: application/json" \
-H "X-User-Token: ${ALLOCATOR℠_TOKEN}" \
-d @smith_household.json \
| jq '{
gamma: .household_gamma,
alloc: .household.optimal_allocation,
metrics: .household.optimal_portfolio,
rfa: .optimal_risk_free_allocation.household,
accounts: .retirement_accounts_by_type,
net_worth: .balance_sheet.net_worth,
net_income: .income_statement.net_income
}'
A typical output looks like:
{
"gamma": 5.93,
"alloc": {
"empirical_global_market_portfolio": 1,
"risk_free_asset": 0
},
"metrics": {
"allocatable": {
"before_tax": {
"expected_return": 0.213,
"std_dev": 0.1182,
"sharpe_ratio": 1.4975
},
"after_tax": {
"expected_return": 0.189,
"std_dev": 0.1113,
"sharpe_ratio": 1.4987
}
},
"total": {
"before_tax": {
"expected_return": 0.1012,
"std_dev": 0.0695,
"sharpe_ratio": 0.939
},
"after_tax": {
"expected_return": 0.0624,
"std_dev": 0.0688,
"sharpe_ratio": 0.5864
}
}
},
"rfa": {
"name": "Risk-Free Asset",
"household_allocation": 0,
"amount_to_invest": 0,
"description": "Optimal household allocation to risk-free asset"
},
"accounts": {
"traditional_ira": 187500,
"roth_ira": 134000,
"sep_ira": 40000,
"simple_ira": 22000,
"traditional_401k": 284365.15,
"roth_401k": 28000,
"solo_401k": 12000
},
"net_worth": 11724078.74,
"net_income": 592352.82
}
(To inspect the portfolio components themselves, run
jq '.empirical_global_market_portfolio_contents' smith_response.json
against the full response — those fields are intentionally left out of the
summary block above.)
A full married-couple request payload (with two earners, two businesses, all
retirement-account types, a deferred joint-life annuity with COLA, life
insurance, real estate, a vehicle, and a bank CD) is inlined in the
married_couple example in openapi.yaml — import that file into Postman
to get a ready-to-send request.
#!/usr/bin/env python3
"""
Minimal ALLOCATOR℠ client. Reads a household payload from household.json,
calls /allocator with an X-User-Token, prints the key summary figures.
"""
import json
import os
import sys
import requests
BASE = "https://api.ria.us"
TOKEN = os.environ["ALLOCATOR℠_TOKEN"] # export ALLOCATOR℠_TOKEN=...
def allocator(payload: dict) -> dict:
r = requests.post(
f"{BASE}/allocator",
headers={
"Content-Type": "application/json",
"X-User-Token": TOKEN,
},
json=payload,
timeout=150, # SLA p99 is 120s; give a small cushion
)
if r.status_code >= 400:
# Structured error envelope is documented; surface it cleanly
try:
err = r.json()
except ValueError:
r.raise_for_status()
raise RuntimeError(
f"HTTP {r.status_code}: {err.get('error')}: {err.get('message')}\n"
+ json.dumps(err.get("details", []), indent=2)
)
return r.json()
def main(path: str) -> None:
with open(path) as f:
payload = json.load(f)
resp = allocator(payload)
alloc = resp["household"]["optimal_allocation"]
print(f"γ (risk aversion) : {resp['household_gamma']:.3f}")
print(f"Total wealth : ${resp['household']['total_wealth']:,.2f}")
print(f"Allocatable wealth : ${resp['household']['total_allocatable_wealth']:,.2f}")
print(f"Market / risk-free split : {alloc['empirical_global_market_portfolio']:.2%} / {alloc['risk_free_asset']:.2%}")
print(f"Allocatable Sharpe (after-tax) : {resp['household']['optimal_portfolio']['allocatable']['after_tax']['sharpe_ratio']:.4f}")
print(f"Allocatable Sharpe (before-tax) : {resp['household']['optimal_portfolio']['allocatable']['before_tax']['sharpe_ratio']:.4f}")
print(f"Risk-free rate (before / after tax) : {resp['risk_free_rate']['before_tax']:.4f} / {resp['risk_free_rate']['after_tax']:.4f}")
print("\nTop 3 EGMP-14 holdings:")
for comp in resp["empirical_global_market_portfolio_contents"][:3]:
print(f" {comp['ticker']:<5s} ({comp['proxy_for']:<25s}) "
f"${comp['household_amount_to_invest']:>12,.2f} ({comp['weight']:.2%})")
warnings = resp.get("data_quality_warnings")
if warnings:
print("\nData-quality warnings present — review before acting:")
print(json.dumps(warnings, indent=2))
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: allocator.py household.json", file=sys.stderr)
sys.exit(2)
main(sys.argv[1])
Save as allocator.py, then:
export ALLOCATOR℠_TOKEN="asys_sk_live_your_token_here"
python3 allocator.py household.json
ALLOCATOR℠_TOKEN
to your token value./allocator request, select the Auth tab and either:
X-User-Token header to {{ALLOCATOR℠_TOKEN}}, orX-Payment-Intent header to your freshly authorized pi_....married_couple example body from the request Examples dropdown.X-User-Token into the “Authorize” prompt at the top. (The UI
does not know about X-Payment-Intent; pay-per-call must go through the
/create-payment-intent flow described above.)/allocator, click Try it out, paste a valid request body
(or keep the pre-filled example), click Execute.From User Agreement §5.1: “User shall pay the per-API-Use fees set forth in Schedule A to this Agreement.” The current Schedule A is summarized below.
From User Agreement §1.3: each individual request made to the API (whether via programmatic API call or web interface submission) that results in a response, whether successful or unsuccessful due to data errors (excluding system errors caused by Company infrastructure).
In practice:
/allocator → billable./allocator — validation failures, California-residency
failures, asset-validation failures — are due to User Data problems and
are NOT billed. (In the pay-per-call flow, the Stripe authorization is
cancelled immediately on any 4xx.)/allocator → never billed. The authorization is cancelled;
the call is on us./allocator (rate-limit) → never billed./health, /openapi.json, /docs, /spec, /create-payment-intent
→ never billed.From User Agreement §5.2(a) and Schedule A:
Each successful /allocator call is recorded as a Stripe metered-usage
event against your Stripe customer record. The per-call price is determined
by your cumulative successful-call count for the calendar month:
| Successful API Uses in calendar month | Fee per API Use in that tier |
|---|---|
| 1 through 25 | $34.95 |
| 26 through 100 | $27.95 |
| 101 and above | $21.95 |
Worked example. Sixty (60) successful API Uses in a calendar month: (25 × $34.95) + (35 × $27.95) = $873.75 + $978.25 = $1,852.00 for the month, exclusive of taxes. Tier boundaries do not roll over to the next calendar month.
Per-token rate limit: 20 requests per rolling 60-minute window. Requests in excess of this limit receive a 429 response and are not billed.
From User Agreement §5.2(b) and Schedule A:
Per User Agreement §5.5 — pricing changes (including modifications to Schedule A) require 60 days’ written notice. Continued use after the effective date constitutes acceptance.
From User Agreement §10: Advisory API Systems receives no revenue-sharing, distribution fees (12b-1), placement fees, or any other compensation from any ETF sponsor or fund family. The sole source of revenue is the per-use fees set forth in Schedule A.
The authoritative document is the Service Level Agreement. Key commitments:
99.5% Monthly Uptime target. Service Credits on shortfall, capped at 100% of the month’s fees:
| Monthly Uptime | Service Credit |
|---|---|
| 99.0% – 99.49% | 10% |
| 95.0% – 98.99% | 25% |
| 90.0% – 94.99% | 50% |
| Below 90.0% | 100% |
Service Credits must be requested via [email protected] within 30 days of the affected month, include the API Key identifier (last 8 chars only), the affected Pacific Time windows, and the impact.
| Metric | Target | Measurement |
|---|---|---|
| Median | < 40 seconds | 50th percentile |
| p95 | < 100 seconds | 95th percentile |
| p99 | < 120 seconds | 99th percentile |
Complex households (extensive background assets, large SSA earnings records) tend toward the upper end of this distribution. Set client-side timeouts to at least 150 seconds to leave headroom.
Less than 1% HTTP 5xx attributable to Company infrastructure.
| Channel | Availability | Response target |
|---|---|---|
| Email ([email protected]) | 24/7 submission | Next business day |
| Documentation / OpenAPI | 24/7 self-service | — |
Issue classification and targets:
| Priority | Description | Initial Response | Resolution Target |
|---|---|---|---|
| P1 Critical | Complete outage / data corruption for all customers | 1 hour | 4 hours |
| P2 High | Major functionality impaired; no workaround | 4 hours | 1 business day |
| P3 Medium | Functionality impaired; workaround available | 1 business day | 5 business days |
| P4 Low | Minor issues, questions, feature requests | 2 business days | Best effort |
Business hours are Monday–Friday, 09:00–17:00 Pacific, excluding US federal holidays.
When reporting issues, include the API Key identifier (last 8 chars only — never the full token), request / response examples, timestamps in Pacific Time, and reproduction steps.
Summarized from the Security Practices Documentation; the full text is authoritative.
-k to curl in production, and do
not disable certificate verification in any HTTP client.X-User-Token values in an encrypted secrets manager, OS
keychain, or an equivalent protected store. Not in source code, env files
committed to git, email, chat transcripts, or public repositories.max_premium: 50000 is being rejected.”max_premium is strictly bounded: 50000 < max_premium < 300000.
Exactly 50000 or exactly 300000 is rejected. Use a value in between.
client — validation says spouse is required.”When client.personal_info.married == true, a spouse block with
personal_info and financial_info (including that spouse’s own
max_premium, tax_filing_status, and quarters_of_coverage) is
required. The spouse’s ssa_earnings_record is optional (same rules
as the client side).
community or joint ownership — is that allowed?”No. When married == false, every ownership must be individual. The
validator will reject the request with
rule: marital_status_consistency.
Multi-Family is 2–4 units. A 5-unit building is Apartment. Fix the
type field.
Set every property’s owner_occupied = false and set the top-level
rents_residence = true on the person whose residence is rented. Include
the annual rent in household_total_annual_expenses.
income_statement.income?”By design. Deferred annuities go into income_statement.future_income[]
with starts_in_years. They are not summed into current income[] or
net_income. Immediate annuities (deferral_period: 0 or absent)
do show up in current income.
solo_401k but I only have W-2 employment.”solo requires business income. If your household has no entry in
client.financial_info.businesses[] (and no spouse-side business either),
don’t mark a 401(k) as solo.
The strategy recommendations flag mandatory-Roth catch-up for prior-year wages > $150,000 (SECURE 2.0 Act §603), but the API treats account classification exactly as you submit it. Classify the catch-up contributions into a Roth 401(k) entry if that’s where they land.
No. The Social Security Fairness Act (signed January 5, 2025) eliminated both the Windfall Elimination Provision and the Government Pension Offset. Supply your SSA-covered earnings as they appear on your SSA statement — the statement already excludes non-covered earnings, and there is no WEP or GPO adjustment in the API any more.
purchase_price?”The original purchase price. That is your cost basis. Current market value is determined by the API from the address, property type, and purchase-price / time combination.
monthly_payment?”No. monthly_payment is principal + interest only. Taxes, homeowners
insurance, HOA, utilities, maintenance, and reserves are all estimated
automatically from property type and current market value. Adding them
would double-count.
social_security_number is optional and never used in the optimization.
The firm retains it only when supplied, for client-file completeness.
If you don’t need it on file, omit it.
Not via a shape flag. Extract only the fields you care about client-side
(as in the jq and Python examples above). The response shape is
documented in openapi.yaml so you can write a strongly-typed
deserializer that only binds the fields you need.
/allocator call hung for 90 seconds before returning.”That’s within normal SLA targets for a complex household. Set HTTP client timeouts to at least 150 seconds.
pi_... from a previous successful call?”No. Each X-Payment-Intent is single-use. The second attempt returns
402 Payment Required with
"X-Payment-Intent is invalid, expired, or already captured.". Create
a fresh PaymentIntent via /create-payment-intent for each call.
Make sequential calls, one per household. Each call is a full household optimization — there is no batched endpoint.
/docs.Yes. Every request the web form submits goes to POST /allocator with the
same JSON body shape documented here. Browser devtools → Network tab →
right-click the request → Copy → Copy as cURL will give you a working
shell command (subject to your auth being in range).
Yes. The web form’s JSON tab has two buttons:
POST /allocator accepts) as
allocator-snapshot-YYYYMMDD-HHMMSS.json to your local machine.The downloaded file is a valid /allocator request body. You can:
Re-open the web form weeks or months later, copy the file’s contents to your clipboard, click Load Snapshot from Clipboard, update only the fields that have changed, and re-run ALLOCATOR℠ without re-entering everything from scratch.
Use the file directly with curl, skipping the web form entirely:
curl -sS https://api.ria.us/allocator \
-H "Content-Type: application/json" \
-H "X-User-Token: $TOKEN" \
-d @allocator-snapshot-20260315-104522.json | jq
Build a payload once in the web form, then drive subsequent runs from a script or scheduled job using the saved file as input.
For RIA Clients running optimizations across many households, keeping a per-client snapshot file alongside your other client records makes annual reviews substantially faster — at the next review you only need to update the fields that have changed (asset values, ages, recent earnings, etc.) rather than re-entering Social Security earnings histories, real estate details, and pension parameters from scratch.
When contacting support with an incident, include:
error field)Document version: aligned with ALLOCATOR℠ API v3.2.0 and User Agreement v2.3
(April 2026). In case of conflict between this guide and either the OpenAPI
specification at /openapi.json or the User Agreement, the authoritative
document controls.