---
title: "Run a coordination strategy (Engine B)"
method: POST
path: "/api/v1/households/{id}/decisions"
tags: ["decisions"]
---

# Run a coordination strategy (Engine B)

`POST /api/v1/households/{id}/decisions`

## Request

- Base URL: `https://worthune.com`
- URL: `https://worthune.com/api/v1/households/{id}/decisions`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-Api-Key`

Runs one of withdrawal-sequencing, roth-ladder, ss-claiming, asset-location, tax-loss-harvesting, annual-gifting, pension-election, bracket-fill, charitable-giving on the stored household, stores the decision append-only, stamps the meter, and emits household.decision.

## Request body

- DecisionRequest: Assumptions resolve like /project: explicit `assumptions`, a pinned `profile`, or the labeled illustrative default (assumptions take precedence when both are sent); the source is always echoed.
  - `strategy` 'withdrawal-sequencing' | 'roth-ladder' | 'ss-claiming' | 'asset-location' | 'tax-loss-harvesting' | 'annual-gifting' | 'pension-election' | 'bracket-fill' | 'charitable-giving', required
  - `horizon` ProjectionHorizon, required
    - `startYear` integer, required: First projected calendar year, an integer in [1900, 2150] on every surface.
    - `years` integer, required: Number of years to project.
  - `assumptions` ProjectionAssumptions: Unknown keys are rejected — the accepted set is exactly: annualReturn, inflationRate, defaultIncomeGrowth, socialSecurityCola, returnsByWrapper.
    - `annualReturn` number, required: Nominal annual return applied to every account balance (one blended rate).
    - `inflationRate` number, required: Annual inflation applied to inflation-adjusted expense streams.
    - `defaultIncomeGrowth` number, required: Growth for income streams that carry no growthRate of their own.
    - `socialSecurityCola` number: COLA applied to COMPUTED social_security benefits from each benefit's start year. Defaults to inflationRate; pass 0 for a flat benefit.
    - `returnsByWrapper` object: Per-wrapper returns; unlisted wrappers keep the blended annualReturn.
      - `taxable` number: Return for taxable accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `traditional` number: Return for traditional accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `roth` number: Return for roth accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `hsa` number: Return for hsa accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `plan529` number: Return for plan529 accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `cash` number: Return for cash accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `annuity` number: Return for annuity accounts, replacing annualReturn (and any sampled path) for that wrapper.
  - `profile` ProfileRef: An assumption profile from GET /api/v1/assumption-profiles, or the built-in illustrative-6-3-3@1.0.0.
    - `id` string, required
    - `version` string, required: Exact version; a pinned run never silently gets a newer one.
  - `params` union: Strategy-specific parameters: withdrawal-sequencing → none; roth-ladder → RothLadderParams; ss-claiming → SsClaimingParams; asset-location → AssetLocationParams; tax-loss-harvesting → TaxLossHarvestingParams; annual-gifting → AnnualGiftingParams; pension-election → PensionElectionParams; bracket-fill → BracketFillParams; charitable-giving → CharitableGivingParams.
    - WithdrawalSequencingParams: withdrawal-sequencing takes no params (omit `params` or send {}).
    - RothLadderParams: roth-ladder
      - `candidates` object[], required: Distinct schedules — the caller's to propose.
        - `annualAmountUsd` number, required
        - `years` integer, required: Ladder length, starting at horizon.startYear.
    - SsClaimingParams: ss-claiming
      - `candidateAges` object: Member id → distinct claiming ages to compare. Default per claimer: [62, FRA, 70]. Every key must be a claiming member (a social_security stream with a computed-benefit block).
    - AssetLocationParams: asset-location: taxRates and either characteristics or illustrativeCharacteristics: true.
      - `taxRates` object, required: Fractions — the advisor knows these.
        - `marginalOrdinary` number, required
        - `qualifiedAndLtcg` number, required
      - `characteristics` object: Per asset class held by the household's accounts. Required unless illustrativeCharacteristics is true.
        - `us_equity` ClassCharacteristics
          - `ordinaryYield` number, required: Annual yield taxed as ordinary income.
          - `qualifiedYield` number, required: Annual yield taxed at qualified-dividend/LTCG rates.
          - `expectedGrowth` number, required: Expected annual price growth.
        - `intl_equity` ClassCharacteristics
          - `ordinaryYield` number, required: Annual yield taxed as ordinary income.
          - `qualifiedYield` number, required: Annual yield taxed at qualified-dividend/LTCG rates.
          - `expectedGrowth` number, required: Expected annual price growth.
        - `bonds` ClassCharacteristics
          - `ordinaryYield` number, required: Annual yield taxed as ordinary income.
          - `qualifiedYield` number, required: Annual yield taxed at qualified-dividend/LTCG rates.
          - `expectedGrowth` number, required: Expected annual price growth.
        - `cash` ClassCharacteristics
          - `ordinaryYield` number, required: Annual yield taxed as ordinary income.
          - `qualifiedYield` number, required: Annual yield taxed at qualified-dividend/LTCG rates.
          - `expectedGrowth` number, required: Expected annual price growth.
        - `real_assets` ClassCharacteristics
          - `ordinaryYield` number, required: Annual yield taxed as ordinary income.
          - `qualifiedYield` number, required: Annual yield taxed at qualified-dividend/LTCG rates.
          - `expectedGrowth` number, required: Expected annual price growth.
        - `other` ClassCharacteristics
          - `ordinaryYield` number, required: Annual yield taxed as ordinary income.
          - `qualifiedYield` number, required: Annual yield taxed at qualified-dividend/LTCG rates.
          - `expectedGrowth` number, required: Expected annual price growth.
      - `illustrativeCharacteristics` boolean: Explicit opt-in to the labeled illustrative class characteristics (not a recommendation). An omitted market view is an error, never a default.
      - `lockedAccountIds` string[]
    - TaxLossHarvestingParams: tax-loss-harvesting
      - `ordinaryMarginalRatePct` number, required: Percent (e.g. 32).
      - `longTermRatePct` number: Percent; defaults to 15.
      - `realizedGains` object
        - `shortTerm` number
        - `longTerm` number
    - AnnualGiftingParams: annual-gifting
      - `doneeCount` integer, required
      - `years` integer, required
    - PensionElectionParams: pension-election
      - `ownerId` string, required: Member id of the participant — an adult (primary or spouse) with `sex`.
      - `startAge` integer, required: Whole-year age at which payments (or the lump sum) commence.
      - `discountRatePct` number, required: Annual discount rate, percent — the caller's view, never defaulted.
      - `options` union[], required: The plan's quoted options; must be distinguishable.
        - union
          - object
            - `kind` 'single-life', required
            - `monthlyAmount` number, required
          - object
            - `kind` 'joint-survivor', required
            - `monthlyAmount` number, required
            - `survivorPct` number, required: e.g. 0.5 for a 50% survivor annuity; needs a spouse with `sex`.
          - object
            - `kind` 'lump-sum', required
            - `amount` number, required
    - BracketFillParams: bracket-fill: each year converts the room between that year's projected taxable income and the bracket's top (model bracket-fill).
      - `fillToRatesPct` number[]: Brackets to fill, percent. Default [12, 22, 24].
      - `years` integer: Years to convert from horizon.startYear. Default 10.
    - CharitableGivingParams: charitable-giving: every year vs bunched vs QCD routes over one cycle (model charitable-bunching).
      - `annualGivingUsd` number, required: Cash the household gives to public charities each year.
      - `otherItemizedDeductionsUsd` number: Other itemized deductions each year. Default 0.
      - `bunchYears` integer: Bunching cycle. Default 2.
      - `iraDistributionsUsd` number: IRA distributions a QCD can replace. Default: the first projected year's RMD.

## Response `200`

The decision object, its evidence record, and the stored decision id.

- object
  - `ok` unknown, required
  - `decisionId` integer, required
  - `householdId` integer, required
  - `householdVersion` integer, required
  - `strategy` 'withdrawal-sequencing' | 'roth-ladder' | 'ss-claiming' | 'asset-location' | 'tax-loss-harvesting' | 'annual-gifting' | 'pension-election' | 'bracket-fill' | 'charitable-giving', required
  - `assumptionsSource` string, required
  - `decision` DecisionObject, required: Decision object v0.1.0 (docs/decision-object-spec.md).
    - `schemaVersion` '0.1.0', required
    - `strategy` string, required
    - `strategySpecVersion` string, required
    - `objective` string, required: What the scores maximize, in one sentence.
    - `baseline` object, required
      - `description` string, required
      - `recordSha256` string
    - `alternatives` DecisionAlternative[], required
      - `id` string, required
      - `label` string, required
      - `score` number, required: Objective score; alternatives are sorted by it, best first (validated).
      - `delta` OutcomeDelta, required
        - `endingNetWorthDelta` number, required: Variant − baseline, final-year net worth.
        - `totalTaxDelta` number, required: Variant − baseline, cumulative federal + state tax across the horizon.
        - `ranOutInYear` object, required
          - `baseline` integer, nullable, required
          - `variant` integer, nullable, required
      - `intents` ExecutionIntent[], required: Empty = status quo.
        - `id` string, required
        - `kind` 'transfer' | 'convert' | 'withdraw' | 'contribute' | 'claim' | 'set-policy', required
        - `summary` string, required: One human sentence; policy-level, never a trade.
        - `accountId` string: Household-schema account id, when the intent targets one.
        - `year` integer
        - `amountUsd` number
      - `notes` string[]
    - `assumptions` object, required
      - `source` string, required
      - `profile` ProfileRef: An assumption profile from GET /api/v1/assumption-profiles, or the built-in illustrative-6-3-3@1.0.0.
        - `id` string, required
        - `version` string, required: Exact version; a pinned run never silently gets a newer one.
    - `evidence` EvidenceLink[], required
      - `kind` 'projection' | 'model-run' | 'decision', required
      - `sha256` string, required
      - `specVersion` string, required
  - `record` DecisionRecord, required: Evidence record v2. Verify with POST /api/v1/records/verify or the SDKs' verifyRecord.
    - `sha256` string, required: SHA-256 (hex) over the canonical JSON of {model, specVersion, inputs, outputs} — keys sorted recursively, no whitespace.
    - `fields` unknown, required
    - `howToVerify` string, required
    - `model` string, required: The hashed model identity — a catalog model name, "household-projection", or "decision-<strategy>".
    - `specVersion` string, required
    - `inputs` object: The exact hashed inputs, present on records whose envelope does not otherwise expose them (household projections, decisions). Point-model run responses echo `inputs` at top level instead; a verify response's record covers the inputs the caller sent in the request.
    - `signature` RecordSignature: Proves Worthune minted the record's hash. Absent when the deployment has no signing key; a stored decision is signed only when its hash is recomputed from the stored inputs.
      - `alg` 'Ed25519', required
      - `keyId` string, required: Matches a key at GET /api/v1/records/keys.
      - `value` string, required: base64url signature.
      - `message` string, required: What was signed: "worthune-record:v1:" + sha256. Rebuild it from the hash when verifying.

## Other responses

- `400`: Malformed JSON, missing strategy/horizon, unknown strategy (path "strategy"), or unresolvable assumptions.
- `401`: No key presented (Authorization: Bearer wk_… or X-Api-Key).
- `402`: Unrecognized or revoked API key.
- `403`: The key's role is below the operation's floor (viewer < developer < owner), the key predates organization support, or the plan lacks the feature. On a sandbox key, also: the cap of 5 active households on create and import, the cap of 1 active Lodestar mandate, and the white-label surfaces (tenant config, embed tokens) and narration or mapping drafts on Worthune's model (they run on your own: a model assigned at /api/v1/model-policy, or x-model-key), which the sandbox does not carry.
- `404`: No household with this id in the key's organization (PUT, PATCH, POST project and POST decisions also answer 404 for archived rows; GET, the decision list and the picture still serve them).
- `422`: Strategy validation failed — missing or out-of-domain params, or a household the strategy cannot run on (errors carry params.… paths).
- `429`: Hourly per-org stateful quota, a per-route durable limit, or the 120 requests/minute per-IP burst backstop.
- `503`: Billing is not configured on this deployment; household resources are disabled rather than served without tenancy (production fail-closed).

---

[API](https://skmtc.dev/worthune/apis/worthune-api.md) · [All operations](https://skmtc.dev/worthune/apis/worthune-api/llms.txt) · [OpenAPI document](https://skmtc.dev/worthune/apis/worthune-api/revisions/9f2241072555?raw)
