---
title: "Money"
description: "The six commands that move or read money from a terminal: charge, payment-status, wallet, spend, transfer and ledger."
---

import { Callout } from "fumadocs-ui/components/callout";

Six commands in the `codespar` binary touch money. Two run against a charge you issue (`charge`, `payment-status`), three against a consumer's mandate wallet (`wallet`, `spend`, `transfer`), and one against the tenant's own double-entry books (`ledger`).

Every flag, argument, message and exit code below was run against `@codespar/cli` 0.10.0 — the version on npm. The blocks labeled `stdout` and `stderr` are the captured bytes of those runs, with the two streams kept apart; the ids and amounts in them are from a test project.

The argument schema of `codespar_charge` and `codespar_ledger` belongs to the server: the CLI checks a handful of fields and forwards the rest of your object untouched, so the field lists below are **the CLI's checks, not the tool's full schema**. A call the CLI accepts can still be refused upstream, with the tool's own message.

## Before any of them

Credentials and scope resolve the same way for all six: a command-line flag beats an environment variable, which beats `~/.codespar/config.json`. The flags are `--api-key <key>`, `--base-url <url>` and `--project <id>`; the variables are `CODESPAR_API_KEY`, `CODESPAR_PROJECT` and `CODESPAR_BASE_URL`. With nothing set, the base URL is `https://api.codespar.dev`. When no key resolves, each of the six refuses before touching the network:

```text title="stderr"
✗ Not logged in. Run `codespar login` or set CODESPAR_API_KEY.
```

`--json` is declared on the root command, and Commander accepts it on either side of the subcommand: `codespar --json wallet consumer_0000` and `codespar wallet consumer_0000 --json` printed the same document. Tables, key/value blocks and JSON go to stdout; the `✓`, `ℹ` and `⚠` lines go to stderr, so a pipe into `jq` gets the data alone.

### Two clients, and why failures look different

`wallet`, `spend` and `transfer` call the REST API through the CLI's own HTTP client. `charge`, `ledger` and `payment-status` go through the SDK, which opens a session first. The split is invisible while things work and decides everything when they fail: the HTTP client turns a non-2xx into a one-line CLI refusal, and the SDK throws an error type the CLI does not recognize as its own.

| Exit | What produced it | How it reads |
|---|---|---|
| `0` | The command finished, including a `--stream` that reached its terminal frame | The output of the command |
| `1` | A refusal the CLI raises itself, including any HTTP error on the `wallet` / `spend` / `transfer` path | `✗ <message>` on stderr |
| `1` | An argument error Commander raises before the command body runs | `error: <message>` on stderr, with no `✗` |
| `2` | Anything else | `✗ internal error:` and a stack trace on stderr |

<Callout type="warning">
Exit `2` is meant for our bugs, and three ordinary situations land there. **A meta-tool that answers `success: false`** exits `2`, because the typed wrapper throws a plain `Error` the CLI does not treat as a refusal: measured as `Error: charge failed: charge declined by provider` and `Error: ledger failed: ...`. **Any HTTP error on the SDK path** does the same: a `403` on session creation surfaced as `CodesparApiError: createSession failed: 403 ...` and exit `2` for `charge`, `ledger` and `payment-status` alike. **A `--input-file` path that does not exist** exits `2` with the `ENOENT` from `readFile`. A script that reads `2` as "page someone, the tool is broken" will page on a declined charge and on a missing scope.
</Callout>

The six split by direction. `charge` and `payment-status` pull money in, so they sit on the sell side. `spend` and `transfer` push it out, on the buy side, and `transfer` plans the move and sends nothing until you add `--execute`. `wallet` and `ledger` move nothing on a rail: `wallet` reads the caps and what is left, `ledger` writes to the tenant's own double-entry books.

<MoneyFlowDiagram side="sell" />

<MoneyFlowDiagram side="buy" />

## `codespar charge`

Opens a throwaway session with no server pinned, calls the `codespar_charge` meta-tool with the JSON object you pass, prints the result, and closes the session in a `finally`. The router picks the rail, so there is no `--server` to choose.

```bash
codespar charge (--input '<json>' | --input-file <path>) [--user <id>] [--json]
```

| Flag | Type | Required | What it does |
|---|---|---|---|
| `-i, --input <json>` | JSON object, inline | one of the two | The charge arguments |
| `-f, --input-file <path>` | path | one of the two | The same object, read from a file |
| `-u, --user <id>` | string | no | The session's user id. Default `cli-user` |

Five fields are checked in the CLI before any request leaves. `amount` must be a `number` in major units, so R$ 125.00 is `125`. `currency` must be a non-empty string. `method`, `buyer.name` and `description` must be present. `method` is checked for presence only and not against a list, so whatever you write is what the server receives; the refusal text names `pix | boleto | card`.

<Split min={380}>
<SplitPane label="Example">

```bash
codespar charge --input '{
  "amount": 150,
  "currency": "BRL",
  "method": "pix",
  "description": "Order 0000",
  "buyer": { "name": "Example Buyer", "email": "buyer@example.com" }
}'
```

</SplitPane>
<SplitPane label="Output">

```text title="stderr"
✓ charge chg_0000 → pending
ℹ Amount: 150 BRL  ·  method: pix
ℹ Charge URL: https://sandbox.example.com/i/chg_0000
ℹ Pix QR (truncated): iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAABi...
ℹ Pix copy-paste:
```

```text title="stdout"
00020126580014BR.GOV.BCB.PIX0136example-key5204000053039865802BR6009SAO PAULO62070503***6304ABCD
```

</SplitPane>
</Split>

In human mode the copy-and-paste string is the only thing on stdout, printed between two blank lines, so redirecting stdout captures the payable string and nothing else from the run. The QR line on stderr is cut at 40 characters and is not the payload. `Charge URL`, the QR line and the copy-and-paste block print only when the result carries those fields. With `--json`, stdout gets the whole result object and stderr stays empty.

**Calls** the meta-tool over the session execute route:

<Endpoint method="POST" path="/v1/sessions/{id}/execute" base="https://api.codespar.dev" mayMove="Resolves `tool` as a CodeSpar meta-tool first, then as a catalog tool" />

The tool's actions, its providers and the full argument schema are on the [codespar_charge page](/docs/concepts/meta-tools/charge); the route's own gates and responses are on [POST /v1/sessions/\{id\}/execute](/docs/api/reference/sessions#post-v1sessionsidexecute).

**Refusals**

| What you did | Message | Exit |
|---|---|---|
| Neither `--input` nor `--input-file` | ``charge requires --input '<json>' or --input-file <path>. Example: --input '{"amount":50,...}'`` | `1` |
| Both of them | `Pass either --input or --input-file, not both.` | `1` |
| JSON that parses to something other than an object | `--input must be a JSON object.`, or the file path in place of `--input` | `1` |
| Unparseable JSON | `--input is not valid JSON: <parser message>` | `1` |
| `amount` missing or not a number | `charge.amount must be a number (major units).` | `1` |
| `currency` missing | `charge.currency is required (e.g. BRL).` | `1` |
| `method` missing | `charge.method is required (pix \| boleto \| card).` | `1` |
| `buyer.name` missing | `charge.buyer.name is required.` | `1` |
| `description` missing | `charge.description is required.` | `1` |
| No credential resolved | ``Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |
| The API answered an error status | `✗ internal error:` then `CodesparApiError: createSession failed: <status> <body>` | `2` |
| The tool answered `success: false` | `✗ internal error:` then `Error: charge failed: <error>` | `2` |
| `--input-file` points at nothing | `✗ internal error:` then the `ENOENT` | `2` |

## `codespar payment-status`

Reads where the payment a tool call started got to. Without `--stream` it opens a session, reads the status once and closes it. With `--stream` it follows the server-sent event stream until the terminal frame, printing every update on the way.

```bash
codespar payment-status <tool-call-id> [--stream] [--timeout <ms>] [--user <id>] [--json]
```

| Argument or flag | Type | Required | What it does |
|---|---|---|---|
| `<tool-call-id>` | string | yes | The `tool_call_id` of the call whose payment you are tracking |
| `--stream` | boolean | no | Follow the stream instead of reading once |
| `--timeout <ms>` | integer, milliseconds | no | Bounds the stream wait. Default `600000`, set in the command body |
| `-u, --user <id>` | string | no | The session's user id. Default `cli-user` |

`--timeout` is validated on every run and applied only to `--stream`: `codespar payment-status tcl_0000 --timeout 0` is refused even with no stream requested. When the budget runs out, or you press Ctrl+C, the stream aborts and the command exits `1`.

<Split min={380}>
<SplitPane label="Example">

```bash
# read once
codespar payment-status tcl_0000

# follow until the terminal frame, capped at 30s
codespar payment-status tcl_0000 --stream --timeout 30000
```

</SplitPane>
<SplitPane label="Output of the single read">

```text title="stderr"
✓ payment_status: succeeded
ℹ tool_call_id: tcl_0000
ℹ original_status: success
ℹ idempotency_key: idm_0000
ℹ events: 1
```

</SplitPane>
</Split>

Nothing reaches stdout in human mode: those five lines are all stderr, and `--json` is what puts the envelope on stdout. `original_status` is the execute-time result and `payment_status` is the settlement state; they move independently, which is why both are printed. `idempotency_key` prints `-` when the call carried none. Under `--stream` without `--json`, each update becomes an `ℹ update #N: payment_status=... events=N` line on stderr before the same five-line ending.

<Callout type="warning">
`--stream --json` writes **more than one JSON document** to stdout: one per update, then the final envelope, which repeats the last update. A run with two updates printed three documents. A `jq` invocation that reads one value sees the first document and stops, so parse this with a streaming reader.
</Callout>

**Calls** one of two routes, depending on `--stream`:

<Endpoint method="GET" path="/v1/tool-calls/{id}/payment-status" base="https://api.codespar.dev" />

<Endpoint method="GET" path="/v1/tool-calls/{id}/payment-status/stream" base="https://api.codespar.dev" />

Both are in the HTTP reference: [GET /v1/tool-calls/\{id\}/payment-status](/docs/api/reference/tool-calls#get-v1tool-callsidpayment-status) and [the stream](/docs/api/reference/tool-calls#get-v1tool-callsidpayment-statusstream), including how the correlation key decides whether anything can be found at all.

**Refusals**

| What you did | Message | Exit |
|---|---|---|
| No tool-call id | `error: missing required argument 'tool-call-id'` | `1` |
| `--timeout 0`, or any non-positive value | `--timeout must be a positive integer (milliseconds).` | `1` |
| The stream ran past `--timeout`, or Ctrl+C | `payment-status stream timed out or was aborted.` | `1` |
| No credential resolved | ``Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |
| The API answered an error status | `✗ internal error:` then `CodesparApiError: createSession failed: <status> <body>` | `2` |

## `codespar wallet`

Reads a consumer's unified wallet and prints one row per currency: what the agent is authorized to spend, what it has spent, and what is left. Caps are per currency and nothing converts between them.

```bash
codespar wallet <consumer> [--json]
```

| Argument | Type | Required | What it does |
|---|---|---|---|
| `<consumer>` | string | yes | The consumer id, percent-encoded into the path |

The command declares no flags of its own; only the root flags apply.

<Split min={380}>
<SplitPane label="Example">

```bash
codespar wallet consumer_0000
```

</SplitPane>
<SplitPane label="Output">

```text title="stdout"
currency  rail          authorized  spent  available
BRL       pix-consent   50000       12500  37500
USDC      usdc-onchain  10000       12500  -2500 (overspent)
```

```text title="stderr"
ℹ Amounts are in minor units (cents / micro-USDC). Caps are per-currency, no FX. A negative available marked (overspent) means settled debits exceed the cap.
```

</SplitPane>
</Split>

`available` is `authorized` minus `spent` and is not floored at zero. When settled debits exceed the cap, the cell carries the literal suffix `(overspent)` next to the negative number, so a minus sign is not the only signal. A consumer with no active slots prints a hint instead of a table, on stderr, and still exits `0`:

```text title="stderr"
ℹ No active mandate slots for consumer consumer_empty.
  Create one with: codespar mandate create --consumer consumer_empty --slot USDC:usdc-onchain:100:100 ...
```

**Calls**

<Endpoint method="GET" path="/v1/consumers/{id}/wallet" base="https://api.codespar.dev" />

Field meanings and the full response are on [GET /v1/consumers/\{id\}/wallet](/docs/api/reference/consumers#get-v1consumersidwallet); the slot model and the current rail status are under [Consumer wallets](/docs/api/reference/wallets#consumer-wallets-multi-slot-mandate-wallet).

**Refusals**

| What you did | Message | Exit |
|---|---|---|
| No consumer id | `error: missing required argument 'consumer'` | `1` |
| The API answered an error status | `GET /v1/consumers/<id>/wallet → 403: <message from the body>` | `1` |
| No credential resolved | ``Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |

## `codespar spend`

Executes a payment inside an existing signed mandate. The CLI sends three values and reads none of the answer: the backend reconstructs the stored mandate, runs the cap and allowlist gates, and picks the rail from the shape of the payee.

```bash
codespar spend --mandate <id> --payee <payee> --amount <minor> --agent <id> [--json]
```

| Flag | Type | Required | What it does |
|---|---|---|---|
| `-m, --mandate <id>` | string | yes | The consumer mandate id, percent-encoded into the path |
| `-p, --payee <payee>` | string | yes | Sent unparsed as `payee` |
| `-a, --amount <minor>` | positive integer | yes | Minor units. Sent as `amount_minor` |
| `--agent <id>` | string | yes | Sent as `agent_id` |

All four are required options, so Commander refuses a missing one before the command body runs. The CLI does not inspect `--payee`: it forwards the string, and the rail is chosen server-side. The command's source records the mapping it expects, a Pix key or copy-and-paste string to Pix, an EVM address to a USDC transfer, an `http(s)` URL to x402; the route reference is where that behaviour is documented and kept honest.

<Split min={380}>
<SplitPane label="Example">

```bash
codespar spend \
  --mandate cm_0000 \
  --payee vendedor@example.com.br \
  --amount 2500 \
  --agent agt_0000
```

</SplitPane>
<SplitPane label="Output">

```text title="stderr"
✓ spend executed
ℹ The on-chain tx / receipt is in the response below:
```

```text title="stdout"
{
  "spend_id": "spd_0000",
  "mandate_id": "cm_0000",
  "amount_minor": 2500,
  "rail": "pix",
  "status": "settled",
  "provider_tx_id": "pix_0001",
  "receipt_id": "rcpt_0000"
}
```

</SplitPane>
</Split>

The response body goes to stdout in both modes. `--json` only suppresses the two stderr lines, so a script parses the same stdout either way. The fields in that block are whatever the route returned; the CLI prints the body as it arrives.

**Calls**

<Endpoint method="POST" path="/v1/consumers/mandates/{id}/spend" base="https://api.codespar.dev" money="Moves money out under a mandate already projected into this organization" />

The gates that refuse this call and the meaning of each refusal are on [POST /v1/consumers/mandates/\{id\}/spend](/docs/api/reference/consumer-mandates#post-v1consumersmandatesidspend).

**Refusals**

| What you did | Message | Exit |
|---|---|---|
| Omitted any of the four flags | `error: required option '--agent <id>' not specified`, and the same shape for the others | `1` |
| `--amount 0`, a negative, or a decimal | `--amount must be a positive integer in minor units (cents).` | `1` |
| The API answered an error status | `POST /v1/consumers/mandates/<id>/spend → 403: <message from the body>` | `1` |
| No credential resolved | ``Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |

## `codespar transfer`

Moves value between two currency slots of one consumer's wallet. There is no FX inside the wallet, so a cross-currency move is a real trade through the ramp. Without `--execute` the call plans the move and moves nothing.

```bash
codespar transfer <consumer> --from <currency> --to <currency> --amount <minor> [--execute] [--agent <id>] [--purpose <text>] [--json]
```

| Argument or flag | Type | Required | What it does |
|---|---|---|---|
| `<consumer>` | string | yes | The consumer id, percent-encoded into the path |
| `--from <currency>` | string | yes | Sent as `from_currency` |
| `--to <currency>` | string | yes | Sent as `to_currency` |
| `--amount <minor>` | positive integer | yes | Sent as `amount_minor`, in the **source** currency's minor units |
| `--execute` | boolean | no | Adds `execute: true` to the body. Omitted, the body carries no `execute` at all |
| `--agent <id>` | string | no | Sent as `agent_id`, only when given |
| `--purpose <text>` | string | no | Sent as `purpose`, only when given |

The human output has two shapes, and the flag is not what chooses between them: the CLI branches on the response's `executed` field. A plan prints a key/value block headed by `Transfer plan for <consumer>`; an execution prints `transfer executed (<route>)` and a different set of rows.

<Split min={380}>
<SplitPane label="Plan, then execution">

```bash
codespar transfer consumer_0000 --from BRL --to USDC --amount 15000

codespar transfer consumer_0000 --from BRL --to USDC --amount 15000 --execute
```

</SplitPane>
<SplitPane label="Output of each">

```text title="stdout (plan)"
route       onramp
converts    yes — via transfero at the real rate (no FX)
executable  yes
from        BRL/pix-consent (cap left 37500)
to          USDC/usdc-onchain
amount      15000 (minor)
```

```text title="stdout (--execute)"
consumer      consumer_0000
route         onramp via transfero (real rate, no FX)
onramp tx     ftx_0000
status        processing
destination   0x0000000000000000000000000000000000000000
source debit  pix_0000 (settled)
```

</SplitPane>
</Split>

On the plan run, stderr carried `ℹ Transfer plan for consumer_0000:`, the response's own `note`, and the ready-made command line to run it for real. On the execute run it carried `✓ transfer executed (onramp)` and, because that response named a `settle_via` path, a line saying the credit settles asynchronously and pointing at `codespar wallet <consumer>` to watch it. A response carrying `source_debit_error` adds a `⚠` line saying the source debit did not complete; the `source debit` row and the async line both depend on fields the response may omit.

**Calls**

<Endpoint method="POST" path="/v1/consumers/{id}/wallet/transfer" base="https://api.codespar.dev" money="With --execute the CLI adds execute: true, which the flag's own help describes as running the ramp legs with real money" />

The body, the plan-versus-execute contract, the matrix of which routes execute today and the error codes for the rest are under [Consumer wallets](/docs/api/reference/wallets#consumer-wallets-multi-slot-mandate-wallet).

**Refusals**

| What you did | Message | Exit |
|---|---|---|
| No consumer id | `error: missing required argument 'consumer'` | `1` |
| Omitted `--from`, `--to` or `--amount` | `error: required option '--amount <minor>' not specified`, and the same shape for the others | `1` |
| A decimal, zero or negative `--amount` | `--amount must be a positive integer in minor units (cents).` | `1` |
| The API answered an error status | `POST /v1/consumers/<id>/wallet/transfer → <status>: <message from the body>` | `1` |
| No credential resolved | ``Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |

## `codespar ledger`

Posts a double-entry transaction, reads an account's balances, or creates an account, against the tenant's own ledger. Like `charge`, it opens a throwaway session with no server pinned, calls the `codespar_ledger` meta-tool, and closes the session in a `finally`.

```bash
codespar ledger (--input '<json>' | --input-file <path>) [--user <id>] [--json]
```

| Flag | Type | Required | What it does |
|---|---|---|---|
| `-i, --input <json>` | JSON object, inline | one of the two | The ledger arguments |
| `-f, --input-file <path>` | path | one of the two | The same object, read from a file |
| `-u, --user <id>` | string | no | The session's user id. Default `cli-user` |

The CLI accepts three actions and checks one or three fields for each, before the network:

| `action` | What the CLI also requires | The refusal when it is missing |
|---|---|---|
| `entry` | `asset`, a non-empty `source` array, a non-empty `destination` array | `ledger.asset is required when action=entry.`, `ledger.source must be a non-empty array when action=entry.`, `ledger.destination must be a non-empty array when action=entry.` |
| `balance` | `account` | `ledger.account (id) is required when action=balance.` |
| `account` | `asset` | `ledger.asset is required when action=account.` |

Anything else is refused with `ledger.action must be one of: entry, balance, account.`, including the receipt-reading actions that the tool's TypeScript argument type carries: the CLI's list is narrower than the type's, measured by sending `action: "receipt"` and getting that refusal. Every field outside that table goes to the server unchecked.

<Split min={380}>
<SplitPane label="Example">

```bash
codespar ledger --input '{
  "action": "entry",
  "asset": "BRL",
  "source": [{ "account": "@external/BRL", "amount": 12500 }],
  "destination": [{ "account": "@wallet/user_0000", "amount": 12500 }],
  "description": "top-up"
}'
```

</SplitPane>
<SplitPane label="Output">

```text title="stderr"
✓ ledger entry → APPROVED
ℹ Id: txn_0000
```

</SplitPane>
</Split>

`Id`, `Account` and `Alias` print only when the result carries them, and an `entry` writes nothing to stdout. `action: "balance"` is the one shape that does: the balances are dumped there as indented JSON, under an `ℹ Balances:` line on stderr. With `--json`, stdout carries the whole result and stderr stays empty.

**Calls** the meta-tool over the same session execute route as `charge`:

<Endpoint method="POST" path="/v1/sessions/{id}/execute" base="https://api.codespar.dev" mayMove="Resolves `tool` as a CodeSpar meta-tool first, then as a catalog tool" />

The actions, the ledger behind them and the full argument schema are on the [codespar_ledger page](/docs/concepts/meta-tools/ledger).

**Refusals**

| What you did | Message | Exit |
|---|---|---|
| Neither `--input` nor `--input-file` | ``ledger requires --input '<json>' or --input-file <path>. Example: ...`` with a ledger example | `1` |
| Both of them, or JSON that is unparseable or not an object | The same three messages as `charge` | `1` |
| An action outside the three | `ledger.action must be one of: entry, balance, account.` | `1` |
| A field the action requires is missing | The message in the action table above | `1` |
| No credential resolved | ``Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |
| The API answered an error status | `✗ internal error:` then `CodesparApiError: createSession failed: <status> <body>` | `2` |
| The tool answered `success: false` | `✗ internal error:` then `Error: ledger failed: <error>` | `2` |

## Next

<NextStepsGrid items={[
  { label: "CLI", title: "Commands", description: "The rest of the surface: catalog, sessions, connections, scaffolding.", href: "/docs/cli/commands" },
  { label: "META-TOOL", title: "codespar_charge", description: "Every action and field the charge tool accepts, and what each provider returns.", href: "/docs/concepts/meta-tools/charge" },
  { label: "API", title: "Consumer wallets", description: "The slot model, the transfer contract, and which routes execute today.", href: "/docs/api/reference/wallets#consumer-wallets-multi-slot-mandate-wallet" },
  { label: "API", title: "Tool calls", description: "Payment status, the correlation key, and the stream frames.", href: "/docs/api/reference/tool-calls" },
]} />
