# Topping up Balance

Everything you need, from a machine with nothing set up.

The **Balance** is prepaid credit for calling models and paid APIs. One
Balance per account, spendable on any model and on paid APIs called with an
API key. $1 buys 1 Balance. It does not expire.

| Package | slug | Price | You get |
|---|---|---|---|
| Mini | `mini` | $1.00 | $1.00 Balance |
| Starter | `starter` | $5.00 | $5.00 Balance |
| Mid | `mid` | $10.00 | $10.00 Balance |
| Pro | `pro` | $25.00 | $25.00 Balance |

This page covers paying by **card**: the human approves a spending limit once,
then their card pays.

---

# Before you start

```bash
npm install -g @fluxa-pay/fluxa-wallet@latest
fluxa-wallet --version              # must be 0.5.0 or newer
```

0.4.x has no `CARD_USD`: every card command fails with "Unsupported currency",
which never mentions the version.

Go straight to Step 1. The approval page in Step 2 collects a card from the
human if they have none, so there is nothing to check or set up first.

---

## Step 1 — Create the order

This is the FluxA side. It fixes the price and produces an **attempt id**, which
is the only thing that crosses from FluxA to the wallet. Nothing is charged and
nothing is reserved yet.

It comes first because its response also names the merchant your mandate has to
be scoped to. Take those values from here rather than from this page, so a
signature can never be spent on a mismatch.

```bash
AUTH=$(fluxa-wallet agent-vc --audience fluxa-wallet-service \
  --challenge wallet-user-info-lookup --ttl 300 | jq -r .data.vc)

ORDER=$(curl -sS -X POST "https://router.fluxapay.xyz/pay/createOrder/topup/<package-slug>" \
  -H "Authorization: Bearer $AUTH")
ATTEMPT_ID=$(echo "$ORDER" | jq -r .attemptId)
STATUS_URL=$(echo "$ORDER" | jq -r .statusUrl)
M_NAME=$(echo "$ORDER" | jq -r .merchant.name)
M_URL=$(echo "$ORDER"  | jq -r .merchant.url)
M_CC=$(echo "$ORDER"   | jq -r .merchant.countryCode)
```

`checkoutUrl` comes back null, and that is correct here: on the card rail there
is no page for anyone to type a card into.

`<package-slug>` is one of the slugs in the table at the top.
Nothing else in that path varies.

**Call this once.** It is not idempotent: every call makes another order.

## Step 2 — Ask the human for permission

A **mandate** is a permission slip: *FluxA may charge my card, up to this much,
for this long*. It is not a payment. Nothing is charged when you create it.

**If a signed mandate already exists, reuse it and skip to Step 4.** Read
**both** keys: filtered by `--host`/`--amount` this returns
`eligibleMandates`, unfiltered it returns `mandates`. Reading one finds
nothing and makes the human sign a mandate they already have.

```bash
fluxa-wallet linked-card mandates --host "${M_URL#https://}" --amount 500 \
  | jq -r '((.eligibleMandates // .mandates // [])[0].mandateId) // empty'
```

**Set the limit to at least 3x the price.** The limit is a running total, not a
per-purchase amount. Every payment you start reserves its slice the moment it
starts, and keeps it until that payment finishes, including one the human walks
away from, which keeps its slice for good. A limit equal to the price therefore
buys exactly one attempt, and a retry costs another signature.

```bash
# $5.00 purchase -> $15.00 limit. The amount is in CENTS.
fluxa-wallet mandate-create --currency CARD_USD --amount 1500 --seconds 7200 \
  --desc "Starter Balance top-up on FluxA, up to \$15.00 total." \
  --merchant-name "$M_NAME" --merchant-url "$M_URL" --merchant-country "$M_CC" \
  --transaction-ref "$ATTEMPT_ID"
```

Pass the merchant through from the order exactly as it came. Those three values
are how the card network identifies FluxA and must match what is registered: a
wrong url fails with `MERCHANT_NOT_AVAILABLE`, a wrong name with
`MERCHANT_DETAILS_MISMATCH`, and neither gets better by retrying.

Give the human `data.authorizationUrl`. **Only they can open it**, and it stops
working after about 10 minutes.

## Step 3 — Wait until the card behind it is live

Signing is not the end of it. The wallet then has to activate a credential
against the card, and that part can fail on its own.

```bash
fluxa-wallet linked-card subcard --mandate <mandateId>
```

Ready is `status: signed`, `isReady: true`, `cardvault.canTransact: true`.
It usually takes about two minutes.

Know when to stop waiting:

- `status: failed` (often `lastErrorCode: CV_FAILED`): the mandate is dead.
  Make a new one and ask for another signature. It costs nothing.
- stuck on `authorizing` with no error: usually nobody pressed the button, or
  the link expired while the page sat open. Same fix.

Neither recovers on its own, so do not loop forever.

## Step 4 — Pay it

```bash
fluxa-wallet headless-checkout --mandate <mandateId> --attempt "$ATTEMPT_ID" \
  --billing @billing.json
```

`--billing` is the only thing you supply: firstName, lastName, address1, city,
state, country, zip. The wallet fills in the rest of the payment context itself.

`ACTION_REQUIRED` with `data.attempt.actionUrl` means the bank wants to see the
human. Send them the link; it works once.

From this moment the price is reserved against the mandate, whether or not the
human finishes. **Never submit this attempt again**, on failure, on a closed tab,
on anything. A retry means a brand-new order.

## Step 5 — Watch it settle

```bash
until [ "$(curl -sS "$STATUS_URL" -H "Authorization: Bearer $AUTH" | jq -r .status)" = "settled" ]; do
  sleep 5
  case "$AUTH" in ey*) AUTH=$(fluxa-wallet agent-vc --audience fluxa-wallet-service \
    --challenge wallet-user-info-lookup --ttl 300 2>/dev/null | jq -r '.data.vc // empty');; esac
done
```

Re-mint as you poll. That credential lasts five minutes and a card payment
usually takes longer, so a loop that does not renew starts getting Unauthorized
and looks exactly like a stuck order.

Done is `status: settled`.

The Balance is topped up at that point.

---

# When it does not work

There are three outcomes, not two.

**Declined.** Tell the human and stop.

**Approved.** Settled, Balance granted.

**Nobody knows.** `PSP_UNKNOWN`, `RETRIEVE_UNKNOWN`, `CREDENTIAL_LOST`, or any
5xx or timeout. The processor never gave a definite answer, so **nobody can say
whether the card was charged**. That attempt is frozen permanently. Resubmitting
it is how a double charge happens. Trying again needs a brand-new order.

Expect this one. It is not rare.
