Optimization
v2 (XGM) deployments don't just predict — they prescribe. Given a row, a budget, and a cost per change, the optimizer searches the model's own contribution curves for the feature changes that best move the outcome. The solution is exact (a global optimum over the candidate grid, not a heuristic) and personalized: because contribution curves are conditioned on each row's immutable values, two similar rows can receive genuinely different prescriptions.
Four endpoints cover the common shapes of the question:
| Endpoint | Question it answers |
|---|---|
/optimize | "Best changes for this row within a budget?" |
/optimize/batch | "Best changes for each of these rows?" |
/counterfactual | "Cheapest way to get this row to a target outcome?" |
/portfolio | "How do I split one shared budget across these rows?" |
All four are v2-only — calling them on a v1 deployment returns
400 Bad Request. Rows are sent in the model's fitted feature space
(original units); deployment preprocessing is not applied.
See it work: pick a customer, drag the budget, and watch the solver's cost-vs-outcome frontier. Note how the same budget buys a very different outcome per customer — that's the personalization:
/v1/optimize, cost structure: discount $500/unit, AOV $2/unit). Compare customers — the same budget buys a very different outcome for each row. That is personalization from interaction terms, not a global rule.The result envelope
Solver results always come back as HTTP 200 with a status envelope —
branch on status, not on the HTTP code:
{ "status": "success", "model_name": "model", "results": { ... } }
{
"status": "error",
"error": {
"code": "invalid_parameter",
"message": "Joint candidate grid too large for coupled group ...",
"context": {
"remedies": [
"lock one of the coupled features",
"lower n_grid",
"raise max_joint_candidates"
]
}
}
}
error.code is stable and machine-readable; error.context carries
structured detail, including remedies where applicable.
Common parameters
These are accepted by all four endpoints unless noted:
256200Feasibility rules configured on the model (e.g. "these two values can never co-occur") are hard constraints — they backfill from the model's persisted optimization config and cannot be overridden per request.
/optimize
Budget-constrained optimization for a single row.
Request
Plus any common parameters.
{
"row": { "tenure_days": 420, "avg_order_value": 86.5, "discount_applied": 0.0, "plan": "premium" },
"budget": 150,
"mutable_features": ["discount_applied", "avg_order_value"],
"cost_structure": { "discount_applied": 500, "avg_order_value": 2 }
}
Response (results)
| Field | Description |
|---|---|
optimal_features | The full optimized row, original units |
prediction | Model outcome at the optimized row (response scale) |
total_cost | True cost of the proposed changes |
frontier | The whole cost-vs-outcome frontier from 0 to budget, as [cost, prediction] pairs |
frontier_solutions | Full feature dicts aligned index-wise with frontier |
global_optimum | true — the solution is exact, not heuristic |
infeasible_baseline | true when the input row already violates a feasibility rule |
The frontier is produced for free by the solver — one call gives you the
optimal action at every budget level up to the one you set, which is useful
for "how much budget is actually worth spending?" analyses.
/optimize/batch
Optimize many rows in one call — the model is built once and every row is solved against it.
Request
'optimum'0.0Plus any common parameters.
{
"rows": [
{ "tenure_days": 420, "avg_order_value": 86.5, "discount_applied": 0.0 },
{ "tenure_days": 31, "avg_order_value": 22.0, "discount_applied": 0.1 }
],
"objective": "budget",
"budget": 100,
"direction": "maximize",
"mutable_features": ["discount_applied", "avg_order_value"]
}
Response
results is a list of per-row result dicts (aligned with rows), each
carrying the optimized features, outcome, and cost for that row —
objective-dependent fields (e.g. the frontier under pareto) included.
direction and objective are orthogonal: objective selects the search
strategy, direction sets the optimization sense (whether "better" means a
higher or lower prediction).
/counterfactual
The minimal-cost set of changes that gets a row to a desired outcome.
Request
Plus any common parameters.
Response
| Field | Description |
|---|---|
found | true if the target is reachable; false returns the best achievable point instead |
desired_outcome | Echo of the requested target |
original_prediction | Outcome at the unmodified row |
prediction | Outcome at the counterfactual row |
total_cost | Cost of the changes |
counterfactual_features | The full modified row |
feature_changes | Just the features that changed, with before/after values |
/portfolio
Allocate one shared budget across a set of rows, exactly and globally
optimally. Unlike /optimize/batch with objective='budget' — where the
budget applies to each row — here it is a single pool: the solver decides
which rows are worth spending on at all.
Request
Plus any common parameters.
Response (portfolio)
| Field | Description |
|---|---|
allocations | Per-row allocation results, aligned to rows |
total_cost | Total spend across all rows |
total_improvement | Total outcome improvement delivered |
total_weighted_improvement | Value-weighted improvement (when value given) |
n_funded | How many rows received any spend |
objective | Which objective was solved |
Batch optimization from the platform
You can also run batch optimizations against hosted datasets — without managing deploy keys or payloads — via the SDK's optimiser surface. It creates a reusable named policy and proxies to the deployed model's runtime:
result = client.workflow.optimise_model(
model_id=model_id,
objective="pareto",
dataset_id=dataset_id,
constraints={
"immutable": ["tenure_days"],
"bounds": {"discount_applied": [0, 0.3]},
},
direction="maximize", # optional; omit to use the model's default
)
Saved optimiser policies accept the same keys as /optimize/batch
(objective, direction, budget, target, cost_weight,
mutable_features, per_row_immutable, feature_bounds, cost_structure,
max_joint_candidates, n_grid, cost_resolution), and per-run params
override the saved policy. Results are stored on the run and retrievable via
client.optimisers.get_optimiser_run().