Skip to main content

Money

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

10 min read
View MarkdownEdit on GitHub

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:

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.

ExitWhat produced itHow it reads
0The command finished, including a --stream that reached its terminal frameThe output of the command
1A refusal the CLI raises itself, including any HTTP error on the wallet / spend / transfer path✗ <message> on stderr
1An argument error Commander raises before the command body runserror: <message> on stderr, with no
2Anything 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.

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]
FlagTypeRequiredWhat it does
-i, --input <json>JSON object, inlineone of the twoThe charge arguments
-f, --input-file <path>pathone of the twoThe same object, read from a file
-u, --user <id>stringnoThe 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.

Example
codespar charge --input '{
  "amount": 150,
  "currency": "BRL",
  "method": "pix",
  "description": "Order 0000",
  "buyer": { "name": "Example Buyer", "email": "buyer@example.com" }
}'
Output
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:
stdout
00020126580014BR.GOV.BCB.PIX0136example-key5204000053039865802BR6009SAO PAULO62070503***6304ABCD

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:

POSThttps://api.codespar.dev/v1/sessions/{id}/execute
Can move money

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; the route's own gates and responses are on POST /v1/sessions/{id}/execute.

Refusals

What you didMessageExit
Neither --input nor --input-filecharge requires --input '<json>' or --input-file <path>. Example: --input '{"amount":50,...}'1
Both of themPass 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 --input1
Unparseable JSON--input is not valid JSON: <parser message>1
amount missing or not a numbercharge.amount must be a number (major units).1
currency missingcharge.currency is required (e.g. BRL).1
method missingcharge.method is required (pix | boleto | card).1
buyer.name missingcharge.buyer.name is required.1
description missingcharge.description is required.1
No credential resolvedNot 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 ENOENT2

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 flagTypeRequiredWhat it does
<tool-call-id>stringyesThe tool_call_id of the call whose payment you are tracking
--streambooleannoFollow the stream instead of reading once
--timeout <ms>integer, millisecondsnoBounds the stream wait. Default 600000, set in the command body
-u, --user <id>stringnoThe 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.

Example
# read once
codespar payment-status tcl_0000

# follow until the terminal frame, capped at 30s
codespar payment-status tcl_0000 --stream --timeout 30000
Output of the single read
stderr
✓ payment_status: succeeded
ℹ tool_call_id: tcl_0000
ℹ original_status: success
ℹ idempotency_key: idm_0000
ℹ events: 1

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.

--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:

GEThttps://api.codespar.dev/v1/tool-calls/{id}/payment-status
GEThttps://api.codespar.dev/v1/tool-calls/{id}/payment-status/stream

Both 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 didMessageExit
No tool-call iderror: 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+Cpayment-status stream timed out or was aborted.1
No credential resolvedNot 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]
ArgumentTypeRequiredWhat it does
<consumer>stringyesThe consumer id, percent-encoded into the path

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

Example
codespar wallet consumer_0000
Output
stdout
currency  rail          authorized  spent  available
BRL       pix-consent   50000       12500  37500
USDC      usdc-onchain  10000       12500  -2500 (overspent)
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.

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:

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

GEThttps://api.codespar.dev/v1/consumers/{id}/wallet

Field 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 didMessageExit
No consumer iderror: missing required argument 'consumer'1
The API answered an error statusGET /v1/consumers/<id>/wallet → 403: <message from the body>1
No credential resolvedNot 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]
FlagTypeRequiredWhat it does
-m, --mandate <id>stringyesThe consumer mandate id, percent-encoded into the path
-p, --payee <payee>stringyesSent unparsed as payee
-a, --amount <minor>positive integeryesMinor units. Sent as amount_minor
--agent <id>stringyesSent 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.

Example
codespar spend \
  --mandate cm_0000 \
  --payee vendedor@example.com.br \
  --amount 2500 \
  --agent agt_0000
Output
stderr
✓ spend executed
ℹ The on-chain tx / receipt is in the response below:
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"
}

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

POSThttps://api.codespar.dev/v1/consumers/mandates/{id}/spend
Moves 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.

Refusals

What you didMessageExit
Omitted any of the four flagserror: required option '--agent <id>' not specified, and the same shape for the others1
--amount 0, a negative, or a decimal--amount must be a positive integer in minor units (cents).1
The API answered an error statusPOST /v1/consumers/mandates/<id>/spend → 403: <message from the body>1
No credential resolvedNot 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 flagTypeRequiredWhat it does
<consumer>stringyesThe consumer id, percent-encoded into the path
--from <currency>stringyesSent as from_currency
--to <currency>stringyesSent as to_currency
--amount <minor>positive integeryesSent as amount_minor, in the source currency's minor units
--executebooleannoAdds execute: true to the body. Omitted, the body carries no execute at all
--agent <id>stringnoSent as agent_id, only when given
--purpose <text>stringnoSent 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.

Plan, then execution
codespar transfer consumer_0000 --from BRL --to USDC --amount 15000

codespar transfer consumer_0000 --from BRL --to USDC --amount 15000 --execute
Output of each
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)
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)

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

POSThttps://api.codespar.dev/v1/consumers/{id}/wallet/transfer
Moves 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.

Refusals

What you didMessageExit
No consumer iderror: missing required argument 'consumer'1
Omitted --from, --to or --amounterror: required option '--amount <minor>' not specified, and the same shape for the others1
A decimal, zero or negative --amount--amount must be a positive integer in minor units (cents).1
The API answered an error statusPOST /v1/consumers/<id>/wallet/transfer → <status>: <message from the body>1
No credential resolvedNot 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]
FlagTypeRequiredWhat it does
-i, --input <json>JSON object, inlineone of the twoThe ledger arguments
-f, --input-file <path>pathone of the twoThe same object, read from a file
-u, --user <id>stringnoThe session's user id. Default cli-user

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

actionWhat the CLI also requiresThe refusal when it is missing
entryasset, a non-empty source array, a non-empty destination arrayledger.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.
balanceaccountledger.account (id) is required when action=balance.
accountassetledger.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.

Example
codespar ledger --input '{
  "action": "entry",
  "asset": "BRL",
  "source": [{ "account": "@external/BRL", "amount": 12500 }],
  "destination": [{ "account": "@wallet/user_0000", "amount": 12500 }],
  "description": "top-up"
}'
Output
stderr
✓ ledger entry → APPROVED
ℹ Id: txn_0000

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:

POSThttps://api.codespar.dev/v1/sessions/{id}/execute
Can move money

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.

Refusals

What you didMessageExit
Neither --input nor --input-fileledger requires --input '<json>' or --input-file <path>. Example: ... with a ledger example1
Both of them, or JSON that is unparseable or not an objectThe same three messages as charge1
An action outside the threeledger.action must be one of: entry, balance, account.1
A field the action requires is missingThe message in the action table above1
No credential resolvedNot 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

Money | CodeSpar