> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

Asset instructions run as a separate service next to the Prividium API. The API authenticates callers and forwards
each request, with the caller's identity in a signed envelope, to the service; the service accepts, orders, authorizes,
signs and confirms instructions against the platform database and the chain.

## Prividium API

| Variable                         | Purpose                                                                                                                          |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `ASSET_INSTRUCTION_PROXY_SECRET` | HMAC secret for the signed identity envelope; at least 32 characters and the same value as the service's `INTERNAL_PROXY_SECRET` |
| `ASSET_INSTRUCTION_DEPLOYMENT`   | JSON object naming the zone's deployment: `{ organizationId, logicalAssetId, assetInstructionApiUrl, sourceBindings }`           |

One zone serves one deployment. `sourceBindings` maps application or user IDs to the source systems they may submit or
correct for. Without `ASSET_INSTRUCTION_DEPLOYMENT` the API mounts no `/api/asset-instructions` route; with it set, the
API refuses to boot without `ASSET_INSTRUCTION_PROXY_SECRET`.

```json
{
  "organizationId": "10000000-0000-4000-8000-000000000001",
  "logicalAssetId": "tokenized-cash",
  "assetInstructionApiUrl": "http://asset-instruction-api:4011",
  "sourceBindings": {
    "<ledger application id>": ["LEDGER"]
  }
}
```

## Service

One zone serves one deployment: one service instance for one organization, one logical asset and one contract, on the
zone's platform database. At startup the service applies its own migration — the tables `asset_instructions`,
`asset_instruction_attempts` and `asset_instruction_execution_hold` — after the core's, under an advisory lock so replicas may boot together. It refuses to
start until the core migrations have run, when `PRIVIDIUM_RPC_URL` serves another chain than `CHAIN_ID`, and in
Fireblocks mode when a vault account holds no address equal to its configured signer address.

The service accepts only requests carrying the API's signed identity envelope, and refuses a second copy of an envelope
it has already accepted. That replay protection is per process: another replica, or the same one after a restart,
accepts a captured envelope once within its five-minute window. Keep the network between the API and the service
private; the Compose profile publishes no host port.

### Deployment

| Variable                     | Purpose                                                      | Default / requirement |
| ---------------------------- | ------------------------------------------------------------ | --------------------- |
| `NODE_ENV`                   | Runtime mode                                                 | Required              |
| `DATABASE_URL`               | Platform PostgreSQL database after the core migrations       | Required              |
| `ORGANIZATION_ID`            | The organization this deployment serves                      | Required              |
| `LOGICAL_ASSET_ID`           | The logical asset this deployment serves                     | Required              |
| `INSTRUCTION_PROFILE_PATH`   | Instruction profile file (see below)                         | Required              |
| `CHAIN_ID`                   | Expected chain ID                                            | Required              |
| `TOKEN_CONTRACT_ADDRESS`     | Deployed contract implementing `IControlledAsset`            | Required              |
| `PRIVIDIUM_RPC_URL`          | Authenticated Prividium RPC URL                              | Required              |
| `PRIVIDIUM_RPC_API_KEY`      | Application key of the deployment's organization             | Required              |
| `INTERNAL_PROXY_SECRET`      | HMAC secret shared with the API                              | Required              |
| `PORT`                       | Service port                                                 | `4011`                |
| `LOG_LEVEL`                  | Logging verbosity                                            | `info`                |
| `VERSION`                    | Version reported by `/health`                                | `local`               |
| `EXECUTION_POLL_INTERVAL_MS` | Stream polling interval                                      | `1000`                |
| `TRANSACTION_TIMEOUT_MS`     | Receipt wait timeout; a timeout leaves the outcome uncertain | `120000`              |

### Policy

| Variable                          | Purpose                                          | Default / requirement          |
| --------------------------------- | ------------------------------------------------ | ------------------------------ |
| `POLICY_API_URL`                  | Prividium API exposing the policy endpoints      | Required                       |
| `POLICY_SESSION_TOKEN`            | User session owning the ordinary signer wallet   | Required                       |
| `CORRECTION_POLICY_SESSION_TOKEN` | User session owning the correction signer wallet | Required to enable corrections |
| `CRYPTO_OPERATIONS_ROLE_ID`       | Approval role the correction policy requires     | Required for corrections       |

The service asks for policy decisions as the signer's user, so each session must belong to the deployment organization,
own the corresponding signer wallet and hold no administrative permissions. Its expiry or revocation stops execution.

### Signing

| Variable                                 | Purpose                               | Default / requirement               |
| ---------------------------------------- | ------------------------------------- | ----------------------------------- |
| `SIGNER_MODE`                            | `local` or `fireblocks`               | `local`; refused in production      |
| `LOCAL_SIGNER_PRIVATE_KEY`               | Ordinary signing key (development)    | Required in local mode              |
| `CORRECTION_LOCAL_SIGNER_PRIVATE_KEY`    | Separate correction key               | Required for local corrections      |
| `FIREBLOCKS_API_URL`                     | Custody REST base URL                 | `https://api.fireblocks.io`         |
| `FIREBLOCKS_API_KEY`                     | Custody API user key                  | Required in Fireblocks mode         |
| `FIREBLOCKS_API_SECRET_KEY`              | RSA PEM credential                    | Required in Fireblocks mode         |
| `FIREBLOCKS_VAULT_ACCOUNT_ID`            | Ordinary signer vault account         | Required in Fireblocks mode         |
| `FIREBLOCKS_ASSET_ID`                    | Asset identifying the EVM signing key | Required in Fireblocks mode         |
| `FIREBLOCKS_SIGNER_ADDRESS`              | Ordinary signer address               | Required in Fireblocks mode         |
| `CORRECTION_FIREBLOCKS_VAULT_ACCOUNT_ID` | Separate correction vault account     | Required for Fireblocks corrections |
| `CORRECTION_FIREBLOCKS_SIGNER_ADDRESS`   | Separate correction signer address    | Required for Fireblocks corrections |
| `FIREBLOCKS_SIGNING_TIMEOUT_MS`          | Custody wait timeout                  | `180000`                            |
| `FIREBLOCKS_POLL_INTERVAL_MS`            | Custody status polling interval       | `2000`                              |

On the contract, the ordinary signer must hold exactly `MINTER_ROLE` and `BURNER_ROLE` and the correction signer exactly
`CORRECTOR_ROLE`; the two must be different accounts. In Fireblocks mode the vault signs the transaction hash and the
service broadcasts; Fireblocks-side approvals apply as configured there. The signing request's note names the
instruction's sequence, kind, operation, amount, account and operation ID, and nothing a caller supplies.

## Instruction profile

The profile is the deployment's vocabulary: which account codes callers may name, which wallet key each maps to, which
source systems exist and which event types mean which operation. The service ships `profiles/tokenized-cash.json`:

```json
{
  "accounts": [
    { "code": "CLIENT-OMNIBUS", "wallet": "clientOmnibus" },
    { "code": "OPERATIONAL", "wallet": "operational" }
  ],
  "sources": [
    {
      "system": "LEDGER",
      "events": [
        { "type": "DEPOSIT", "operation": "mint" },
        { "type": "WITHDRAWAL", "operation": "burn" }
      ]
    }
  ]
}
```

`accounts[].code` is what callers send as `account`; `accounts[].wallet` is the key registered on the contract, carried
on chain as its ASCII `bytes32`, so `walletAddress(bytes32)` must resolve it. Account codes, wallet keys and source
systems are unique, and each event type appears once across all sources and maps to exactly one operation.

## Docker Compose

The `asset-instructions` profile of the repository's `docker-compose.yaml` runs the service image next to the API and
maps every service variable from `ASSET_INSTRUCTION_*`:

```bash
docker compose --profile asset-instructions up -d
```

The profile defaults to `NODE_ENV=production` and the Fireblocks signer, which refuses a local key. For a local run set
`ASSET_INSTRUCTION_NODE_ENV=development`, `ASSET_INSTRUCTION_SIGNER_MODE=local` and
`ASSET_INSTRUCTION_LOCAL_SIGNER_PRIVATE_KEY` in the repository's `.env`; the database defaults to the stack's own.
