# External wallets

> Connect a dedicated Nodaro Cloud deployment to your own shared wallet, which reserves, settles and reports each customer's credits across all your products.

Source: https://nodaro.ai/docs/developers/external-wallet

An **external wallet** is a service you run that holds each customer's shared budget, and that a dedicated Nodaro Cloud deployment consults before it spends. Nodaro asks the wallet to reserve credits before each generation, settles the exact amount afterwards, and reads the balance to show it to the customer. The deployment's billing account still pays for platform usage from its prepaid pool; the wallet decides how much of that each customer may use, across all your products.

## How it fits together

- **A billing account pays.** The deployment names one billing account (`billing.payerAccount` in its surface profile), whose prepaid pool pays for every action on the deployment. See [Editions and surface profiles](https://nodaro.ai/docs/self-hosting/editions-and-profiles).
- **SSO identifies the customer.** Customers sign in through the deployment's [SSO provider](https://nodaro.ai/docs/developers/sso). The provider's subject for a customer, sent as `sso_subject`, is how your wallet recognizes the customer.
- **The wallet decides.** For every single-node request and every workflow node, Nodaro checks the billing account's prepaid pool, then asks your wallet to reserve against the customer's shared budget. The customer's local Nodaro credit balance plays no part.

An empty prepaid pool can stop a request before the wallet is asked. Once asked, the wallet's decision is final for the customer's shared budget.

### Reserve

A customer starts a generation. Nodaro sends `reserve` with an `operation_id` and an amount, and your wallet holds that amount atomically and answers `allow` or `deny`.

### Run

On `allow`, the model runs. On anything else, nothing is sent to the model.

### Settle

When the work ends, Nodaro sends `settle` with the actual amount. Your wallet charges exactly that amount and releases the rest of the hold.

### Show the balance

The studio asks your wallet for the customer's `balance`, and shows it in the deployment's display unit.

## Configure the deployment

Set these variables on the API and on the workers:

| Variable | What it sets |
| --- | --- |
| `DEPLOYMENT_WALLET_URL` | The wallet's endpoint. It must use HTTPS. |
| `DEPLOYMENT_WALLET_TOKEN` | A dedicated server secret that Nodaro sends as a bearer token. |
| `DEPLOYMENT_WALLET_SSO_PROVIDER` | The id of the SSO provider whose subjects identify customers. |
| `DEPLOYMENT_WALLET_TIMEOUT_MS` | The time limit of each wallet call, from 1000 to 30000 milliseconds. The default is 10000. |

- **Set the first three together.** Leave all three unset to keep the deployment's existing billing.
- **Turn off local allowances.** The per-user allowance enforcement, `billing.allowances: "enforce"`, must be off. A configuration that combines the two is refused at activation.
- **Agree on the unit first.** Before you activate the wallet, agree on the conversion between Nodaro credits and your product's budget. Set `billing.unitRate` and `billing.unitLabel` in the surface profile to match, so both products show the same figure.

## The request contract

Nodaro sends every call as a `POST` with a JSON body that carries `contract: 1` and `unit: "nodaro_credit"`, and the header `Authorization: Bearer <token>`. The amounts are Nodaro credits. Never read them as your own product's units without an explicit conversion that your wallet owns.

### Reserve

```json
{
"contract": 1,
"unit": "nodaro_credit",
"action": "reserve",
"operation_id": "a3354131-4e5e-43a6-8f61-3914f09c658e",
"job_id": null,
"user_id": "5bf0d884-47b1-468e-a7b2-2433f957b267",
"sso_provider": "partner",
"sso_subject": "customer-123",
"model_identifier": "example-model",
"reserved_credits": 30
}
```

| Field | Meaning |
| --- | --- |
| `operation_id` | One usage record. A job or a workflow can contain several. |
| `sso_subject` | The customer. It comes from trusted sign-in data that Nodaro's server manages, so use it to identify the customer. |
| `sso_provider`, `user_id` | The SSO provider and the Nodaro user behind the request. |
| `model_identifier` | The model being billed. |
| `reserved_credits` | The whole number of credits to hold. |

Reserve the amount atomically against the customer's shared available budget before you answer. To allow the request:

```json
{"contract":1,"unit":"nodaro_credit","operation_id":"a3354131-4e5e-43a6-8f61-3914f09c658e","decision":"allow","reserved_credits":30}
```

For insufficient funds or an account that may not spend, answer HTTP `200` with the same envelope and `"decision": "deny"`; no amount is needed. Answer a service error with a status outside the `2xx` range.

Nothing reaches the model unless your wallet allows it. A timeout, a malformed answer, a mismatched amount or id, a missing identity and a denial all stop the request. The request's `Idempotency-Key` header is `<operation_id>:reserve`.

### Settle or release

When the work ends, Nodaro sends the same operation fields with `"action": "settle"` and `actual_credits`. Charge exactly that amount and release the rest of the hold. A settle of `0` releases everything.

```json
{"contract":1,"unit":"nodaro_credit","operation_id":"a3354131-4e5e-43a6-8f61-3914f09c658e","settled":true,"actual_credits":12}
```

- **The key is `<operation_id>:settle`.** Delivery is at least once. A retry must return the original result without charging again.
- **Refuse conflicts.** Reject a request for the same operation with a different identity, reservation ceiling or final amount.
- **A settle can arrive before a delayed reserve.** A settle of `0` must create a permanent final record, even for an operation you do not know yet. A later reserve must never reopen it.
- **Holds never expire.** A generation, and a human review of its result, can outlive any HTTP request. Keep each hold until it is settled, and keep your idempotency records. Never release an active hold because a callback is late.

### Balance

```json
{"contract":1,"unit":"nodaro_credit","action":"balance","user_id":"5bf0d884-47b1-468e-a7b2-2433f957b267","sso_provider":"partner","sso_subject":"customer-123"}
```

```json
{"contract":1,"unit":"nodaro_credit","sso_subject":"customer-123","available_credits":250}
```

- **Available means after holds.** `available_credits` excludes every outstanding hold, across all your products.
- **Fractions are allowed here.** The balance may be fractional, so a remainder smaller than one billable credit can still be shown. Reserve and settle amounts are always whole credits.
- **One figure in the studio.** The studio converts the balance once into the deployment's display unit and hides its own per-user allowance display. An unavailable balance appears as unknown.
- **Reserve decides races.** When requests arrive at the same moment, the reserve call, not the balance, is authoritative.

## Delivery and recovery

- Nodaro tries to deliver each settlement at once. If that fails, a worker that runs every minute retries it, with a backoff of up to one hour. Unacknowledged settlements are kept until they are delivered.
- Nodaro cancels on its own only the reservations that were never authorized. Authorized jobs that are running follow the normal job recovery.
- **Never remove the wallet configuration, or point it to another wallet, while operations remain open.** To rotate the token, rotate it on the same wallet.

## Price discovery

`GET /v1/deployment-billing/pricing` returns the deployment's price list for your wallet's own display and planning. It needs the billing account's session or one of its billing integration keys.

| Field | Meaning |
| --- | --- |
| `creditCost` | The base tariff of a model. |
| `creditIdentifier` | The identifier that is billed. |
| `pricingBasis` | What the base tariff assumes. |
| `pricing` | For text models, the identifiers of each operation. Text models are listed as `llm-chat` at default settings. |
| `denomination` | The credit unit and the display conversion. |
| `purchaseOptions` | The standard top-ups in USD, with their effective price per credit. Separate commercial terms may apply. |

- Media variants and text operations resolve through the prices in effect. A variant without a configured price stays `null`, with an error code.
- Settings, quantity, duration and measured usage can change the final charge. Account with the reserve and settle amounts, never with a base tariff from this list.
- To detect changes, prefer a full request with `If-None-Match`. The `since` parameter follows the timestamps of price rows, and cannot see every change of runtime configuration.

## Frequently asked questions

### What is an external wallet in Nodaro?

A service you run that holds each customer's shared budget across your products. A dedicated Nodaro Cloud deployment asks it to reserve credits before a generation runs, settles the actual amount afterwards, and reads the balance to show it.

### Which Nodaro deployments can use an external wallet?

A dedicated Nodaro Cloud deployment with a billing account, the one account that pays for every action on the deployment. Its customers sign in through the deployment's SSO provider, which identifies them to the wallet.

### What happens if the wallet does not answer in time?

Nothing is generated. A timeout, a malformed answer, a mismatched amount or id, a missing identity and a denial all stop the request before any model runs.

### Can the same settle request arrive twice?

Yes. Delivery is at least once, so your wallet must return the original result for a repeated settle without charging again. The Idempotency-Key header identifies repeats.

### Do reservations expire?

No. A generation and a human review can take longer than any HTTP request, so a hold stays until Nodaro settles it. Never release an active hold because a callback is late.
