Product runtime
How Hyperscale fits
Hyperscale keeps the books and runs a company's Products. Providers hold the regulated money and make the lending, underwriting, credit, rating and claims decisions. Hyperscale never lends or holds funds.
| Layer, customer side first | What it does | Kubernetes analogue |
|---|---|---|
| Tenant apps and their customers | Call the operation API | Workloads |
| CLI, portals, MCP and SDK | Clients of that one API, with no authority of their own | kubectl, dashboards, generated clients |
| HSX program and UDL contract | Declare the Product's rules and record decisions | Manifest and stored API object |
| Hyperscale runtime | Admits operations; records accounts, holds, receipts and audit | Admission and API server |
| Product Build | The contract an agreement keeps | Resource revision |
| ADL adapters | Turn the contract into a provider's API and return evidence | Drivers and plugins |
| Providers | Hold money, perform the service, carry the risk | Machines |
Money moves once: each instruction has a durable identity and consumes provider evidence once. Every move conserves money within its book. A Build binds its providers; no scheduler picks one per move. Sandbox adapters report fixture outcomes, not real origination or custody.
Discover operations
| Source | Gives | Names operations as |
|---|---|---|
@hyperscale0/sdk |
createClient(...).call(name, input, options) and types |
Dispatch name, product.objects.create |
| /openapi.json | Method, route, parameters, body, validated examples | operationId is the wire name, product_objects_create; x-operation-name is the dispatch name |
| /.well-known/mcp.json | Every MCP tool | Wire name, product_objects_create |
| Runtime discovery | The Product's own kinds, fields and actions | Public action names from HSX |
Operations["product.objects.create"] in the SDK gives input, output,
error and errorCode; SharedErrorCode lists the codes every operation
shares. The types do not validate network data, so treat an unknown error as
an unknown outcome. The SDK retries reads, and retries a write only under an
idempotency key, which it makes when you pass none.
The HTTP API page owns the base URL, environments, idempotency, the error envelope, pagination and rate limits.
Who may call what
OpenAPI marks each operation with the credential it takes.
| Extension | Value | Means |
|---|---|---|
x-principal |
api_key |
A Product API key can call it once its Product has the capability |
x-principal |
user_session |
A Product key is refused; a person or an admitted token calls it |
x-personal-access-token-scope |
a scope | The scope a personal access token needs; null refuses such tokens |
x-customer-session |
allowed | A customer session token acts as that customer |
x-environment |
sandbox, live |
Which environments admit it |
| confirmation header | SHA-256 hex | Required on live requests: the hash of { name, arguments } |
An agent calls only what its grant and the agent policy admit, and each call
also checks the delegating person's current permissions. Sandbox work inside
the grant runs at once; live work waits for browser approval.
Sign in covers grants, approval and
agent setup; access
covers Product scope from the agent's side.
MCP results have their own shape:
| Result | Fields |
|---|---|
| Success | status, output, operationId, receiptId, traceId (the IDs may be null) |
| Failure | status, error, and the MCP result sets isError |
A mutation tool takes idempotencyKey; send one to keep control of retries.
rehearse: true runs it with no effect and answers the rehearsal as output,
with null operationId and receiptId.
A tool that needs confirmation advertises the exact value in its schema. No
MCP tool returns a credential except the webhook signing secret: key_create
and api_key_sandbox_create answer with
hyperscale key create --product <productId> --name <name>, and live keys come
only from the founder with hyperscale key create-live.
List shapes
Lists page with cursor and nextCursor as the HTTP API page says. Read each
operation's schema for its limit and default. Most lists return items; these
do not:
| Read | Collection field |
|---|---|
| Team members | members |
| Team roles | roles |
| Sessions | sessions |
| Object action discovery | actions and evidence |
| Account holds | items, plus hasMore and observedAt |
Product objects and actions
A Build defines object kinds, their metadata, attachments and public actions. Creating an object stores metadata only: it opens no agreement and moves no money.
| Operation | Route |
|---|---|
product.objects.discover |
GET /v1/products/{productId}/objects |
product.objects.create |
POST /v1/products/{productId}/objects/{kind} |
product.objects.list |
GET /v1/products/{productId}/objects/{kind} |
product.objects.retrieve |
GET /v1/products/{productId}/objects/{kind}/{objectId} |
product.objects.actions |
GET /v1/products/{productId}/objects/{kind}/{objectId}/actions |
product.objects.agreements.list |
GET /v1/products/{productId}/objects/{kind}/{objectId}/agreements |
product.objects.agreements.evidence.list |
GET /v1/products/{productId}/objects/{kind}/{objectId}/agreements/{instanceId}/evidence |
product.objects.timeline.list |
GET /v1/products/{productId}/objects/{kind}/{objectId}/timeline |
product.objects.preview |
POST /v1/products/{productId}/objects/{kind}/{objectId}/actions/{action}/preview |
product.objects.execute |
POST /v1/products/{productId}/objects/{kind}/{objectId}/actions/{action} |
The same steps from the CLI, with a Product key in HYPERSCALE_API_KEY:
hyperscale object kinds
hyperscale object create <kind> --fields-file fields.json
hyperscale object actions <kind> <object-id>
hyperscale object run <kind> <object-id> <action> --input-file input.json
The CLI takes and prints money in major units; REST and MCP take minor-unit strings. HSX money has the table.
Calling an action. kind, objectId and action are enough. Execute and
preview fill in expectedRevision, productBuildId, digest and target
from the record when you leave them out, and check them when you send them.
The CLI's --attachment and --instance pick between agreements that offer
the same action. api call reaches every other published route.
Acting for a customer. An object a Product key creates without
onBehalfOf belongs to the Product, and money the Product pays itself is
never an earning. When a customer pays, create the object and run the payer's
steps with onBehalfOf naming that customer. A Product key, a member with
desk.products.objects_write, or an agent inside its grant may send
onBehalfOf on product.objects.create, actions, execute and preview. The
call uses the customer's accounts, keeps the operator's view of records and
records both identities.
Money and commits. An action checks input, authority and requirements,
then commits its intent; ledger effects follow with durable recovery. A
refused admission moves nothing; an uncertain outcome is not a refusal. The
acting party pays from its cash account, which for a Product key is the
Product pool, so fund the pool before the first money action. A completed
action lists its transfers in outcome.moves, each with transferId, amount
and accounts. Object schemas and action availability come from runtime
discovery, never from the SDK. Admission counts each action's worst case,
its own transfers plus every action it invokes, and refuses a Product create,
compose or recompose with an action over 253 as execution_budget_exceeded,
naming the action and its count. Std lending blocks end a payoff, default or
refinance in one transfer per party and money purpose, and walk long lists in
batches across clock steps, so a 360-month mortgage stays under the limit.
| Refusal | When | Details |
|---|---|---|
409 insufficient_balance |
The paying account is short. The action list and preview show the same blocker first. | accountId, available, required, shortfall in minor units |
409 target_needs_instance |
A non-create action names an attachment that already has an open agreement. Nothing opens. |
The open instances and the exact target to send |
409 state_conflict, transfer_limit |
One action would post more than 253 transfers, the ledger's limit per request. Nothing posts. Only a Build admitted before the 253 check can reach it. | limit: 253. Split the work. |
400 invalid_request, list_position |
A list position past the end, sent in this request | Fix the request |
409 state_conflict, list_position |
The same, on a list saved earlier | Correct the stored record first |
Customers and sandbox money
A customer belongs to one company and enrols in one Product.
customer.create (POST /v1/customers) takes
{ productId, entity: { kind, displayName, country } }. A new customer's
entity and access start pending, and money stays refused until a legal profile
clears. In sandbox, test: true fills a fake profile and activates access in
the same call; live refuses test. A test customer runs no identity check, so
reads show entityStatus not_checked and identity test.
A customer can also customer.signup and customer.login for a session
token. Sign-up needs a browser challenge, so a script acts for a customer with
its Product key and onBehalfOf.
Your own app signs its customers up and in. It calls customer.signup
(POST /v1/customer-identity/signup) or customer.login
(POST /v1/customer-identity/login) with the productId, and gets back
sessionToken and sessionExpiresAt. Signup leaves the entity and the
enrolment pending: no account opens and no money moves until the entity clears
and access is active. The session belongs to that Product. Send it as
Authorization: Bearer on the customer's own calls, honour its expiry, and
end it with customer.logout (POST /v1/customer-identity/logout).
Every sandbox account starts at zero. sandbox.account.fund
(POST /v1/sandbox/account-funding) takes
{ destinationAccountId, amount, currency }, with amount in minor units:
"10000" is SAR 100.00. A Product key or a session with transfer:write calls
it, never in live. The key names the company, so a key call that also sends
tenantId gets invalid_request. The console's Add test money
button and the CLI's fund call it in major units.
hyperscale customer create "Noura Alharbi" --test
hyperscale account open <customer-id>
hyperscale fund <account-id> 500
account.deposit (POST /v1/account-deposits) takes
{ destinationAccountId, adapter, amount, currency } and runs in sandbox and
live. The adapter delivers the deposit and returns its credit reference as
bankReference, and the account is credited from the company's bank mirror,
so the finance report stays complete. The fixture_bank adapter moves fake
money. In live it answers only on an estate that sets
HYPERSCALE_LIVE_MONEY=fixture; otherwise the call gets
capability_not_configured.
Both calls store the bank's credit as a provider fact on the transfer:
provider fixture_bank, reference credit:<transferId>, with the amount and
value date. A bank statement that lists the top-up then matches it, by that
reference or by amount and value date, so a sandbox company rehearses
reconciliation against its own funding.
Sandbox clock
Each sandbox Product has its own clock. sandbox.clock.advance
(POST /v1/sandbox/clock/advance, CLI hyperscale clock advance 3d) moves it
forward to at; release: true (hyperscale clock reset) returns it to wall
time from any point. Records stamped while the clock ran ahead keep their
stamps.
What an advance does. It runs every clock action due by at before it
answers, and lists them in ran: instrument, action, objectId,
instanceId, dueAt, amount for money, status, and reason when
refused. Each piece's transfer carries its own dueAt as its value date, and
one transaction packs one calendar month (UTC), so a two-month jump files each
month's dues in that month, in the books, activity and statements. A piece
that waited on an acceptance or another write to its own agreement posts at
that write; a write to another agreement never moves it. A retried charge
posts at the Product clock.
| Limit | Value |
|---|---|
| Clock actions per transaction | 366 |
| Transactions per advance | 1,000, then the maintenance sweep takes over |
| Declared transfers per transaction | 253, so a plan far behind catches up 253 at a time |
| Maintenance sweep | Every 60 seconds |
In live, and on a sandbox Product whose clock reads wall time, a piece runs
within a minute of its dueAt with no request.
Refused clock work. A refusal is either retryable or lasting.
| Refusal | Retries | How it clears |
|---|---|---|
insufficient_balance |
Yes. ran carries retries: true, accountId, required, shortfall |
Fund the account. The sweep tries about once a minute and the next advance tries first. In sandbox, any action on the record tries it first; in live only the sweep does. |
Ledger rejection after a competing spend, a concurrent write, clock_work_pending from other clocks |
Yes | Same as above |
| Anything the action itself decides, such as a broken reference or terms that no longer hold | No. work list shows it under needs_human; schedule list shows refused, retries: false |
Fix the cause, then run any action or write on the record. It retries the refused clock first. |
Before a caller's action runs, the Product's due clock work runs. If that work
cannot finish, the action returns 409 state_conflict with details.reason
clock_work_pending and changes nothing: either 8 scheduled updates were
refused in that request and each is now set aside with its reason, or the
Product is further behind than one request catches up. Retry: each attempt runs
more of the due work.
Piece states in schedule list.
| State | Means |
|---|---|
waiting |
Due, but its record is in another state, such as a draft schedule. Accepting the schedule collects every piece due, once each. |
refused |
A charge that failed. Only charges are ever refused. |
completed |
Posted, with its amount, shown beside the next piece |
cancelled |
The schedule or agreement was cancelled or stopped. It never runs. |
waived |
The payee forgave it |
ended |
The record finished another way, such as completed or expired |
A start date written as today's date, such as startsAt: "2026-10-10", means
now on the Product clock, so accepting a schedule that starts today collects
its first piece in the same request. work list holds only what is due or
refused and waits for a person; an action with no due date, such as ending a
membership, stays an option on the record. A due date on any other action only
makes it runnable, and a passed deadline refuses it. work list --staff-role credit_underwriter keeps only the decisions authored for that staff role, with
the customer, amount and request time, and needs a role that grants
credit:underwrite.
Which time a read shows. Sandbox reads date money on the Product clock:
money summary, account balance, statements, the finance report, margin,
treasury and block reads, activity, the request log and object timelines.
asOf, occurredAt and a statement period are Product time; a host time is
observedAt (recordedAt on a log row). In live the two are the same.
Refunds and returns
refund.create (POST /v1/refunds, CLI hyperscale transfer refund) gives a
posted payment into a Product's pool back to the account that paid it, over
the same two accounts. It takes { tenantId, paymentTransferId }, an optional
amount in minor units for part of it, and an optional reason: duplicate,
fraudulent or requested_by_customer. A founder session calls it; a Product
key and a personal access token cannot. A full refund first gives back the
earn-rate fees the Product took; a partial one keeps them.
hyperscale transfer refund --payment-transfer-id <transfer-id> --amount 25
| Refusal | When |
|---|---|
409 transfer_lineage_conflict |
The payment already has a refund, or was reversed or returned. details.existingTransferId names it. |
409 transfer_return_exists |
A return is in progress |
400 transfer_adjustment_amount_invalid |
amount is above the payment |
transfer_return.create opens a return and moves no money until it completes.
waitsFor says what completes it: sandbox_clock_advance in sandbox, where
the next advance or the minute sweep completes every open return, and
provider_settlement in live. A transfer with a refund or reversal takes no
return.
Team invitations and API keys
These are company operations for people signed in to the portals, so
/openapi.json, which lists the Product API and the routes a CLI login or
agent token calls, does not list them. Reach them with
the CLI (hyperscale key list), MCP or a browser session.
team.member.listreturns{ teamId, members, nextCursor? }.team.invitation.list(GET /v1/team/invitations) takes{ tenantId, cursor?, limit? }and returns{ teamId, items, nextCursor? }. Items haveinvitationId,email,role,invitedByUserId,expiresAtandcreatedAt; only pending, unexpired invitations appear, oldest first, then by ID descending.limitis 1 to 100, default 50. The cursor binds environment, tenant and team, and a foreign one returnsinvalid_request. It needs the same team read permission as the member list. Neither read returns invitation tokens.api_key.listandapi_key.retrieveitems carryexpiresAtandlastUsedAt, an ISO UTC time ornull. A use is recorded at most once every five minutes, so a key used a minute ago can show an older time.