Documentation
Getting Started
Welcome to the Last Price platform documentation. This guide explains pricing, integrations, and workspace workflows. Use the API reference for the current endpoint contracts.
Base URL & Authentication
Use this base URL for API requests. Secured operations accept the credentials listed in the API reference, commonly a JWT bearer token or an API key passed via the x-api-key header.
# Base URL
https://api.last-price.ai
# Example authenticated request
curl https://api.last-price.ai/api/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"Ways to Access the API
Building an agent? Read /llms.txt first. It is the current word on getting a key with no human, keyless calls, pay per call, limits and error fields, and this guide does not repeat it. Machine-readable versions: the OpenAPI contract, the agent manifest, the MCP manifest and the changelog feed.
Choose one way to start. These options and limits come from this deployment's current discovery document.
Open the API reference for request bodies and response schemasLoading current access options...
Platform Overview
Last Price is a pricing optimization and A/B testing platform that helps businesses find the price that maximizes revenue.
| Component | What It Does |
|---|---|
| Elo | A/B testing engine - deterministic variant assignment via SHA-256 hashing |
| Jale | AI pricing optimizer - elasticity analysis, revenue simulation, recommendations |
| PCN | Pricing Compute Network - function registry, inference routing, metering |
| Price Router | Multi-vendor pricing facade - one API in front of all built-in and custom pricing models |
| Rosetta | Schema translation - map any price data format to canonical schemas with AI-assisted adapters |
| Commerce Control Plane | End-to-end pipeline - connectors, observations, policies, approvals, execution |
| Oja | Admin dashboard - manage experiments, functions, usage, marketplace |
| Snippet | Embeddable JavaScript widget - zero-dependency, drop into any website |
| API | REST API - full programmatic control over every feature |
| Marketplace | Publish and consume community pricing functions with revenue sharing |
| Scraper | Competitive pricing data collection from any website |
Quick Start
- Set Up Your Workspace
For this experiment workflow, sign up and create an API key in Settings, API keys. If you started with a sandbox, use its claim link to move into a full workspace first. Discover your tenant_id with GET /api/me using that key, then use it in the experiment URL below. Keep the key on your server and send it in x-api-key. If using OAuth, send the access token in Authorization: Bearer instead.
Create a workspace - Create Your First Experiment
curl -X POST https://api.last-price.ai/api/tenants/YOUR_TENANT_ID/experiments \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Homepage Pricing Test", "key": "homepage_price_test", "variants": [ { "name": "control", "price": 29.99, "weight": 50 }, { "name": "experiment", "price": 39.99, "weight": 50 } ] }' - Choose an Integration Method
Method Best For Effort Shopify, WordPress, any HTML site 5 minutes Next.js, React apps 15 minutes Any backend or frontend 30 minutes
Platform Integrations
Step-by-step integration guides for Shopify, SaaS apps, e-commerce platforms, marketplaces, and digital products.
Shopify Store Integration
Integrate Last Price to A/B test product prices, subscription tiers, and promotional offers in your Shopify store.
Option A: Embed Snippet (No Coding)
Go to Shopify Admin → Online Store → Themes → Edit Code → theme.liquid. Add before the closing </body> tag:
<!-- Last Price A/B Testing -->
<script src="https://last-price.ai/lastprice.js"></script>
<script>
LastPrice.configure({
apiBase: 'https://api.last-price.ai',
embedToken: 'lpe_YOUR_EMBED_TOKEN',
tenantId: 'YOUR_TENANT_ID',
experimentId: 'YOUR_EXPERIMENT_KEY',
debug: false
});
</script>Add a pricing container to your product template:
<div id="lastprice-pricing"></div>
<script>
LastPrice.showPricing('#lastprice-pricing', {
customRenderer: function(pricing) {
// Escaped because this string is assigned to innerHTML and the value
// comes from the variant's metadata, which can carry markup.
var price = String(pricing.price)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return '<div class="product-price">' +
'<span class="money">' + price + '</span></div>';
}
});
// Record the conversion from Shopify's own Add to Cart form.
// onConvert is not used here: the SDK attaches it to a button with id
// lp-convert-btn, which the default renderer emits and a custom one like
// the above does not. Give your rendered button that id if you would
// rather use onConvert.
document.addEventListener('submit', function (event) {
var action = event.target && event.target.action;
if (action && action.indexOf('/cart/add') !== -1) {
// Record what this visitor was shown. A variant's metadata can
// override the price, so leaving revenue out would store the
// variant's own figure and disagree with the page.
var shown = LastPrice.getVariant();
LastPrice.convert(shown && typeof shown.price === 'number'
? { revenue: shown.price }
: {});
}
});
</script>Option B: Headless / Custom Storefront
Note: @lastprice/client is a hypothetical SDK package name shown for illustration. Use the REST API directly.
This example runs on your server, so it sends your secret API key. Do not move these calls into the browser as they are: a page has no credential here, and the cross-origin preflight does not allow the header this uses. For a browser component, configure the snippet with an embed token instead and register the page origin on it.
import { cookies } from 'next/headers';
const API_BASE = 'https://api.last-price.ai';
const TENANT_ID = 'YOUR_TENANT_ID';
export default async function ProductPage({ product }) {
// A Server Component has no localStorage, so the participant id comes from
// a cookie. It has to already exist: minting one here would mint a new one
// on every render, and assignment is stable per participant, so the visitor
// would move between variants as they browsed. Set it once at the edge (see
// the middleware below) and read it everywhere else.
const userId = (await cookies()).get('lp_uid')?.value;
if (!userId) return null; // middleware sets it on the first request
// Get the A/B tested price via the pricing endpoint
// Server side, so the secret key is safe here. Never ship this to a browser.
const resp = await fetch(
`${API_BASE}/api/experiments/product_price_test/pricing?userId=${userId}&tenantId=${TENANT_ID}`,
{ headers: { 'x-api-key': process.env.LASTPRICE_API_KEY } }
);
const { pricing } = await resp.json();
return (
<div>
<h1>{product.title}</h1>
<p className="price">${pricing.price}</p>
<AddToCart userId={userId} revenue={pricing.price} />
</div>
);
}The participant id is minted once, at the edge, so every later render reads the same one and the visitor stays in one variant:
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
if (request.cookies.get('lp_uid')) return NextResponse.next();
const response = NextResponse.next();
response.cookies.set('lp_uid', crypto.randomUUID(), {
maxAge: 60 * 60 * 24 * 365,
sameSite: 'lax',
path: '/'
});
return response;
}The button is a client component, because a click handler cannot be passed out of a Server Component. It posts to a route on your own domain, which holds the key:
'use client';
// app/product/add-to-cart.jsx
export function AddToCart({ userId, revenue }) {
return (
<button
onClick={async () => {
// Your own route, which holds the secret key. Posting straight to the
// API from a page would carry no credential the API accepts.
await fetch('/api/record-conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, revenue })
});
}}
>
Add to Cart
</button>
);
}Shopify Use Cases
| Use Case | Experiment Setup |
|---|---|
| Product price testing | Variant A: $49.99 vs Variant B: $54.99 |
| Subscription tier pricing | Variant A: $9/mo vs Variant B: $12/mo |
| Bundle pricing | Variant A: $79 bundle vs Variant B: $89 bundle |
| Discount depth testing | Variant A: 10% off vs Variant B: 20% off |
| Free shipping threshold | Variant A: free at $50 vs Variant B: free at $75 |
| Psychological pricing | Variant A: $49.99 vs Variant B: $50.00 |
SaaS / Subscription Integration
Test pricing tiers, plans, and subscription models in React/Next.js apps.
Note: @lastprice/react is a hypothetical SDK package name shown for illustration. Use the REST API directly.
"use client";
import { useState, useEffect } from 'react';
export default function PricingPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const userId = localStorage.getItem('lp_uid') || crypto.randomUUID();
localStorage.setItem('lp_uid', userId);
fetch(`/api/experiments/saas_pricing_test/pricing?userId=${userId}&tenantId=YOUR_TENANT_ID`)
.then(r => r.json()).then(setData).finally(() => setLoading(false));
}, []);
if (loading || !data) return <PricingSkeleton />;
return (
<PricingCard
name={data.variant === 'control' ? 'Starter' : 'Growth'}
price={data.pricing.price}
features={data.pricing.features}
onSubscribe={async () => {
await fetch('/api/experiments/saas_pricing_test/convert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: localStorage.getItem('lp_uid'),
tenantId: 'YOUR_TENANT_ID',
revenue: data.pricing.price
})
});
window.location.href = `/api/checkout?plan=${data.variant}&price=${data.pricing.price}`;
}}
/>
);
}Use Jale for AI-powered price recommendations:
curl -X POST https://api.last-price.ai/api/compute/price \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: YOUR_TENANT_ID" \
-d '{
"function_name": "lastprice/jale-optimizer",
"input": {
"current_price": 29,
"product_type": "saas_subscription",
"market_data": {
"competitor_prices": [19, 25, 39, 49],
"current_conversion_rate": 0.03
}
}
}'E-commerce Platform Integration
Server-side price assignment for e-commerce platforms (recommended for SEO and consistency).
// pages/api/product-price.ts (Next.js API route)
// Uses the REST API directly
export default async function handler(req, res) {
const { userId, productId } = req.query;
const tenantId = process.env.TENANT_ID;
const resp = await fetch(
`${process.env.LASTPRICE_API_URL}/api/experiments/product_${productId}_price/pricing?userId=${userId}&tenantId=${tenantId}`
);
const data = await resp.json();
res.json({
productId,
price: data.pricing.price,
variant: data.variant,
experimentId: data.experimentId
});
}Track conversions after a successful payment:
// After successful payment - track conversion via REST API
async function onPaymentSuccess(order) {
await fetch(`${process.env.LASTPRICE_API_URL}/api/experiments/${order.experimentKey}/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: order.userId,
tenantId: process.env.TENANT_ID,
revenue: order.totalAmount
})
});
}Marketplace Integration
For platforms connecting buyers and sellers - test commission rates, platform fees, and pricing models.
// Test different commission rates using the REST API
const resp = await fetch(
`${API_BASE}/api/experiments/commission_rate_test/pricing?userId=${sellerId}&tenantId=${TENANT_ID}`
);
const { variant, pricing } = await resp.json();
const commissionRate = variant === 'control' ? 0.10 : 0.15;
const platformFee = transactionAmount * commissionRate;
// Track when a transaction completes
await fetch(`${API_BASE}/api/experiments/commission_rate_test/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: sellerId, tenantId: TENANT_ID, revenue: platformFee })
});Digital Products Integration
For courses, ebooks, templates, and downloadable content.
// Test course pricing ($199 control vs $299 premium positioning)
const resp = await fetch(
`${API_BASE}/api/experiments/course_pricing_test/pricing?userId=${userId}&tenantId=${TENANT_ID}`
);
const { pricing } = await resp.json();
const coursePrice = pricing.price;
// Track enrollment as conversion
await fetch(`${API_BASE}/api/experiments/course_pricing_test/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId, tenantId: TENANT_ID, revenue: coursePrice
})
});Embed Snippet (No-Code)
The fastest way to add Last Price to any website. No build tools needed, just a drop-in JavaScript widget.
The snippet authenticates with an embed token, created on the Embed Snippet page. It is publishable, so it is safe in your page source: it reaches the pricing lookup and the conversion beacon only, and it is rate limited per client address (visitors sharing an address share a budget). Calls from a browser work only from the sites you register on it, which is what stops someone copying the token onto their own page. That is a browser check, not proof of where a request came from: a script outside a browser can claim any site. Anything that has to be trustworthy, revenue reporting included, belongs on your server with your secret API key, which stays there, where nobody viewing a page can read it. A page on another origin cannot send one at all, because the cross-origin preflight does not allow the header that carries it, though that follows from crossing an origin rather than from anything protecting a key you have published.
Basic Setup
<div id="pricing-container"></div>
<script src="https://last-price.ai/lastprice.js"></script>
<script>
LastPrice.configure({
apiBase: 'https://api.last-price.ai',
embedToken: 'lpe_YOUR_EMBED_TOKEN',
tenantId: 'YOUR_TENANT_ID',
experimentId: 'YOUR_EXPERIMENT_KEY'
});
LastPrice.showPricing('#pricing-container');
</script>Auto-Initialize via Data Attributes
<div id="pricing-container"></div>
<script
src="https://last-price.ai/lastprice.js"
data-api-base="https://api.last-price.ai"
data-embed-token="lpe_YOUR_EMBED_TOKEN"
data-tenant-id="YOUR_TENANT_ID"
data-experiment-id="YOUR_EXPERIMENT_KEY"
data-container="#pricing-container"
data-auto-load
></script>Custom Rendering
LastPrice.showPricing('#pricing-container', {
customRenderer: function(pricing) {
// The plan name and the features come from the variant's metadata and
// end up in innerHTML, so escape them the way the default card does.
var esc = function(value) {
return String(value)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
};
return '<div class="my-pricing-card">' +
'<h2>' + esc(pricing.plan) + '</h2>' +
'<p class="price">$' + esc(pricing.price) + '/mo</p>' +
'<button id="lp-convert-btn">Subscribe Now</button>' +
'</div>';
},
onConvert: async function(pricing) {
await LastPrice.convert({ revenue: pricing.price });
window.location.href = '/checkout?price=' + pricing.price;
}
});Snippet API Reference
| Method | Description |
|---|---|
| LastPrice.configure(options) | Set API base URL, embed token, tenant ID, experiment key |
| LastPrice.showPricing(selector, options?) | Fetch variant and render pricing card |
| LastPrice.convert(options?) | Record a conversion event with optional revenue |
| LastPrice.getUserId() | Get the current user's persistent ID (cookie-based) |
| LastPrice.getVariant() | Get the current variant assignment |
API Client (Code-Level)
Full programmatic control over Last Price features using TypeScript, React hooks, REST, or Python.
The server samples below carry your secret API key in the x-api-key header. Without it the API answers 401. They show the header once and then leave it out for brevity; add it to each request. The React sample is the exception and is browser code: it calls a route on its own origin, which is the proxy pattern, so the key stays on the server behind it. Calling the API from a page directly is a different thing again: only the pricing lookup and the conversion beacon answer a cross-origin call at all, and only with a publishable embed token, which is what the snippet above does. A secret key cannot be used from another origin and must never be put in page source.
TypeScript / JavaScript Client
Note: @lastprice/client is a hypothetical SDK package name shown for illustration. In this repo, use the REST API directly.
const API_BASE = 'https://api.last-price.ai';
const TENANT_ID = 'YOUR_TENANT_ID';
// Server side only: this key must never reach a browser.
const HEADERS = { 'x-api-key': process.env.LASTPRICE_API_KEY };
// 1. Get pricing (assigns a variant automatically)
const pricingResp = await fetch(
`${API_BASE}/api/experiments/pricing_experiment_1/pricing?userId=user_123&tenantId=${TENANT_ID}`,
{ headers: HEADERS }
);
const { variant, pricing } = await pricingResp.json();
console.log(variant); // "control" or "experiment"
console.log(pricing.price); // 29.99 or 39.99
// 2. Track a conversion
await fetch(`${API_BASE}/api/experiments/pricing_experiment_1/convert`, {
method: 'POST',
headers: { ...HEADERS, 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'user_123',
tenantId: TENANT_ID,
revenue: 29.99
})
});React Hooks
Note: @lastprice/react is a hypothetical SDK package name shown for illustration. Use the REST API with fetch in a custom hook, as below.
import { useState, useEffect } from 'react';
// Browser code. The path is relative, so this calls a route on your own
// origin that forwards to the API with your secret key. To call the API
// directly from the page instead, use the snippet and its embed token: those
// are the only two operations a browser may reach.
function usePricing(experimentKey, tenantId) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const userId = localStorage.getItem('lp_uid') || crypto.randomUUID();
localStorage.setItem('lp_uid', userId);
fetch(`/api/experiments/${experimentKey}/pricing?userId=${userId}&tenantId=${tenantId}`)
.then(r => r.json()).then(setData).finally(() => setLoading(false));
}, [experimentKey, tenantId]);
return { ...data, loading };
}
function PricingPage() {
const { variant, pricing, loading } = usePricing('my_experiment', 'YOUR_TENANT_ID');
if (loading) return <div>Loading prices...</div>;
return (
<div>
<h2>Plan: {pricing?.plan}</h2>
<p>Price: ${pricing?.price}/month</p>
</div>
);
}REST API (Any Language)
# Assign a variant. The tenant comes from your API key.
curl "https://api.last-price.ai/api/experiments/EXPERIMENT_KEY/pricing?userId=USER_ID" \
-H "x-api-key: YOUR_API_KEY"
# Track a conversion. Send 0 for one that earned nothing; omit revenue to
# record the assigned variant's list price instead.
curl -X POST https://api.last-price.ai/api/experiments/EXPERIMENT_KEY/convert \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "USER_ID", "revenue": 29.99}'
# Get experiment results
curl https://api.last-price.ai/api/experiments/EXPERIMENT_KEY/results \
-H "x-api-key: YOUR_API_KEY"Python Client
import os
import requests
class LastPriceClient:
# Server side. api_key never reaches a browser.
def __init__(self, base_url, tenant_id, api_key):
self.base_url = base_url
self.tenant_id = tenant_id
self.headers = {"x-api-key": api_key}
def get_pricing(self, user_id, experiment_key):
"""Assign a variant and get pricing via the pricing endpoint."""
resp = requests.get(
f"{self.base_url}/api/experiments/{experiment_key}/pricing",
params={"userId": user_id, "tenantId": self.tenant_id},
headers=self.headers
)
resp.raise_for_status()
return resp.json()
def track_conversion(self, user_id, experiment_key, revenue=None):
"""Record a conversion.
Pass revenue=0 for a conversion that earned nothing; it is recorded as
zero. Leaving it out is what asks for the variant's list price, so the
key is omitted rather than sent as null, which the API rejects.
"""
body = {"userId": user_id, "tenantId": self.tenant_id}
if revenue is not None:
body["revenue"] = revenue
resp = requests.post(
f"{self.base_url}/api/experiments/{experiment_key}/convert",
json=body,
headers={**self.headers, "Content-Type": "application/json"}
)
resp.raise_for_status()
return resp.json()
def get_results(self, experiment_key):
"""Get experiment results."""
resp = requests.get(
f"{self.base_url}/api/experiments/{experiment_key}/results",
params={"tenantId": self.tenant_id},
headers=self.headers
)
resp.raise_for_status()
return resp.json()
# Usage
lp = LastPriceClient("https://api.last-price.ai", "YOUR_TENANT_ID", os.environ["LASTPRICE_API_KEY"])
result = lp.get_pricing("user_123", "pricing_test")
print(f"Variant: {result['variant']}, Price: ${result['pricing']['price']}")
results = lp.get_results("pricing_test")
print(f"Control conversion rate: {results['control']['conversionRate']}")Pricing Compute Network (PCN)
The Pricing Compute Network lets you register modular pricing functions, invoke them with rich context, and route requests across multiple strategies - all through a single API.
Pricing Functions
A pricing function encapsulates a single pricing strategy. You can register five types of functions:
- rule_based - Deterministic rules (e.g., margin tables, tiered pricing)
- ml_model - Machine-learning model hosted externally or within Last Price
- llm_prompt - LLM-powered pricing via a prompt template
- external_api - Delegates to your own REST endpoint
- composite - Chains or aggregates multiple functions
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Unique human-readable name |
| function_type | enum | Yes | One of: rule_based, ml_model, llm_prompt, external_api, composite |
| description | string | No | Short description of the function's purpose |
| config | object | Yes | Type-specific configuration (rules, model_url, prompt, endpoint, etc.) |
| cost_multiplier | number | No | Scales the metering cost per invocation (default: 1). Use higher values for premium/expensive models. |
| is_public | boolean | No | Whether to list on the marketplace (default: false) |
# Register a new pricing function
curl -X POST https://api.last-price.ai/api/functions \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "margin-lookup-v2",
"function_type": "rule_based",
"description": "Applies margin based on product category",
"config": {
"rules": [
{ "match": { "product_type": "SaaS" }, "margin": 0.35 },
{ "match": { "product_type": "Hardware" }, "margin": 0.18 }
],
"default_margin": 0.25
}
}'# List your pricing functions
curl https://api.last-price.ai/api/functions \
-H "Authorization: Bearer YOUR_TOKEN"
# Test a function with sample input
curl -X POST https://api.last-price.ai/api/functions/fn_abc123/test \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_001",
"product_type": "SaaS",
"current_price": 99.00,
"currency": "USD"
}'Built-in Functions
The platform ships with 15 pre-registered functions ready to use out of the box - 6 Jale (pricing intelligence), 3 Elo (A/B testing), and 6 Rosetta (data translation). Each has a different cost multiplier reflecting its computational complexity.
| Function | Description | Multiplier |
|---|---|---|
| lastprice/jale-optimizer | Statistical price optimizer | 2× |
| lastprice/jale-elasticity | Price elasticity calculator | 1× |
| lastprice/jale-advanced | Advanced recommendations with alternatives | 5× |
| lastprice/jale-psychological | Psychological pricing (.99 endings) | 1× |
| lastprice/jale-bundle | Bundle pricing optimizer | 2× |
| lastprice/jale-tier-optimizer | Tiered pricing structure optimizer | 2× |
| lastprice/elo-ab-test | Two-arm price split for one call, not recorded (use the Experiments API to track a test) | 1× |
| lastprice/elo-significance | Catalog entry, not callable yet | 1× |
| lastprice/elo-allocator | Catalog entry, not callable yet | 2× |
| lastprice/rosetta-translate | Translate price schemas across platforms | 1× |
| lastprice/rosetta-normalize | Normalize price fields to canonical schema | 1× |
| lastprice/rosetta-validate | Validate schema against canonical contracts | 1× |
| lastprice/rosetta-suggest | AI-assisted schema mapping suggestions | 2× |
| lastprice/rosetta-freeze | Freeze a finalized schema adapter | 1× |
| lastprice/rosetta-ingest | Autonomous merchant onboarding via schema inference | 3× |
The cost_multiplier scales the base compute cost for each invocation. A 5× multiplier means each call costs 5 times the base rate. When a fallback function handles a request, the multiplier of the executed function is used, not the originally requested one.
Compute Price
Submit a single pricing request with full context. The platform selects the appropriate function based on your active routing policy and returns the computed price.
| Parameter | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Unique product identifier |
| product_type | string | No | Product category (e.g., SaaS, Hardware) |
| current_price | number | No | Current listed price |
| currency | string | No | ISO 4217 currency code (default: USD) |
| customer_segment | string | No | Segment label (e.g., enterprise, smb) |
| geo | string | No | Geographic region code (e.g., US, EU, APAC) |
| metadata | object | No | Arbitrary key-value pairs for custom context |
# Compute a single price
curl -X POST https://api.last-price.ai/api/compute/price \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_001",
"product_type": "SaaS",
"current_price": 99.00,
"currency": "USD",
"customer_segment": "enterprise",
"geo": "US"
}'
# Response
{
"request_id": "req_xyz789",
"function_id": "fn_abc123",
"function_name": "margin-lookup-v2",
"computed_price": 128.70,
"currency": "USD",
"confidence": 0.85,
"explanation": "Applied 35% margin for SaaS category",
"latency_ms": 42,
"metering": {
"input_units": 12,
"output_units": 8,
"compute_duration_ms": 42,
"cost": 0.0023
}
}Request Tracing
Compute responses can include a trace with ordered steps, handoffs, and total_ms. Steps describe cache lookup, routing, execution, fallback, constraints, and metering, with a status and duration_ms for each step. A cache hit has cache and meter steps only. Keep the response request_id when investigating a result. See the ComputeTrace schema in the API reference for the complete response contract.
Batch Compute
Process up to 100 pricing requests in a single API call. Each item in the batch is evaluated independently and may route to different functions.
| Parameter | Type | Required | Description |
|---|---|---|---|
| requests | array | Yes | Array of pricing request objects (max 100) |
| fail_fast | boolean | No | Abort the entire batch on the first failure (default: false) |
| max_concurrency | number | No | Maximum number of parallel requests (default: 10) |
# Batch compute prices (up to 100 items)
curl -X POST https://api.last-price.ai/api/compute/batch \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requests": [
{
"product_id": "prod_001",
"product_type": "SaaS",
"current_price": 99.00,
"currency": "USD"
},
{
"product_id": "prod_002",
"product_type": "Hardware",
"current_price": 450.00,
"currency": "EUR",
"geo": "EU"
}
],
"fail_fast": false,
"max_concurrency": 10
}'
# Response
{
"results": [
{
"request_id": "req_001",
"product_id": "prod_001",
"computed_price": 128.70,
"currency": "USD",
"function_id": "fn_abc123"
},
{
"request_id": "req_002",
"product_id": "prod_002",
"computed_price": 531.00,
"currency": "EUR",
"function_id": "fn_def456"
}
],
"summary": {
"total": 2,
"succeeded": 2,
"failed": 0,
"total_cost": 0.0046,
"total_duration_ms": 120
}
}Routing Policies
Routing policies control how the platform selects a pricing function for each request. You can configure strategy, weights, cost/latency constraints, and fallback chains.
cheapest
Select the function with the lowest compute cost
fastest
Select the function with the lowest latency
round_robin
Cycle through functions evenly
weighted
Distribute traffic by configured weights
policy
Rule-based selection from conditions
explicit
Caller specifies the function_id directly
default
Use the tenant's default function
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Policy name, up to 255 characters |
| strategy | enum | Yes | Routing strategy (see above) |
| is_default | boolean | No | Make this the workspace default. Only the default policy is consulted when a request states no routing_preference |
| fallback_function_id | string | No | Single fallback function ID. Send null to clear it |
| config | object | No | Everything below lives inside this object, not at the top level |
| config.weights | object | No | Function ID to weight mapping (for weighted strategy). Keys must be function IDs and at least one weight must be above zero |
| config.rules | array | No | Array of condition → function rules (for policy strategy). Each rule needs a non-empty field, a supported operator, and a function_id |
| config.max_latency_ms | number | No | Maximum acceptable latency in ms. Applied when this policy's strategy selects a function |
| config.max_cost_per_unit | number | No | Maximum cost per compute unit. Applied when this policy's strategy selects a function |
| config.fallback_chain | array | No | Ordered list of fallback function IDs, used when the strategy yields nothing available |
# Create a weighted routing policy with fallback
curl -X POST https://api.last-price.ai/api/routing-policies \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "weighted-with-fallback",
"strategy": "weighted",
"is_default": true,
"config": {
"weights": {
"3f1a7c88-0b2e-4d51-9a63-1c4e7b905d12": 0.7,
"9d2b4e60-7a13-4c8f-b5e2-6f0a83c71d44": 0.3
},
"max_latency_ms": 200,
"fallback_chain": [
"c7e4a1f2-8d35-4b90-a6c1-2e58f3b74a09",
"5b8f0d33-6c21-4a7e-9f84-0d1c6e29b537"
]
}
}'Circuit Breaker
Each pricing function has an automatic circuit breaker that prevents cascading failures. When a function fails repeatedly, the circuit opens and requests are routed to the fallback chain instead.
Closed
Normal operation - requests flow through to the function
Open
Function is unhealthy - requests are immediately rejected or rerouted to fallbacks
Half-Open
Recovery mode - a limited number of trial requests are allowed to test if the function has recovered
| Parameter | Type | Required | Description |
|---|---|---|---|
| failure_threshold | number | No | Consecutive failures before opening the circuit (default: 5) |
| recovery_timeout_ms | number | No | Milliseconds to wait before transitioning from open → half-open (default: 30000) |
| half_open_max_calls | number | No | Trial requests allowed in half-open state (default: 3) |
# Create a routing policy with circuit breaker config
curl -X POST https://api.last-price.ai/api/routing-policies \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "explicit-with-breaker",
"strategy": "explicit",
"config": {
"fallback_chain": ["c7e4a1f2-8d35-4b90-a6c1-2e58f3b74a09"],
"circuit_breaker": {
"failure_threshold": 3,
"recovery_timeout_ms": 60000,
"half_open_max_calls": 2
}
}
}'Price Router
Price Router is the multi-vendor pricing facade - a single API endpoint in front of the entire catalog of built-in and tenant-owned pricing models. Think of it as the "OpenRouter for price computation": you submit a pricing request and the router selects the best function based on your routing policy, handles metering, and returns the result.
The route mirrors POST /api/compute/price but is the canonical product-named entrypoint for external clients, OpenAPI tooling, and AI-agent tool runtimes. The GET returns a public capability descriptor - no auth needed - so AI agents can introspect the network at planning time.
Overview
The Price Router dashboard at /price-router shows all available routing strategies, the built-in function catalog, and a live test console. It links out to the Routing Policies, Compute, and Functions pages so you don't lose context.
| Strategy | Description |
|---|---|
| cheapest | Pick the lowest-cost function that can handle the request |
| fastest | Pick the function with the lowest median latency |
| round_robin | Distribute requests evenly across eligible functions |
| weighted | Send traffic proportional to per-function weights |
| explicit | Use the specified function_id. A missing, unreachable, or inactive id fails immediately; the fallback chain applies only if the chosen function fails while running |
| default | Use the tenant's active routing policy |
Route a Request
Submit a pricing request to the router. The request body is identical to POST /api/compute/price.
Naming a function, by id or by name, reaches built-in functions, functions your workspace owns, and functions with a public marketplace listing. Anything else is answered exactly as a function that does not exist, so the response never reveals whether another workspace holds it. The same rule applies to the single compute endpoint and to every item of a batch.
POST /api/price-router
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"function_id": "00000000-0000-0000-0000-000000000001", // optional UUID - omit to let routing_preference pick
"routing_preference": "explicit", // cheapest | fastest | round_robin | weighted | policy | default
"context": {
"product_id": "prod_001",
"product_type": "ecommerce",
"current_price": 99.00,
"currency": "USD",
"customer_segment": "enterprise"
},
"options": {
"include_explanation": true,
"include_confidence": true
}
}
// Response
{
"request_id": "req_...",
"function_id": "00000000-0000-0000-0000-000000000001",
"function_name": "lastprice/jale-optimizer",
"computed_price": 109.00,
"currency": "USD",
"confidence": 0.87,
"explanation": "Elasticity-adjusted recommendation based on 3-month cohort data",
"alternatives": [],
"metering": { "input_units": 1, "output_units": 1, "compute_duration_ms": 42, "cost": 0.005 },
"metadata": { "model_version": "1", "routing_reason": "explicit", "cached": false }
}Capability Descriptor
The public GET /api/price-router returns a static capability document: platform version, all available providers, routing strategies, and the full built-in function catalog. No auth required - suitable for AI-agent tool manifests and external documentation tooling.
GET /api/price-router
// Response (public, cached 5 minutes)
// model_count values are computed dynamically at runtime from the built-in registry
{
"version": "1.0.0",
"providers": [
{ "id": "jale", "display_name": "Jale", "model_count": 6,
"description": "Elasticity-driven price optimization, bundle pricing, tier optimization, and psychological-pricing models." },
{ "id": "elo", "display_name": "Elo", "model_count": 3,
"description": "Pricing experimentation: a stable two-arm price split for a single call. Significance testing and bandit allocation are catalog entries, not callable yet. Tracked experiments are the Experiments API." },
{ "id": "rosetta", "display_name": "Rosetta", "model_count": 6,
"description": "Schema mapping and contract translation between merchant catalogs / vendor APIs and the canonical pricing core." }
],
"strategies": [
{ "id": "explicit", "description": "Use the function_id supplied in the request. The id must name a built-in function, one this tenant owns, or one with a public marketplace listing; anything else is refused exactly as a function that does not exist. Fails fast if the function is missing, unreachable, or inactive." },
{ "id": "cheapest", "description": "Pick the active function with the lowest cost_per_unit. Compares the base rate, not the metered cost, so a function with a higher cost_multiplier can still be chosen. As a stored policy strategy it selects only from functions inside that policy config.max_cost_per_unit and config.max_latency_ms, and defers to the policy fallback_chain when none qualify; as a request routing_preference it overrides the policy and neither ceiling applies." },
{ "id": "fastest", "description": "Pick the active function with the lowest avg_latency_ms. As a stored policy strategy it selects only from functions inside that policy config.max_cost_per_unit and config.max_latency_ms, and defers to the policy fallback_chain when none qualify; as a request routing_preference it overrides the policy and neither ceiling applies." },
{ "id": "round_robin", "description": "Distribute requests evenly across active functions in the catalog. As a stored policy strategy it selects only from functions inside that policy config.max_cost_per_unit and config.max_latency_ms, and defers to the policy fallback_chain when none qualify; as a request routing_preference it overrides the policy and neither ceiling applies." },
{ "id": "weighted", "description": "Pick a function by weighted random draw using config.weights (function_id to weight) from the tenant default policy. Overrides the strategy that policy declares. With no policy in reach, or no usable weight in it, the draw is an even one over the available functions; a stored weighted policy in that position defers to its fallback chain instead." },
{ "id": "policy", "description": "Use the tenant default routing policy: config.rules in order, first match wins, then the strategy that policy declares, then its fallback_chain, then its fallback_function_id, then the default selection." },
{ "id": "default", "description": "Use the tenant default routing policy if set: a matching rule first, then the strategy that policy declares, then its fallback_chain, then its fallback_function_id. With no policy, or when none of those yield a function, prefer a built-in function, then the cheapest." }
],
"builtins": [ /* all built-in function descriptors - count reflects current registry */ ],
"metering": {
"cost_per_unit": 0.005,
"input_factor": 0.001,
"output_factor": 0.003,
"formula": "(input_units * cost_per_unit * input_factor + output_units * cost_per_unit * output_factor) * cost_multiplier"
}
}Rosetta - Schema Translation
Rosetta solves the hardest integration problem in pricing: every platform uses a different schema. Shopify calls it list_price, Amazon calls it selling_price, your ERP calls it unit_cost.
Rosetta maps any source schema to a canonical schema using AI-assisted planning with hard contracts: the plan is compiled into a vendor contract, validated structurally, and optionally frozen into a sealed adapter that never drifts. Every built-in Rosetta function is also callable as a PCN function (prefix lastprice/rosetta-*) for agent-driven workflows.
| API Route | Dashboard Tab | Purpose |
|---|---|---|
| POST /api/rosetta/translate | Translate | Full pipeline: plan → compile → validate in one call |
| POST /api/rosetta/plan | Mapping Studio | AI-assisted field mapping suggestions from sample data |
| POST /api/rosetta/normalize | - | Normalize a vendor response to canonical schema |
| POST /api/rosetta/freeze | Frozen Adapters | Promote a plan to a sealed, versioned adapter |
| GET/POST /api/rosetta/review | Review Queue | Human review of low-confidence mapping proposals |
| POST /api/rosetta/ingest | Onboard | Autonomous merchant onboarding - infer schema from sample |
| GET /api/rosetta/canonical-schemas | Canonical Schemas | List all built-in canonical price schemas |
Overview
The pipeline has three stages:
- Plan - the heuristic/AI mapper proposes field mappings with per-field confidence scores.
- Compile - the plan is turned into a vendor contract (a validated, executable mapping).
- Validate - the compiled contract is structurally verified against the canonical schema.
Low-confidence mappings are queued for human review. High-confidence adapters can be frozen into immutable, versioned artifacts that the Commerce Control Plane uses at execution time.
Translate
The translate endpoint runs the full plan → compile → validate pipeline in a single call and returns both the mapping plan and the compiled canonical record.
POST /api/rosetta/translate
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"source_schema": {
"id": "merchant.product.v1",
"fields": [
{ "path": "sku", "type": "string" },
{ "path": "list_price", "type": "number" },
{ "path": "currency", "type": "string" }
]
},
"source_record": { "sku": "BLUE-001", "list_price": 29.99, "currency": "USD" },
"canonical_schema": "offer",
"vendor_contract": {
"id": "lastprice.price.v1",
"fields": [
{ "path": "sku", "type": "string", "required": true },
{ "path": "current_price", "type": "number", "required": true },
{ "path": "currency", "type": "string", "required": false }
]
}
}
// Response
{
"ready": true,
"plan": {
"source_schema_id": "merchant.product.v1",
"target_schema_id": "lastprice.price.v1",
"confidence": 0.95,
"tier": "schema-guided",
"notes": [],
"mappings": [
{ "source": "list_price", "target": "current_price", "confidence": 0.97, "origin": "heuristic" },
{ "source": "sku", "target": "sku", "confidence": 0.99, "origin": "heuristic" }
]
},
"payload": { "sku": "BLUE-001", "current_price": 29.99, "currency": "USD" },
"skipped": [],
"validation": { "valid": true, "issues": [] },
"review_queued": null
}Mapping Studio (Plan)
The Mapping Studio lets you draft a mapping from sample data without running the full pipeline. Useful for iterating on schemas before committing. Also agent-callable as lastprice/rosetta-suggest.
POST /api/rosetta/plan
{
"source_schema": {
"id": "my.schema.v1",
"fields": [
{ "path": "list_price", "type": "number" },
{ "path": "currency", "type": "string" }
]
},
"canonical_schema": "offer"
}
// Response
{
"plan": {
"source_schema_id": "my.schema.v1",
"target_schema_id": "offer",
"confidence": 0.88,
"tier": "schema-guided",
"notes": [],
"mappings": [
{ "source": "list_price", "target": "current_price", "confidence": 0.92, "origin": "heuristic" },
{ "source": "currency", "target": "currency", "confidence": 0.99, "origin": "heuristic" }
]
},
"review_queued": null
}Normalize
Normalize takes a raw vendor API response (after execution) and maps it back to the canonical schema. Use this when you receive a price update confirmation from a connector and need to store it in a standard format.
POST /api/rosetta/normalize
{
"plan": {
"sourceSchemaId": "shopify.price.v1",
"targetSchemaId": "offer",
"confidence": 0.95,
"tier": "schema-guided",
"mappings": [
{ "source": "new_list_price", "target": "current_price", "confidence": 0.96, "origin": "heuristic" },
{ "source": "sku", "target": "sku", "confidence": 0.99, "origin": "heuristic" }
]
},
"vendor_response": { "new_list_price": 34.99, "sku": "BLUE-001" }
}
// Response
{
"canonical": { "current_price": 34.99, "sku": "BLUE-001" },
"missing": [],
"lossy": [],
"passthrough": {}
}Freeze Adapter
Freezing promotes a mapping plan to an immutable, versioned adapter. Frozen adapters are used by the Commerce Control Plane at execution time and can be referenced by ID across runs.
POST /api/rosetta/freeze
{
"plan": {
"sourceSchemaId": "shopify.price.v1",
"targetSchemaId": "offer",
"confidence": 0.97,
"tier": "schema-guided",
"mappings": [
{ "source": "list_price", "target": "current_price", "confidence": 0.97, "origin": "heuristic" },
{ "source": "sku", "target": "sku", "confidence": 0.99, "origin": "heuristic" }
]
}
}
// Response
{
"adapter": {
"sourceSchemaId": "shopify.price.v1",
"targetSchemaId": "offer",
"mappings": [
{ "source": "list_price", "target": "current_price", "confidence": 0.97, "origin": "frozen" },
{ "source": "sku", "target": "sku", "confidence": 0.99, "origin": "frozen" }
],
"frozenAt": 1747400000000,
"fingerprint": "a1b2c3d4e5f6"
}
}Review Queue
Mappings with low confidence (or those flagged by the validator) are placed in the review queue for human resolution. Reviewers approve or reject each item; approved items can then be frozen.
# List pending review items
GET /api/rosetta/review
# Approve an item
POST /api/rosetta/review
{ "id": "rev_abc", "action": "approve" }
# Reject an item
POST /api/rosetta/review
{ "id": "rev_abc", "action": "reject", "reason": "wrong field mapped" }Onboard (Ingest)
The Onboard tab provides autonomous merchant onboarding: you paste a sample payload and Rosetta infers the schema, plans the mapping, and proposes a frozen adapter - all in one call. Also callable as the PCN function lastprice/rosetta-ingest for agent-driven onboarding workflows.
POST /api/rosetta/ingest
{
"canonical_target": "offer",
"payload": {
"product_id": "P001",
"sale_price": 49.99,
"currency_code": "USD",
"stock": 12
},
"merchant_id": "merchant_abc"
}
// Response - auto-provisioned (confidence ≥ 0.9, adapter frozen immediately)
{
"status": "auto_provisioned",
"adapter_id": "ada_...",
"fingerprint": "f1a2b3c4",
"canonical_record": { "current_price": 49.99, "currency": "USD" },
"ready": true,
"confidence": 0.94,
"plan": {
"tier": "schema-guided",
"notes": [],
"mappings": [
{ "source": "sale_price", "target": "current_price", "confidence": 0.95, "origin": "heuristic" },
{ "source": "currency_code", "target": "currency", "confidence": 0.98, "origin": "heuristic" }
]
}
}
// status values: "stable" | "auto_provisioned" | "provisional" | "needs_review" | "drift_detected"Marketplace
The Last Price Marketplace allows you to discover, publish, and monetize pricing functions. Publish your own functions to earn revenue when other tenants use them.
Browse Listings
Discover publicly available pricing functions from other providers. Browsing needs no API key: it is rate limited per IP address, and only public listings of active functions appear. Your own listings, private ones included, are ?scope=owned, which needs a key.
# Browse public marketplace listings (no key needed)
curl "https://api.last-price.ai/api/marketplace/listings?limit=20&offset=0"
# Your own listings need a key
curl "https://api.last-price.ai/api/marketplace/listings?scope=owned" \
-H "x-api-key: YOUR_API_KEY"
# Response (public scope)
{
"listings": [
{
"function": {
"id": "8b1c...",
"name": "acme/saas-pricer",
"display_name": "Dynamic SaaS Pricer",
"execution_type": "ml_model",
"cost_per_unit": 0.002,
"is_builtin": false
},
"listing": { "id": "4f2e...", "is_public": true, "commission_rate": 0.15 },
"stats": {
"total_invocations": 124500,
"distinct_consumers": 38,
"usage_status": "published",
"avg_latency_ms": 38,
"avg_confidence": 0.85
}
}
],
"pagination": { "limit": 20, "offset": 0, "total": 47 }
}Publish Functions
List your pricing functions on the marketplace. Set a commission rate and provide a description so other users can discover and subscribe to your function.
# Publish a function to the marketplace
curl -X POST https://api.last-price.ai/api/marketplace/listings \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"function_id": "fn_abc123",
"commission_rate": 0.10,
"category": "SaaS",
"description": "Enterprise SaaS pricing with geo-aware margins"
}'Earnings
Track revenue earned from your published marketplace functions. View totals, breakdowns per function, and historical trends.
# View marketplace earnings
curl https://api.last-price.ai/api/marketplace/earnings \
-H "Authorization: Bearer YOUR_TOKEN" \
-G \
-d "period=last_30d"
# Response
{
"total_earned": 1284.50,
"currency": "USD",
"period": "last_30d",
"by_function": [
{ "function_id": "fn_abc123", "name": "margin-lookup-v2", "earned": 892.30 },
{ "function_id": "fn_def456", "name": "geo-pricer", "earned": 392.20 }
]
}Payouts
View your payout history and current payout status. Payouts are processed automatically via Stripe Connect on a configurable schedule.
# List payout history
curl https://api.last-price.ai/api/marketplace/payouts \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"payouts": [
{
"payout_id": "po_001",
"amount": 892.30,
"currency": "USD",
"status": "paid",
"paid_at": "2024-12-01T00:00:00Z"
},
{
"payout_id": "po_002",
"amount": 1284.50,
"currency": "USD",
"status": "pending",
"estimated_at": "2025-01-01T00:00:00Z"
}
]
}Usage & Billing
Monitor your platform consumption, view granular event logs, and manage Stripe-based billing - all from a single set of endpoints.
Usage Summary
Retrieve an aggregated summary of total requests, compute time, and costs for a given period.
| Parameter | Type | Required | Description |
|---|---|---|---|
| start_date | string (ISO 8601) | Yes | Start of the period |
| end_date | string (ISO 8601) | Yes | End of the period |
| granularity | enum | No | daily, weekly, or monthly (default: daily) |
# Get usage summary
curl https://api.last-price.ai/api/usage \
-H "Authorization: Bearer YOUR_TOKEN" \
-G \
-d "start_date=2024-12-01" \
-d "end_date=2024-12-31" \
-d "granularity=daily"
# Response
{
"total_requests": 48210,
"total_compute_ms": 1932840,
"total_cost": 142.38,
"currency": "USD",
"period": { "start": "2024-12-01", "end": "2024-12-31" }
}Usage Events
Access the per-request granular event log. Each event contains the function invoked, latency, cost, input context, and computed result.
# List usage events (paginated)
curl https://api.last-price.ai/api/usage/events \
-H "Authorization: Bearer YOUR_TOKEN" \
-G \
-d "start_date=2024-12-01" \
-d "page=1" \
-d "per_page=50"
# Response
{
"events": [
{
"event_id": "evt_001",
"timestamp": "2024-12-15T10:32:01Z",
"function_id": "fn_abc123",
"product_id": "prod_001",
"latency_ms": 38,
"cost": 0.003,
"result": { "computed_price": 128.70 }
}
],
"total": 48210,
"page": 1
}Usage Breakdown
View a function-level breakdown of usage. Identify which pricing functions consume the most resources and generate the most traffic.
# Get function-level usage breakdown
curl https://api.last-price.ai/api/usage/breakdown \
-H "Authorization: Bearer YOUR_TOKEN" \
-G \
-d "start_date=2024-12-01" \
-d "end_date=2024-12-31"
# Response
{
"breakdown": [
{
"function_id": "fn_abc123",
"function_name": "margin-lookup-v2",
"requests": 32140,
"avg_latency_ms": 35,
"total_cost": 96.42
},
{
"function_id": "fn_def456",
"function_name": "geo-pricer",
"requests": 16070,
"avg_latency_ms": 62,
"total_cost": 45.96
}
]
}AI Usage
Usage of the AI-backed features (agent generation, task estimation). Reported separately from pricing functions, and split by which key paid for each call: calls served with your own provider key are not billed by Last Price, so platform_key_cost is the billable amount.
A date-only range is normalised before it is queried, so the echoed period comes back as UTC instants and the end covers the whole final day. Fractional seconds are accepted to microsecond precision and preserved exactly.
# Get AI feature usage for the current month
curl https://api.last-price.ai/api/usage/llm \
-H "Authorization: Bearer YOUR_TOKEN" \
-G \
-d "start_date=2024-12-01" \
-d "end_date=2024-12-31"
# Response
{
"tenant_id": "3f2b1c88-5d4e-4a91-9c2f-7b6a0d1e8f42",
"period": {
"start": "2024-12-01T00:00:00.000000Z",
"end": "2024-12-31T23:59:59.999999Z"
},
"usage": {
"total_calls": 128,
"total_input_tokens": 98400,
"total_output_tokens": 24100,
"total_compute_ms": 184000,
"total_cost": 3.87,
"tenant_key_calls": 90,
"platform_key_calls": 38,
"platform_key_cost": 1.12,
"by_operation": [
{
"operation": "llm.agent-generation",
"calls": 74,
"input_tokens": 61200,
"output_tokens": 15300,
"compute_duration_ms": 110400,
"cost": 2.41
},
{
"operation": "llm.task-estimation",
"calls": 54,
"input_tokens": 37200,
"output_tokens": 8800,
"compute_duration_ms": 73600,
"cost": 1.46
}
]
}
}Usage Signals
Record custom signals for analytics and observability. Signals can track conversion rates, A/B test outcomes, or any custom metric you define.
# Record a custom usage signal
curl -X POST https://api.last-price.ai/api/usage/signals \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"eventType": "price_accepted",
"properties": {
"product_id": "prod_001",
"computed_price": 128.70,
"customer_segment": "enterprise"
}
}'Stripe Integration
Last Price integrates natively with Stripe for metered billing, invoicing, and marketplace payouts. Usage is automatically reported to Stripe and reflected on your monthly invoice.
Metered Billing
Usage automatically synced to Stripe meter events
Invoicing
Monthly invoices generated and sent via Stripe
Marketplace Payouts
Earnings paid out via Stripe Connect
Payment Methods
Cards, ACH, and wire supported through Stripe
Authentication & Security
If you are calling the API from your own code, you have two credentials: API keys (long-lived, ideal for server-to-server) and JWT Bearer tokens (short-lived, minted for agents from an agent-bound key). Every secured endpoint takes one of these. The credential exchanges are the exception, since they carry their own credential: /api/oauth/token takes client credentials, and native apps call /api/auth/native-token with their Supabase access token to get a Last Price JWT. Two other credentials exist but are not for API callers: webhook receivers authenticate the sender by signature rather than by key, such as x-paid-signature on /api/webhooks/paid, and the dashboard authenticates with its own session cookie, which is what the settings pages use. Explore all endpoints interactively in the API Reference.
JWT Tokens
This section covers the agent JWT. Native apps get a JWT too, from the Supabase exchange at /api/auth/native-token described above, which carries the signed-in user rather than an agent. Exchange an agent-bound API key for a one-hour token with the OAuth 2.0 client credentials grant, then send it in the Authorization header as a Bearer token. The client_id is the key prefix and the client_secret is the full key. Plain tenant keys are not exchanged: they authenticate directly as x-api-key.
# Exchange an agent-bound key for a JWT
curl -X POST https://api.last-price.ai/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=lp_a3f9e2b1" \
-d "client_secret=lp_a3f9e2b1c4d5e6f7..."
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "price:compute commerce:catalog:read",
"agent_id": "550e8400-e29b-41d4-a716-446655440000"
}
# Use the token
curl https://api.last-price.ai/api/functions \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."API Keys
For server-to-server integrations, use long-lived API keys. Keys are prefixed with lp_ and stored as SHA-256 hashes; the plaintext is returned only once at creation. Optionally restrict a key to specific scopes (e.g., price:compute, billing:read, or a wildcard such as commerce:*) for least-privilege access. Any other name is refused with a 400 that lists the valid scopes, which /.well-known/agents.json also publishes. Keys created earlier with the old read, write or admin labels keep their billing and commerce access. Manage keys via Settings → API Keys in the dashboard, or directly via the API.
# Create a key with scopes (requires JWT)
curl -X POST https://api.last-price.ai/api/api-keys \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"name": "Production server", "scopes": ["price:compute", "pcn:function:read"]}'
# Response (key shown only once, save it now)
{
"api_key": {
"id": "550e8400-...",
"name": "Production server",
"key": "lp_a3f9e2b1c4d5e6f7...",
"key_prefix": "lp_a3f9e2",
"scopes": ["price:compute", "pcn:function:read"],
"created_at": "2025-01-01T00:00:00Z",
"expires_at": null
}
}
# Use the key in subsequent requests
curl https://api.last-price.ai/api/functions \
-H "x-api-key: lp_a3f9e2b1c4d5e6f7..."
# List all active keys
curl https://api.last-price.ai/api/api-keys \
-H "Authorization: Bearer <JWT>"
# Revoke a key
curl -X DELETE https://api.last-price.ai/api/api-keys/<keyId> \
-H "Authorization: Bearer <JWT>"Rotate an API Key
POST /api/api-keys/rotate with the current key in x-api-key replaces that key. Save api_key.key from the 201 response immediately: it is shown once, and the old key stops working. The replacement preserves scopes, agent binding, name, and expiry. Rotation does not extend a sandbox key's lifetime or increase its permissions. Sandbox rotation limits still apply. Use Settings, API keys to manage keys with a browser session.
Legacy Authentication
The x-tenant-id header is rejected on all secured endpoints. Use Authorization: Bearer <JWT> or x-api-key: lp_... instead.
# ❌ Rejected - x-tenant-id no longer accepted on secured endpoints
curl https://api.last-price.ai/api/customers \
-H "x-tenant-id: 00000000-0000-0000-0000-000000000001"
# → 401 Unauthorized
# ✅ Use JWT Bearer
curl https://api.last-price.ai/api/customers \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
# ✅ Or use an API key (lp_ prefix)
curl https://api.last-price.ai/api/customers \
-H "x-api-key: lp_a3f9e2b1c4d5e6f7..."Rate Limiting
All API endpoints are rate-limited per tenant using a sliding window algorithm. When you exceed the limit, the API returns a 429 Too Many Requests response.
| Parameter | Type | Required | Description |
|---|---|---|---|
| X-RateLimit-Limit | header | No | Maximum requests allowed in the window |
| X-RateLimit-Remaining | header | No | Remaining requests in the current window |
| X-RateLimit-Reset | header | No | Unix timestamp when the window resets |
| Retry-After | header | No | Seconds to wait before retrying (on 429) |
# Rate limit headers in every response
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1704067200
# When rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Retry after 30 seconds."
}Webhooks
Receive real-time event notifications via webhooks. Every webhook delivery includes an HMAC-SHA256 signature in the x-paid-signature header, the event type in x-webhook-event, and a Unix timestamp in x-paid-timestamp.
# Register a webhook endpoint (an owner or admin of the workspace)
curl -X POST https://api.last-price.ai/api/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/last-price",
"events": ["balance.low", "platform.changelog", "rosetta.adapter.review_required"]
}'
# The response carries the signing secret (whsec_...). Send a test event with
# POST /api/webhooks/{webhookId}/ping.
# Verify the signature in your handler: HMAC-SHA256 of the raw body, hex encoded
import hmac, hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Core Business Objects
Last Price provides a full suite of business objects for managing your commercial operations. All objects support standard CRUD operations and are accessible via RESTful endpoints.
Customers
Manage customer records including billing details, segments, and metadata.
# Create a customer
curl -X POST https://api.last-price.ai/api/customers \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"email": "billing@acme.com",
"externalId": "acme_123",
"metadata": { "industry": "technology" }
}'
# Response (201)
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": "acme_123",
"name": "Acme Corp",
"email": "billing@acme.com",
"metadata": { "industry": "technology" },
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
# List customers
curl "https://api.last-price.ai/api/customers?limit=25&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"data": [ /* customer rows */ ],
"pagination": { "limit": 25, "offset": 0, "total": 1 }
}
# Look one up by your own identifier
curl https://api.last-price.ai/api/customers/external/acme_123 \
-H "Authorization: Bearer YOUR_TOKEN"Contacts
A contact is a person on a customer account: who an invoice is addressed to, and who an order can name as its billing contact. A contact belongs to exactly one customer and cannot be moved between them.
# Create a contact
curl -X POST https://api.last-price.ai/api/contacts \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Jane Doe",
"email": "jane.doe@acme.com",
"phone": "+1-555-0123",
"externalId": "acme_contact_123",
"address": { "line1": "123 Business Ave", "city": "San Francisco", "country": "USA" }
}'
# Response (201)
{
"id": "3f1a9c88-2b47-4e0d-9f0a-6c2f7d5b1e34",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"external_id": "acme_contact_123",
"name": "Jane Doe",
"email": "jane.doe@acme.com",
"phone": "+1-555-0123",
"address": { "line1": "123 Business Ave", "city": "San Francisco", "country": "USA" },
"metadata": {},
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
# List one customer's contacts
curl "https://api.last-price.ai/api/contacts?customer_id=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer YOUR_TOKEN"
# Update one. Only the properties you send are written; send null to clear
# externalId, phone or address.
curl -X PATCH https://api.last-price.ai/api/contacts/3f1a9c88-2b47-4e0d-9f0a-6c2f7d5b1e34 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "phone": null }'
# Look one up by your own identifier. External IDs are unique per customer,
# so the customer has to come with it.
curl "https://api.last-price.ai/api/contacts/external/acme_contact_123?customerId=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer YOUR_TOKEN"
# Delete one. An order billing this contact keeps its own row and has
# billing_contact_id cleared.
curl -X DELETE https://api.last-price.ai/api/contacts/3f1a9c88-2b47-4e0d-9f0a-6c2f7d5b1e34 \
-H "Authorization: Bearer YOUR_TOKEN"Orders
Create and manage orders. Orders can contain multiple line items and track fulfillment status. An order that has started billing cannot be deleted outright: cancel it first, then delete it.
# Create an order. customerId is a customer UUID.
# Line items are added afterwards, one request each.
curl -X POST https://api.last-price.ai/api/orders \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"totalAmount": 643.50,
"currency": "USD",
"metadata": { "channel": "self-serve" }
}'
# Response (201). Money columns come back as decimal strings, not numbers.
{
"id": "9b2d4f61-0c3a-4e58-8d77-1f5a6b9c0e23",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"agent_id": null,
"status": "draft",
"total_amount": "643.5000",
"currency": "USD",
"metadata": { "channel": "self-serve" },
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
# Add a line, one request per line
curl -X POST https://api.last-price.ai/api/orders/9b2d4f61-0c3a-4e58-8d77-1f5a6b9c0e23/lines \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "description": "Pro plan, January", "quantity": 2, "unitPrice": 19.99 }'
# Activate it
curl -X POST https://api.last-price.ai/api/orders/9b2d4f61-0c3a-4e58-8d77-1f5a6b9c0e23/activate \
-H "Authorization: Bearer YOUR_TOKEN"
# List orders, optionally for one customer
curl "https://api.last-price.ai/api/orders?limit=25&offset=0&customer_id=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"data": [ /* order rows */ ],
"pagination": { "limit": 25, "offset": 0, "total": 1 }
}
# List the line items on an order, oldest first
curl https://api.last-price.ai/api/orders/9b2d4f61-0c3a-4e58-8d77-1f5a6b9c0e23/lines \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{ "data": [ /* order line rows */ ] }
# Cancel an order. This is also how you make one deletable.
curl -X PATCH https://api.last-price.ai/api/orders/9b2d4f61-0c3a-4e58-8d77-1f5a6b9c0e23 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "cancelled" }'
# Delete a draft or cancelled order, and its line items with it.
# Any other status answers 409 and tells you to cancel first.
curl -X DELETE https://api.last-price.ai/api/orders/9b2d4f61-0c3a-4e58-8d77-1f5a6b9c0e23 \
-H "Authorization: Bearer YOUR_TOKEN"Invoices
Generate and manage invoices with line items, tax calculations, and due dates. Invoices can be synced to Stripe for automated collection.
# Create an invoice
curl -X POST https://api.last-price.ai/api/invoices \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"invoiceNumber": "INV-2026-0001",
"amount": 643.50,
"currency": "USD",
"dueDate": "2026-02-01T00:00:00.000Z"
}'
# Response (201). The amount comes back as a decimal string, not a number.
{
"id": "c5a7e310-84bd-4f62-9f0a-2d7c1b8e4a56",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"invoice_number": "INV-2026-0001",
"status": "draft",
"amount": "643.5000",
"currency": "USD",
"due_date": "2026-02-01T00:00:00.000Z",
"metadata": {},
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
# Issue it, and email it to the customer when delivery is configured
curl -X POST https://api.last-price.ai/api/invoices/c5a7e310-84bd-4f62-9f0a-2d7c1b8e4a56/send \
-H "Authorization: Bearer YOUR_TOKEN"
# Response: the invoice row plus the delivery outcome
{
"id": "c5a7e310-84bd-4f62-9f0a-2d7c1b8e4a56",
"status": "sent",
"message": "Invoice issued and emailed to the customer.",
"email": { "delivered": true, "skipped": false }
}
# List invoices, optionally for one customer
curl "https://api.last-price.ai/api/invoices?limit=25&offset=0&customer_id=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"data": [ /* invoice rows */ ],
"pagination": { "limit": 25, "offset": 0, "total": 1 }
}Payments
Record and track payments against invoices. Supports partial payments and multiple payment methods.
# Record a payment
curl -X POST https://api.last-price.ai/api/payments \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"invoiceId": "c5a7e310-84bd-4f62-9f0a-2d7c1b8e4a56",
"amount": 643.50,
"currency": "USD",
"status": "completed",
"paymentMethod": "card",
"metadata": { "reference": "ch_abc123" }
}'
# Response (201). The amount comes back as a decimal string, not a number.
{
"id": "e81f0d45-6a2c-4b93-8177-0c9d3e5f2a68",
"invoice_id": "c5a7e310-84bd-4f62-9f0a-2d7c1b8e4a56",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amount": "643.5000",
"currency": "USD",
"status": "completed",
"payment_method": "card",
"metadata": { "reference": "ch_abc123" },
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
# Refund it, in full or in part
curl -X POST https://api.last-price.ai/api/payments/e81f0d45-6a2c-4b93-8177-0c9d3e5f2a68/refund \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "amount": 100.00, "reason": "Partial credit" }'
# Response: the payment row plus a confirmation
{
"id": "e81f0d45-6a2c-4b93-8177-0c9d3e5f2a68",
"status": "refunded",
"metadata": { "refund": { "amount": 100, "reason": "Partial credit" } },
"message": "Payment refunded successfully"
}
# List payments
curl "https://api.last-price.ai/api/payments?limit=25&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"data": [ /* payment rows */ ],
"pagination": { "limit": 25, "offset": 0, "total": 1 }
}Disputes
Manage payment disputes and chargebacks. Track status, submit evidence, and monitor resolutions.
# Open a dispute against a payment
curl -X POST https://api.last-price.ai/api/disputes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"paymentId": "e81f0d45-6a2c-4b93-8177-0c9d3e5f2a68",
"amount": 643.50,
"currency": "USD",
"reason": "product_not_received"
}'
# List disputes
curl "https://api.last-price.ai/api/disputes?limit=25&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response. The amount comes back as a decimal string, not a number.
{
"data": [
{
"id": "1d6b8f23-9e04-47ac-b5f1-3a8c0d2e7b94",
"payment_id": "e81f0d45-6a2c-4b93-8177-0c9d3e5f2a68",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amount": "643.5000",
"currency": "USD",
"status": "open",
"reason": "product_not_received",
"metadata": {},
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
],
"pagination": { "limit": 25, "offset": 0, "total": 1 }
}
# Move it on. Valid states: open, under_review, resolved, rejected
curl -X PATCH https://api.last-price.ai/api/disputes/1d6b8f23-9e04-47ac-b5f1-3a8c0d2e7b94 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "resolved" }'Credits
Issue credits to customer accounts. Credits can be applied to future invoices automatically or manually.
# Issue a credit bundle
curl -X POST https://api.last-price.ai/api/credits \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amount": 50.00,
"currency": "USD",
"expiresAt": "2026-12-31T23:59:59.000Z",
"metadata": { "reason": "Service disruption compensation" }
}'
# Response (201). Amount and balance come back as decimal strings.
{
"id": "a3c05e78-1b94-4d26-8f50-6e7b2c9a1d43",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amount": "50.0000",
"balance": "50.0000",
"currency": "USD",
"expires_at": "2026-12-31T23:59:59.000Z",
"metadata": { "reason": "Service disruption compensation" },
"created_at": "2026-01-15T14:30:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
# View a customer's balance, summed over the bundles that have not expired
curl "https://api.last-price.ai/api/credits/balance?customerId=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response: one row per currency, plus those rows added together
{
"customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"balances": [
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"total_balance": "50.0000",
"currency": "USD",
"bundle_count": "1"
}
],
"totalBalance": 50
}Agents
Agents represent AI-powered pricing bots that automate pricing workflows. Each agent can have its own pricing model, metadata, and optional external identifier for integration with your systems.
Create Agent
Register a new agent with a name, optional description, pricing model, and custom metadata.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Human-readable agent name |
| externalId | string | No | Your own identifier for cross-system lookups |
| description | string | No | Short description of the agent's purpose |
| pricingModel | string | No | Pricing model identifier to use |
| metadata | object | No | Arbitrary key-value pairs for custom data |
# Create a new agent
curl -X POST https://api.last-price.ai/api/agents \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Dynamic Pricer",
"description": "Adjusts SaaS pricing based on usage patterns",
"externalId": "agent-prod-001",
"pricingModel": "usage_based",
"metadata": {
"tier": "enterprise",
"region": "us-east"
}
}'
# Response (201)
{
"id": "a1b2c3d4-...",
"external_id": "agent-prod-001",
"name": "Dynamic Pricer",
"description": "Adjusts SaaS pricing based on usage patterns",
"pricing_model": "usage_based",
"metadata": { "tier": "enterprise", "region": "us-east" },
"created_at": "2025-01-10T09:00:00Z",
"updated_at": "2025-01-10T09:00:00Z"
}Manage Agents
List, retrieve, update, or delete agents. Pagination is supported via limit (max 1000) and offset query parameters.
# List agents (paginated)
curl "https://api.last-price.ai/api/agents?limit=50&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
# Get a single agent
curl https://api.last-price.ai/api/agents/AGENT_ID \
-H "Authorization: Bearer YOUR_TOKEN"
# Update an agent
curl -X PATCH https://api.last-price.ai/api/agents/AGENT_ID \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Pricer",
"metadata": { "tier": "premium" }
}'
# Delete an agent
curl -X DELETE https://api.last-price.ai/api/agents/AGENT_ID \
-H "Authorization: Bearer YOUR_TOKEN"Signals
Signals is the behavioral data layer for pricing intelligence. It captures every meaningful event - agent pricing decisions, inference calls, conversion outcomes, customer interactions - and surfaces them as a queryable stream. The Commerce Control Plane, Jale, and Elo all emit signals automatically; you can also push custom signals via the API.
Overview
Navigate to /signals in the dashboard to browse the signal stream. Toggle Raw signalsto see unprocessed events. Filter by time range, agent, customer, or event type. Signals feed into Jale's optimization loop and are available via the usage-signals API endpoint.
| Signal type | Emitted by | Use |
|---|---|---|
| price_computed | PCN / Price Router | Every routed inference call |
| variant_assigned | Elo | A/B test bucket assignment |
| conversion | Elo / Snippet / API | Revenue event attached to a variant |
| proposal_created | Commerce CP | Pricing policy produced a proposal |
| proposal_executed | Commerce CP | Price pushed to connector |
| agent_action | Agents | Any agent-driven pricing decision |
| custom | Your code | Any domain event you push via the API |
Agent Pricing Signals
AI agents that call the Price Router or PCN can record price_computed and agent_action signals via POST /api/usage/signals. The Agent Pricing Signals dashboard (/signals) lets you drill into per-agent decision histories, see which pricing models were used, and correlate outcomes with revenue.
# Record a signal from an agent pricing action
POST /api/usage/signals
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"eventType": "price_computed",
"agentId": "4f1b8a91-2e07-44b6-9a3a-1b8c2e72b001",
"customerId": "0b6c7d1e-5f43-4a8e-9d2b-3c1f6e8a7b90",
"properties": {
"function_used": "lastprice/jale-optimizer",
"input_price": 49.99,
"output_price": 54.99,
"confidence": 0.91,
"latency_ms": 230
},
"metadata": {}
}
// Response (201 Created)
{
"id": "9d3e2c71-6a4b-4f8e-b1c2-7e5d8a9f0b13",
"message": "Usage signal recorded successfully",
"signal": {
"id": "9d3e2c71-6a4b-4f8e-b1c2-7e5d8a9f0b13",
"tenant_id": "2a7c9e14-3b5d-4c6f-8e1a-6d4b2f9c7e05",
"agent_id": "4f1b8a91-2e07-44b6-9a3a-1b8c2e72b001",
"customer_id": "0b6c7d1e-5f43-4a8e-9d2b-3c1f6e8a7b90",
"event_type": "price_computed",
"properties": { "function_used": "lastprice/jale-optimizer", "output_price": 54.99 },
"metadata": {},
"created_at": "2025-01-15T10:23:00.000Z"
}
}Experiments & A/B Testing
Run pricing experiments across your products. Define variants with different price points, assign users deterministically, track conversions, and view statistical results - all through a simple API.
Create Experiment
Create a new experiment with a unique key and exactly two pricing variants, namedcontrol and experiment. Those are the only names the assignment engine serves, so any other name, or a third arm, is rejected. Each variant can carry custom metadata (e.g., features, discount percentages) that gets forwarded to clients.
| Parameter | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Unique key (lowercase alphanumeric + underscores) |
| name | string | Yes | Human-readable experiment name |
| description | string | No | Description of what is being tested |
| variants | array | Yes | Exactly two variants: the engine is two-armed |
| variants[].name | string | Yes | Must be 'control' or 'experiment', lowercase. Any other name is rejected |
| variants[].price | number | Yes | Price for this variant (≥ 0) |
| variants[].weight | number | No | Relative traffic weight, normalised against the other arm, so 70/30 and 0.7/0.3 are the same split. Set both or neither (default: equal). The engine resolves traffic to 1%, so neither arm may work out below that: 1/99 is the most uneven split accepted |
| variants[].metadata | object | No | Arbitrary data forwarded to clients |
| targetSampleSize | number | No | Target number of users, stored with the experiment and returned with it. Nothing completes an experiment automatically yet, so this records your intent rather than triggering anything |
# Discover your tenant first. Do not type a UUID: the tenant in
# the path must match the one your credential resolves to, or it is a 403.
curl https://api.last-price.ai/api/me -H "Authorization: Bearer YOUR_TOKEN"
# => { "tenant_id": "...", ... } Use that value as TENANT_ID below.
# Create a pricing experiment. A JWT goes in Authorization; an API key goes
# in x-api-key and is not accepted as a bearer token. Send one or the other.
curl -X POST https://api.last-price.ai/api/tenants/TENANT_ID/experiments \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "homepage_pricing",
"name": "Homepage Pricing Test",
"description": "Testing $29 vs $39 price points",
"variants": [
{
"name": "control",
"price": 29,
"weight": 0.7,
"metadata": {
"plan": "Pro",
"features": ["Unlimited projects", "Priority support"]
}
},
{
"name": "experiment",
"price": 39,
"weight": 0.3,
"metadata": {
"plan": "Pro Plus",
"features": ["Unlimited projects", "Priority support", "API access"]
}
}
],
"targetSampleSize": 1000
}'
# List experiments
curl "https://api.last-price.ai/api/tenants/TENANT_ID/experiments?status=active" \
-H "Authorization: Bearer YOUR_TOKEN"Get Pricing
Retrieve the pricing for a specific user. The system deterministically assigns the user to a variant and returns the full pricing payload including any variant metadata. The path parameter is the experiment key you chose, such as homepage_pricing, not the UUID of the experiment record.
# From your server, with your secret API key
curl "https://api.last-price.ai/api/experiments/homepage_pricing/pricing?userId=user_abc123" \
-H "x-api-key: lp_YOUR_API_KEY"
# From a web page, the snippet sends a publishable embed token instead
# x-embed-token: lpe_YOUR_EMBED_TOKEN
# Response
{
"userId": "user_abc123",
"experimentId": "homepage_pricing",
"variant": "control",
"pricing": {
"plan": "Control",
"price": 29,
"features": ["Feature A", "Feature B", "Feature C"]
}
}The pricingobject includes the variant's plan, price, features, and any additional metadata fields from the variant definition.
Track Conversions
Record a conversion event when a user completes a purchase or desired action. Send the amount you actually settled, including 0 for a free trial activation or a fully discounted order: an explicit zero is recorded as zero. Omitting revenueis what asks for the assigned variant's list price instead. The tenant comes from your credential; the optional tenantId in the body is only checked against it, and a mismatch is rejected.
# From your server, with your secret API key. A browser sends
# x-embed-token instead; the snippet does this for you.
curl -X POST https://api.last-price.ai/api/experiments/homepage_pricing/convert \
-H "x-api-key: lp_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_abc123",
"revenue": 29.00
}'
# Response
{
"success": true,
"userId": "user_abc123",
"experimentId": "homepage_pricing",
"variant": "control",
"revenue": 29
}View Results
Retrieve experiment performance data including per-variant views, conversions, conversion rates, and revenue metrics. Use the definition endpoint for the full experiment configuration.
# Get experiment results. Server side only: an embed token does not
# reach these, and there is no cross-origin access to them.
curl https://api.last-price.ai/api/experiments/homepage_pricing/results \
-H "x-api-key: lp_YOUR_API_KEY"
# Get experiment definition
curl https://api.last-price.ai/api/experiments/homepage_pricing/definition \
-H "x-api-key: lp_YOUR_API_KEY"
# Results response
{
"experimentId": "EXPERIMENT_ID",
"control": {
"views": 523,
"conversions": 47,
"revenue": "1363.00",
"conversionRate": "8.99%",
"arpu": "29.00"
},
"experiment": {
"views": 477,
"conversions": 31,
"revenue": "1209.00",
"conversionRate": "6.50%",
"arpu": "39.00"
}
}Jale - AI Price Optimizer
Jale is the built-in AI pricing optimization engine. It analyzes experiment data to recommend optimal prices and can automatically propose new variants based on conversion performance.
Optimize Pricing
Submit an optimization request to get AI-powered pricing recommendations for an active experiment. Optionally specify an objective (e.g., maximize revenue or conversions) and candidate price points. Invalid candidate prices return 400. Missing experiments return 404. Unavailable or malformed experiment data returns 502 without a recommendation.
| Parameter | Type | Required | Description |
|---|---|---|---|
| experimentId | string | Yes | Experiment to optimize |
| objective | string | No | Optimization goal (e.g., 'maximize_revenue', 'maximize_conversions') |
| candidates | array | No | Candidate price points to evaluate |
| lookbackDays | number | No | Days of historical data to consider |
# Get pricing optimization recommendation
curl -X POST https://api.last-price.ai/api/jale/optimize \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"experimentId": "homepage_pricing",
"objective": "maximize_revenue",
"candidates": [19, 24, 29, 34, 39],
"lookbackDays": 30
}'Propose Variant
Not usable yet. This would add a third variant to an existing experiment, but the assignment engine is two-armed and a valid experiment already holds both arms, so every proposal is rejected with a 400 rather than stored where it could never be served. The endpoint becomes useful when multi-variant testing lands. Documented here so the behaviour is not a surprise.
| Parameter | Type | Required | Description |
|---|---|---|---|
| experimentId | string | Yes | Target experiment |
| price | number | Yes | Price point for the new variant (must be greater than 0) |
| label | string | No | Human-readable variant name (auto-generated if omitted) |
| metadata | object | No | Custom data for the variant |
# Propose a new variant
curl -X POST https://api.last-price.ai/api/jale/propose-variant \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"experimentId": "homepage_pricing",
"price": 34,
"label": "mid_tier_test",
"metadata": {
"plan": "Pro",
"features": ["Unlimited projects", "Priority support", "Custom branding"]
}
}'
# Response today. A valid experiment already holds both arms the engine can
# serve, so this proposal would add a third that is never assigned.
{
"error": "Cannot propose an additional variant",
"message": "Exactly 2 variants are required, named control and experiment. Received 3. The assignment engine is two-armed, so any additional variant would never be served. Multi-variant testing is not supported yet."
}JavaScript SDK
A lightweight (~4 KB minified, ~1.5 KB gzipped) vanilla JavaScript SDK for embedding A/B tested pricing on any website. Zero dependencies - works with any framework or static site.
Installation
Include the SDK via a script tag. You can either configure it programmatically or use data attributes for automatic initialization.
<!-- Option 1: Script tag with manual configuration -->
<div id="pricing-container"></div>
<script src="https://last-price.ai/lastprice.js"></script>
<script>
LastPrice.configure({
apiBase: 'https://api.last-price.ai',
embedToken: 'lpe_YOUR_EMBED_TOKEN',
tenantId: 'YOUR_TENANT_ID',
experimentId: 'homepage_pricing'
});
LastPrice.showPricing('#pricing-container');
</script>
<!-- Option 2: Data attributes (auto-load) -->
<div id="pricing-container"></div>
<script
src="https://last-price.ai/lastprice.js"
data-api-base="https://api.last-price.ai"
data-embed-token="lpe_YOUR_EMBED_TOKEN"
data-tenant-id="YOUR_TENANT_ID"
data-experiment-id="homepage_pricing"
data-auto-load
data-container="#pricing-container"
></script>Configuration
Configure the SDK with your API base URL, embed token, tenant ID, and experiment key. Optional settings control cookie behavior and debug logging.
The embed token is required: the SDK refuses to call the API without one. Create it on the Embed Snippet page and register every site the snippet runs on. It is publishable, so it belongs in page source; your secret API key does not. The API also refuses a secret key on a cross-origin call, because the preflight does not allow its header, but do not read that as a safety net: it says nothing about the key being readable by every visitor once it is in the page.
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiBase | string | Yes | Your Last Price API base URL |
| embedToken | string | Yes | Publishable embed token (lpe_...), valid only from the sites registered on it |
| tenantId | string | Yes | Your tenant identifier |
| experimentId | string | Yes | Experiment key to load pricing from, such as homepage_pricing. Not the UUID of the experiment record. |
| cookieName | string | No | Cookie name for user ID persistence (default: lp_user_id) |
| cookieMaxAge | number | No | Cookie expiry in seconds (default: 1 year) |
| debug | boolean | No | Enable debug logging to console (default: false) |
LastPrice.configure({
apiBase: 'https://api.last-price.ai',
embedToken: 'lpe_YOUR_EMBED_TOKEN',
tenantId: 'YOUR_TENANT_ID',
experimentId: 'homepage_pricing',
cookieName: 'lp_user_id', // optional
cookieMaxAge: 31536000, // optional (1 year)
debug: false // optional
});
// Helper methods
const userId = LastPrice.getUserId(); // Get or create persistent user ID
const variant = LastPrice.getVariant(); // Get current variant assignmentShow Pricing
Render a pricing card into a container element. The SDK fetches the user's assigned variant and renders it automatically. Use a custom renderer for full control over the UI.
// Default renderer - pricing card with "Buy Now" button
LastPrice.showPricing('#pricing-container');
// With conversion callback. It REPLACES the default handler, so record the
// conversion yourself: the button records nothing otherwise.
LastPrice.showPricing('#pricing-container', {
onConvert: async function(pricing) {
await LastPrice.convert({ revenue: pricing.price });
window.location.href = '/checkout';
}
});
// Custom renderer - full control over markup
let convertPricing;
LastPrice.showPricing('#pricing-container', {
customRenderer: function(pricing, convertFn) {
convertPricing = convertFn;
// The plan name and the features come from the variant's metadata and
// end up in innerHTML, so escape them the way the default card does.
var esc = function(value) {
return String(value)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
};
return '<div class="my-pricing">'
+ '<h2>' + esc(pricing.plan) + '</h2>'
+ '<p class="price">$' + esc(pricing.price) + '/mo</p>'
+ '<ul>' + (pricing.features || []).map(function(f) {
return '<li>' + esc(f) + '</li>';
}).join('') + '</ul>'
// Deliberately NOT id="lp-convert-btn". The SDK wires that id up
// itself, so a button carrying it and the handler below would both
// fire on one click and record two conversions. Use the reserved id
// and drop this listener, or use your own id and keep it.
+ '<button id="my-subscribe-btn" type="button">Subscribe</button>'
+ '</div>';
}
});
document.addEventListener('click', function(event) {
if (event.target && event.target.id === 'my-subscribe-btn' && convertPricing) {
convertPricing();
}
});Track Conversions (SDK)
Record conversion events programmatically. Useful when the purchase flow happens outside the embedded pricing card (e.g., after a checkout process).
// Record a conversion with revenue
LastPrice.convert({
revenue: 29.00,
onSuccess: function(response) {
console.log('Conversion recorded:', response);
},
onError: function(error) {
console.error('Conversion failed:', error);
}
});
// Simple conversion without revenue
LastPrice.convert();Cost Tracking
Track costs across agents, customers, and operations. Record individual cost events and retrieve aggregated analytics to understand spending patterns over time.
Record Costs
Record a cost event associated with an agent, customer, or custom cost type.
| Parameter | Type | Required | Description |
|---|---|---|---|
| costType | string | Yes | Category of cost (e.g., 'inference', 'compute', 'api_call') |
| amount | number | Yes | Cost amount |
| currency | string | Yes | ISO 4217 currency code (e.g., USD) |
| agentId | uuid | No | Associated agent identifier |
| customerId | uuid | No | Associated customer identifier |
| metadata | object | No | Additional context about the cost |
# Record a cost
curl -X POST https://api.last-price.ai/api/costs \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "a1b2c3d4-e5f6-4789-8abc-1234567890ab",
"customerId": "b2c3d4e5-f6a7-4890-bcde-2345678901bc",
"costType": "inference",
"amount": 0.0042,
"currency": "USD",
"metadata": {
"model": "gpt-4",
"tokens": 1250
}
}'List Costs
Retrieve cost records with optional date-range filtering and pagination.
# List costs with date range
curl "https://api.last-price.ai/api/costs?startDate=2025-01-01T00:00:00Z&endDate=2025-01-31T23:59:59Z&limit=100&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"data": [
{
"id": "d4e5f6a7-b8c9-4012-def0-456789012345",
"agent_id": "a1b2c3d4-e5f6-4789-8abc-1234567890ab",
"customer_id": "b2c3d4e5-f6a7-4890-bcde-2345678901bc",
"cost_type": "inference",
"amount": "0.0042",
"currency": "USD",
"metadata": { "model": "gpt-4", "tokens": 1250 },
"created_at": "2025-01-15T14:30:00Z"
}
],
"pagination": {
"limit": 100,
"offset": 0,
"total": 1
}
}Cost Analytics
Get aggregated cost analytics grouped by time period, agent, customer, or cost type.
| Parameter | Type | Required | Description |
|---|---|---|---|
| startDate | ISO 8601 | No | Start of the analysis window |
| endDate | ISO 8601 | No | End of the analysis window |
| groupBy | enum | No | Group results by: day, week, month, agent, customer, or type (default: day) |
# Get cost analytics grouped by day
curl "https://api.last-price.ai/api/costs/analytics?startDate=2025-01-01&endDate=2025-01-31&groupBy=day" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response. Every count and sum is aggregated in Postgres and arrives as a
# decimal string, not a number.
{
"analytics": [
{
"group_key": "2025-01-15T00:00:00.000Z",
"count": "42",
"total_amount": "1.7600",
"avg_amount": "0.0420",
"min_amount": "0.0010",
"max_amount": "0.1500",
"currency": "USD"
}
],
"totals": [
{
"total_count": "1250",
"total_amount": "52.3000",
"currency": "USD"
}
],
"groupBy": "day"
}Demo Apps
Pre-built demo applications that showcase Last Price pricing experiments in action. Three demo apps are included: an e-commerce store, a marketplace, and a digital products platform - each with its own pricing experiment and variant configurations.
Overview
List all demo apps with their current status, variant counts, and performance metrics. Demo apps use a special demo tenant managed by the system.
# List demo apps
curl https://api.last-price.ai/api/demo-apps \
-H "Authorization: Bearer YOUR_TOKEN"
# Response
{
"apps": [
{
"id": "ecommerce-store",
"name": "E-Commerce Store",
"description": "Product pricing experiment",
"experimentKey": "ecommerce_pricing",
"status": "active",
"variants": 3,
"views": 1250,
"conversions": 89,
"conversionRate": "7.1%"
},
{
"id": "marketplace",
"name": "Marketplace",
"description": "Seller commission experiment",
"experimentKey": "marketplace_commission",
"status": "not_seeded",
"variants": 2,
"views": 0,
"conversions": 0,
"conversionRate": "0.0%"
},
{
"id": "digital-products",
"name": "Digital Products",
"description": "Course pricing experiment",
"experimentKey": "course_pricing",
"status": "active",
"variants": 3,
"views": 620,
"conversions": 41,
"conversionRate": "6.6%"
}
]
}Seed Data
Initialize demo experiments by seeding sample data. You can seed all demo apps at once or a specific app by passing the app query parameter.
# Seed all demo apps
curl -X POST https://api.last-price.ai/api/demo-apps/seed \
-H "Authorization: Bearer YOUR_TOKEN"
# Seed a specific demo app
curl -X POST "https://api.last-price.ai/api/demo-apps/seed?app=ecommerce-store" \
-H "Authorization: Bearer YOUR_TOKEN"
# Response (201)
{
"tenantId": "00000000-0000-0000-0000-000000000000",
"results": [
{
"app": "ecommerce-store",
"experimentId": "exp_uuid_...",
"experimentKey": "ecommerce_pricing",
"status": "active",
"action": "created"
}
]
}A/B Testing with Elo
Elo is the A/B testing engine that powers deterministic variant assignment, experiment management, and conversion tracking.
How Variant Assignment Works
Elo uses SHA-256 hashing for deterministic variant assignment:
- Same
userId + experimentId→ always the same variant - Assignment is deterministic, though the server may still read or persist assignment and view data
- 100-bucket system supports percentage-based splits
- Consistent across sessions, devices, and servers
Every request needs a credential: your secret API key from a server, or a publishable embed token from a page, which is what the snippet sends. Without one the API answers 401. The workspace comes from that credential, so tenantId is optional and only has to agree with it. The same user and experiment always return the same variant, using a 100-bucket SHA-256 hashing scheme.
| Parameter | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | Unique user/visitor identifier |
| tenantId | string | No | Optional. The workspace comes from your credential; when this is present it only has to agree with it. Query parameter for GET, body field for POST |
# From a server. A page sends x-embed-token instead.
curl "https://api.last-price.ai/api/experiments/EXPERIMENT_KEY/pricing?userId=USER_ID" \
-H "x-api-key: YOUR_API_KEY"
# Response
{
"userId": "USER_ID",
"experimentId": "EXPERIMENT_KEY",
"variant": "control",
"pricing": {
"plan": "control",
"price": 29,
"features": ["Feature A", "Feature B"],
"metadata": {}
}
}Create an Experiment
Create a new pricing experiment. Variant names must be "control" and "experiment"to match Elo's assignVariant() output. weight is optional - Elo defaults to an even 50/50 split when weights are not provided.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Human-readable experiment name |
| key | string | Yes | Unique key used in API URLs |
| variants[].name | string | Yes | Variant name: must be "control" or "experiment", lowercase |
| variants[].price | number | Yes | Price for this variant |
| variants[].weight | number | No | Relative traffic weight, normalised against the other arm, so 70/30 and 0.7/0.3 are the same split. Set both or neither (default: even). The engine resolves traffic to 1%, so 1/99 is the most uneven split accepted |
curl -X POST https://api.last-price.ai/api/tenants/YOUR_TENANT_ID/experiments \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q2 Pricing Test",
"key": "q2_pricing",
"variants": [
{ "name": "control", "price": 29 },
{ "name": "experiment", "price": 39 }
]
}'View Experiment Results
| Parameter | Type | Required | Description |
|---|---|---|---|
| tenantId | string | Yes | Tenant identifier (query parameter) |
curl "https://api.last-price.ai/api/experiments/EXPERIMENT_KEY/results?tenantId=YOUR_TENANT_ID"
# Response
{
"experimentId": "q2_pricing",
"control": {
"views": 5000,
"conversions": 150,
"revenue": "4350.00",
"conversionRate": "3.00%",
"arpu": "29.00"
},
"experiment": {
"views": 5000,
"conversions": 120,
"revenue": "4680.00",
"conversionRate": "2.40%",
"arpu": "39.00"
}
}AI Pricing Optimization (Jale)
Jale is the AI pricing optimization engine - it provides elasticity analysis, revenue simulation, and intelligent price recommendations.
Price Recommendations
curl -X POST https://api.last-price.ai/api/compute/price \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: YOUR_TENANT_ID" \
-d '{
"function_name": "lastprice/jale-optimizer",
"input": {
"current_price": 49.99,
"product_type": "saas_subscription",
"market_data": {
"competitor_prices": [29, 39, 59, 79],
"current_conversion_rate": 0.025,
"monthly_visitors": 10000,
"churn_rate": 0.05
}
}
}'Elasticity Analysis
curl -X POST https://api.last-price.ai/api/compute/price \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: YOUR_TENANT_ID" \
-d '{
"function_name": "lastprice/jale-elasticity",
"input": {
"prices": [19, 29, 39, 49, 59],
"quantities": [500, 350, 200, 120, 60],
"product_category": "software"
}
}'Psychological Pricing
curl -X POST https://api.last-price.ai/api/compute/price \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: YOUR_TENANT_ID" \
-d '{
"function_name": "lastprice/jale-psychological",
"input": {
"base_price": 50,
"strategies": ["charm", "prestige", "anchor"]
}
}'Competitive Price Scraping
The built-in scraper collects competitor pricing data to feed into Jale's optimization engine.
# Setup
cd scraper
pip install -r requirements.txtfrom scrape_api import scrape_competitor_prices
# Scrape competitor pricing data
results = scrape_competitor_prices(
urls=["https://competitor1.com/pricing", "https://competitor2.com/pricing"],
selectors={"price": ".price-amount", "plan": ".plan-name"}
)
# Feed into Jale for optimization
import requests
requests.post("https://api.last-price.ai/api/compute/price", json={
"function_name": "lastprice/jale-optimizer",
"input": {
"current_price": 49.99,
"competitor_prices": [r["price"] for r in results]
}
}, headers={"X-Tenant-ID": "YOUR_TENANT_ID"})Commerce Control Plane
The commerce control plane turns computed prices into actually-shipped prices. It sits on top of the core PriceRouter and automates the full pipeline: pull catalog data from your stores → observe competitor pricing → match items → apply pricing policies → queue proposals for approval → push approved prices back via connector adapters.
The pipeline is implemented as seven packages orchestrated end-to-end:
| Stage | Package | Purpose |
|---|---|---|
| Connectors | @packages/connectors | Pull catalog data from Shopify, Amazon SP-API, and memory adapters |
| Observations | @packages/observations | Ingest competitor prices from PriceAPI, Keepa, or manual POST |
| Matching | @packages/matching | Match catalog items to observed competitor items via Rosetta |
| Pricing Policy | @packages/pricing-policy | Evaluate floor/ceiling/competitor rules; calls PriceRouter internally |
| Approvals | @packages/approvals | Queue proposals; auto-approve or hold for manual review |
| Execution Router | @packages/execution-router | Push approved prices back to connectors with retry/backoff |
| Orchestration | @packages/commerce-control-plane | End-to-end runCycle(); exposes /api/commerce/* routes |
Overview
Navigate to /commerce in the Oja dashboard to access the Commerce Control Plane UI. Tabs cover each pipeline stage. The Run cycle button at the top executes one full end-to-end pass (dry-run by default; set execute: true to actually push prices).
A scoped API key or token needs the scope for each step: commerce:catalog:read to read, commerce:configure to set up connectors, policies and data, commerce:proposal:draft to run a cycle, commerce:proposal:approve or commerce:proposal:reject to decide a proposal, and commerce:execute to push prices, including a cycle run with execute: true. A missing scope answers 403 insufficient_scope. Your dashboard session and unscoped keys have full access.
POST /api/commerce/cycle
{
"execute": true // set false for a dry-run (default)
}
// Response
{
"counts": {
"items": 42,
"observations": 120,
"matches": 38,
"proposals": 38,
"auto_approved": 12,
"executed": 12,
"failed": 0
},
"proposals": 38,
"matches": 38,
"executions": [
{ "proposal_id": "prop_...", "status": "executed", "attempts": 1, "last_response": {} },
{ "proposal_id": "prop_...", "status": "failed", "attempts": 2, "reason": "connector_error" }
]
}Connectors
Connector accounts link your storefronts to the pipeline. Supported platforms: shopify, amazon-sp, ucp-http, memory (testing).
# List connector accounts
GET /api/commerce/connectors
# Create a connector account
POST /api/commerce/connectors
{
"platform": "shopify",
"display_name": "My Shopify Store",
"credentials": { "shop": "mystore.myshopify.com", "access_token": "..." },
"metadata": {}
}
# Trigger a catalog sync
POST /api/commerce/connectors/{id}/syncCatalog
Catalog items are pulled from connectors during a sync. Each item carries a current price, SKU, and optional GTIN used for competitor matching.
GET /api/commerce/catalog?limit=200
// Response
{
"items": [
{
"id": "ci_...",
"connector_account_id": "ca_...",
"external_id": "shopify_prod_001",
"title": "Blue Widget",
"sku": "BLUE-WGT-001",
"gtin": "00012345678905",
"current_price": 29.99,
"currency": "USD"
}
]
}Observations
Competitor observations are ingested automatically by the observation sources (PriceAPI, Keepa) during a cycle, or pushed manually via the API.
# Manually ingest a competitor observation
POST /api/commerce/observations
{
"source": "manual",
"competitor": "acme-widgets",
"gtin": "00012345678905",
"observed_price": 24.99,
"currency": "USD"
}Pricing Policies
Policies define the rules that produce price proposals. Each policy has an approval_mode: manual (human reviews all), auto (auto-approve when delta is within auto_approve_max_delta_pct), or shadow (log-only, never executes).
POST /api/commerce/policies
{
"name": "Undercut competitors by 2%",
"rules": [{ "id": "r1", "kind": "undercut_competitor", "by_pct": 2 }],
"approval_mode": "auto",
"auto_approve_max_delta_pct": 10,
"is_active": true,
"scope": {}
}
# Dry-run a policy against current catalog
POST /api/commerce/policies/{id}/evaluateApprovals
Proposals generated by the pricing policy engine land in the approval queue with status proposed. Review and approve or reject them individually, or let auto-approval handle them.
GET /api/commerce/proposals?limit=200
POST /api/commerce/proposals/{id}/approve
{}
POST /api/commerce/proposals/{id}/reject
{ "reason": "price too aggressive" }Execution
The execution router drains all approved proposals through the connector adapters, applying exponential backoff on transient failures. Terminal failures move the proposal to failed and capture the adapter response in the audit log.
POST /api/commerce/execution
// Drains all approved proposals
GET /api/commerce/audit?limit=200
// Append-only event log for the entire pipelineRun Pipeline Cycle
A single POST /api/commerce/cycle runs every stage in order: pull catalogs → pull observations → match snapshots → decide prices → create proposals from decisions → execute approved. All stages are logged to the audit table. The dashboard Run cycle button calls this endpoint with execute: false (dry-run) by default.
POST /api/commerce/cycle
{ "execute": true }
// Cycle summary response
{
"counts": {
"items": 42, "observations": 120, "matches": 38,
"proposals": 38, "auto_approved": 12, "executed": 12, "failed": 0
},
"proposals": 38,
"matches": 38,
"executions": [{ "proposal_id": "prop_...", "status": "executed", "attempts": 1 }]
}Using the Oja Dashboard
The Oja dashboard is your control center for managing all aspects of the platform.
| Section | What You Do There |
|---|---|
| Dashboard | Overview of active experiments, conversions, revenue |
| Functions | Register and manage pricing functions (built-in + custom) |
| Compute | Run single or batch pricing computations |
| Routing Policies | Configure how pricing requests are routed |
| Commerce | Commerce control plane - connectors, catalog, policies, approvals, execution |
| Usage & Costs | View token metering, cost breakdowns, billing periods |
| Marketplace | Browse community pricing functions, publish your own |
| Customers | Manage customer records |
| Orders | View and manage orders |
| Invoices | Generate and track invoices |
| Payments | Track payment status |
| Credits | Manage credit bundles |
| Integrations | Configure inference providers (OpenAI, Anthropic, etc.) |
| API Keys | Generate and manage API keys |
| Settings | Company info, team management, billing, tax configuration |
Workflow: Setting Up a Price Test
- Go to Functions → Verify your pricing functions are registered
- Go to Routing Policies → Set routing strategy
- Create an experiment via the API
- Embed the snippet or integrate via API on your site
- Go to Dashboard → Monitor impressions, conversions, and revenue
- Go to Usage & Costs → Track API usage and costs
Glossary
| Term | Definition |
|---|---|
| Tenant | An isolated account on Last Price (your store/company) |
| Experiment | An A/B test comparing two or more price variants |
| Variant | One of the price options in an experiment (e.g., "$29" vs "$39") |
| Assignment | The process of deterministically placing a user into a variant |
| Conversion | A purchase or subscription triggered by a variant |
| PCN | Pricing Compute Network - the execution engine for pricing functions |
| Elo | The A/B testing engine (named after the Elo rating system) |
| Jale | The AI pricing optimization engine |
| Oja | The admin dashboard for managing everything |
| Function | A pricing computation unit (built-in or custom) |
| Routing Policy | Rules for how pricing requests are distributed across functions |
| Metering | Token-based usage tracking for billing |
| Cost Multiplier | A scaling factor on function execution cost (e.g., 5× for advanced AI) |
| Snippet | The embeddable JavaScript widget for any website |
| Fallback Chain | Ordered backup functions used when the primary fails |
| Circuit Breaker | Automatic failure detection that stops routing to unhealthy functions |
| Price Router | Multi-vendor pricing facade - one API endpoint in front of all built-in and custom pricing models |
| Rosetta | Schema translation service - maps any price data format to canonical schemas using AI-assisted adapters |
| Canonical Schema | A standardized price data structure all platforms are mapped to (e.g., lastprice.price.v1) |
| Frozen Adapter | An immutable, versioned schema mapping promoted from a Rosetta plan |
| Commerce Control Plane | End-to-end pipeline: connectors → observations → matching → policies → approvals → execution |
| Connector | An integration adapter that pulls catalog data and pushes prices to a platform (Shopify, Amazon, etc.) |
| Observation | A competitor price data point ingested by the Commerce Control Plane |
| Proposal | A candidate price change produced by a pricing policy, awaiting approval before execution |
| Signals | Behavioral event stream capturing pricing decisions, assignments, conversions, and agent actions |