Money
The six commands that move or read money from a terminal: charge, payment-status, wallet, spend, transfer and ledger.
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:
✗ 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 |
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.
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.
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.
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.
codespar charge --input '{
"amount": 150,
"currency": "BRL",
"method": "pix",
"description": "Order 0000",
"buyer": { "name": "Example Buyer", "email": "buyer@example.com" }
}'✓ 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:00020126580014BR.GOV.BCB.PIX0136example-key5204000053039865802BR6009SAO PAULO62070503***6304ABCDIn 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:
https://api.codespar.dev/v1/sessions/{id}/executeResolves `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; the route's own gates and responses are on POST /v1/sessions/{id}/execute.
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.
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.
# read once
codespar payment-status tcl_0000
# follow until the terminal frame, capped at 30s
codespar payment-status tcl_0000 --stream --timeout 30000✓ payment_status: succeeded
ℹ tool_call_id: tcl_0000
ℹ original_status: success
ℹ idempotency_key: idm_0000
ℹ events: 1Nothing 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.
--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.
Calls one of two routes, depending on --stream:
https://api.codespar.dev/v1/tool-calls/{id}/payment-statushttps://api.codespar.dev/v1/tool-calls/{id}/payment-status/streamBoth are in the HTTP reference: GET /v1/tool-calls/{id}/payment-status and the stream, 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.
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.
codespar wallet consumer_0000currency rail authorized spent available
BRL pix-consent 50000 12500 37500
USDC usdc-onchain 10000 12500 -2500 (overspent)ℹ 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.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:
ℹ No active mandate slots for consumer consumer_empty.
Create one with: codespar mandate create --consumer consumer_empty --slot USDC:usdc-onchain:100:100 ...Calls
https://api.codespar.dev/v1/consumers/{id}/walletField meanings and the full response are on GET /v1/consumers/{id}/wallet; the slot model and the current rail status are under Consumer wallets.
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.
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.
codespar spend \
--mandate cm_0000 \
--payee vendedor@example.com.br \
--amount 2500 \
--agent agt_0000✓ spend executed
ℹ The on-chain tx / receipt is in the response below:{
"spend_id": "spd_0000",
"mandate_id": "cm_0000",
"amount_minor": 2500,
"rail": "pix",
"status": "settled",
"provider_tx_id": "pix_0001",
"receipt_id": "rcpt_0000"
}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
https://api.codespar.dev/v1/consumers/mandates/{id}/spendMoves 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.
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.
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.
codespar transfer consumer_0000 --from BRL --to USDC --amount 15000
codespar transfer consumer_0000 --from BRL --to USDC --amount 15000 --executeroute 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)consumer consumer_0000
route onramp via transfero (real rate, no FX)
onramp tx ftx_0000
status processing
destination 0x0000000000000000000000000000000000000000
source debit pix_0000 (settled)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
https://api.codespar.dev/v1/consumers/{id}/wallet/transferWith --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.
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.
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.
codespar ledger --input '{
"action": "entry",
"asset": "BRL",
"source": [{ "account": "@external/BRL", "amount": 12500 }],
"destination": [{ "account": "@wallet/user_0000", "amount": 12500 }],
"description": "top-up"
}'✓ ledger entry → APPROVED
ℹ Id: txn_0000Id, 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:
https://api.codespar.dev/v1/sessions/{id}/executeResolves `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.
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 |