/api/wallets/consolidate

POST

Reduce wallet size by automatically joining smaller denomination coins into larger ones.

Important

This endpoint modifies your wallet by joining coins via RAIDA. The operation cannot be undone. Source coins are moved to Changed/ and a new larger-denomination coin is written to Bank/. Ensure you understand the implications before running.

Description

Reduces the number of coins in Bank/ by joining ten coins of denomination D into one coin of denomination D+1, repeatedly, sweeping from the lowest denomination upward.

The endpoint is asynchronous. The HTTP call returns a task_id and a poll url immediately; the actual work runs in a background thread. Poll GET /api/system/tasks?task_id=... until status becomes "success" or "failed".

How It Works
  1. Loads every single-coin file from Bank/ into per-denomination buckets in memory.
  2. Sweeps denominations from -8 (0.00000001 CC) upward to 5 (100,000 CC) — the highest source whose join target (6, 1,000,000 CC) is still in human circulation.
  3. For each denomination, while the bucket has enough coins (see Modes below), takes the next 10 source coins and runs the RAIDA Join command (group 9, code 93) to mint one coin of the next-higher denomination.
  4. On RAIDA consensus (≥ 13 of 25), the 10 sources move to Changed/ and the new coin is written to Bank/. The new coin is added to the next-higher in-memory bucket so cascades can fire in the same run (e.g. ten DN0 → one DN1, repeated until DN1 itself reaches the threshold).
  5. If a join round fails (consensus not reached, network error, no available SN), the source coins stay in Bank/ and that denomination is skipped for the rest of the run. Other denominations still proceed.

Modes

The mode parameter controls the loop guard for each denomination:

  • tidy (default) — joins while count > 10. Leaves up to 10 coins of every denomination. Best when you want to keep a usable spread of small notes.
  • complete (alias full) — joins while count >= 10. Maximally consolidates: only the leftover <10 of each denomination remains.
Example
  • You have 35 coins of denomination 0 (value 1) and nothing of denomination 1.
  • mode=tidy: while 35 > 10, join 10 → 1 DN1; bucket now has 25, then 15, then 5 of DN0 plus 3 of DN1. Final: 5×DN0 + 3×DN1.
  • mode=complete: while 35 >= 10, join three batches; after the first cascade DN1 has 3 coins (still <10, so no further action). Final: 5×DN0 + 3×DN1 — same as tidy here, but tidy and complete diverge whenever a denomination has exactly 10 coins (tidy leaves them; complete merges them).

Fracked coins are not consolidated

Only single-coin .bin files in Bank/ are eligible. Coins in Fracked/ must be healed via /api/coins/authenticate or /api/coins/fix first.

Request

HTTP method: GET. All parameters are sent on the query string; there is no request body.

GET /api/wallets/consolidate
GET /api/wallets/consolidate?mode=complete
GET /api/wallets/consolidate?denomination=0&max_rounds=50

Query Parameters

Parameter Type Required Default Description
wallet_path string No Default wallet Wallet to operate on. Resolved using the standard wallet-path rules.
mode string No tidy One of tidy, complete, or full (alias of complete). Case-insensitive. Any other value returns 400.
denomination integer No (sweep all) If present, only this denomination is consolidated. Must be in range -8..5 (the highest human-circulation source). Values outside that range return 400.
max_rounds integer No 1000 Safety cap on total join rounds. Clamped to the range 1..10000.
debug bool No false Per-request debug verbosity. Does not change behavior; only adds detail to main.log.

Response

Kickoff Response (HTTP 200)

Returned immediately, before any RAIDA work happens.

{
  "command": "consolidate",
  "success": true,
  "task_id": "Apr-24-26_05-43-08-pm-b44b",
  "url": "http://localhost:8080/api/system/tasks?task_id=Apr-24-26_05-43-08-pm-b44b",
  "wallet_path": "E:\\Client_Data\\Wallets\\Default",
  "mode": "tidy",
  "max_rounds": 1000,
  "message": "Consolidate started - poll /api/system/tasks for status"
}

If denomination was supplied, it is echoed in the kickoff response.

Kickoff Response Properties

Property Type Description
command string Always "consolidate".
success boolean true when the worker thread was successfully spawned. This does not mean any join has completed — poll the task to find out.
task_id string Identifier for the background task.
url string Convenience: the exact URL to poll for status.
wallet_path string The resolved wallet path the worker will operate on.
mode string The mode that was selected (tidy or complete).
denomination integer Only present when a denomination filter was supplied.
max_rounds integer The clamped max_rounds the worker will use.

Polling Response (Terminal)

When the task finishes, GET /api/system/tasks?task_id=... returns:

{
  "status": "success",
  "payload": {
    "id": "Apr-24-26_05-43-08-pm-b44b",
    "status": "success",
    "progress": 100,
    "message": "Done: 70 joined, 7 created, 0 failed round(s)",
    "data": {
      "command": "consolidate",
      "mode": "tidy",
      "wallet_path": "E:\\Client_Data\\Wallets\\Default",
      "rounds_completed": 7,
      "rounds_failed": 0,
      "coins_joined": 70,
      "coins_created": 7,
      "starting_totals": { "0": 2, "2": 40, "3": 5 },
      "ending_totals":   { "0": 2, "2": 0,  "3": 9 },
      "rounds": [
        {
          "source_denomination": 2,
          "target_denomination": 3,
          "source_serials": [111, 222, 333, 444, 555, 666, 777, 888, 999, 1010],
          "new_serial": 987654,
          "raida_success_count": 25,
          "result": "ok"
        }
      ]
    },
    "raida_total": 25
  }
}

data Object Properties

Property Type Description
command string Always "consolidate".
mode string The mode used (tidy or complete).
wallet_path string The wallet operated on.
rounds_completed integer Number of join rounds where ≥ 13 RAIDAs reached consensus.
rounds_failed integer Number of join rounds that did not reach consensus or hit a transport / SN-availability error.
coins_joined integer Total source coins consumed across all successful rounds (10 × rounds_completed).
coins_created integer Total new higher-denomination coins minted (= rounds_completed).
starting_totals object Map of denomination code → coin count in Bank/ at the start. Only denominations with at least one coin are listed.
ending_totals object Same shape as starting_totals, after the run.
rounds array One entry per attempted round, in chronological order. See below.

Round Object

Each entry in rounds[]:

Property Type When present Description
source_denomination integer always The denomination of the 10 source coins.
target_denomination integer always The denomination of the new minted coin (source_denomination + 1).
raida_success_count integer always Number of RAIDAs (0–25) that returned a success status for this round. ≥ 13 is consensus.
result string always "ok" if consensus was reached and disk operations completed, "failed" otherwise.
source_serials array of integers only on result: "ok" The 10 serial numbers consumed.
new_serial integer only on result: "ok" The serial number of the new minted coin.
error string only on result: "failed" The error string from the underlying join command (e.g. "Join command failed to achieve consensus (8/25)", "Failed to find an available serial number for the new coin.").

Partial Success

If some rounds succeed and others fail, the task still ends with status: "success"rounds_failed > 0 is the partial-success signal. Inspect rounds[] to see which denominations could not be consolidated. Re-running consolidate is safe and idempotent: succeeded coins are already in Bank/, and skipped denominations will be retried.

Failure Modes (task ends with status: "failed")

The whole task fails (no data object) only when the worker cannot start at all:

  • "Transport not initialized" — RAIDA transport layer was not bootstrapped.
  • "No encryption key coin available" — no Bank or Fracked coin in any registered wallet to use as an AES-128 key for encrypted RAIDA traffic.
  • "Failed to scan Bank folder" — filesystem error reading the wallet's Bank/.

HTTP Error Responses (kickoff)

These are returned synchronously from the kickoff GET, before any task is created:

Invalid wallet_path

{ "error": true, "message": "Invalid wallet_path", "code": 400 }

Invalid mode

{ "error": true, "message": "Invalid mode (must be 'tidy', 'complete', or 'full')", "code": 400 }

Invalid denomination

{ "error": true, "message": "Invalid denomination (must be -8 to 5; the join target must stay within human-circulation denominations)", "code": 400 }

Consolidation Rules

Denomination Range

CloudCoin denominations are pure metric powers of ten. The denomination byte is a signed exponent: code 0 = 1 CC, code 1 = 10 CC, code 2 = 100 CC, etc., and negative codes are fractions: code -1 = 0.1 CC, -2 = 0.01 CC, down to -8 = 0.00000001 CC.

  • Eligible source denominations: -8 through 5. Joining ten denom-D coins mints one denom-(D+1) coin, so the highest target is 6 (1,000,000 CC) — the largest denomination in human circulation.
  • Not eligible as a source: 6 and above. Codes 7-11 exist in the protocol as reserved headroom but are not in circulation, so the API refuses to mint into them.

Loop Guard

For each denomination D processed in ascending order:

  • mode=tidy: while count(D) > 10, take 10 coins → mint 1 coin of D+1.
  • mode=complete: while count(D) >= 10, take 10 coins → mint 1 coin of D+1.

Newly minted coins are appended to the in-memory bucket for the next denomination, so cascading works in a single run (e.g. minting the tenth DN1 coin can immediately trigger a DN1→DN2 round).

On Failure

If a join round fails (network error, no available SN, RAIDA consensus < 13, etc.), the source coins are left untouched in Bank/ and the failing denomination is skipped for the rest of this run. Other denominations still proceed. Call /api/wallets/consolidate again later to retry.

Disk Ordering

On a successful round, the 10 source files are moved from Bank/ to Changed/ before the new coin is written to Bank/. If the process crashes between the two steps, the RAIDA-side state and the source-coin location are still consistent: the sources are in Changed/ as the record of what was consumed, and the new coin can be recovered via authenticate / heal flows.

Example Usage

// Kick off consolidate and poll until done.
async function consolidateWallet(mode = 'tidy') {
  const params = new URLSearchParams();
  if (mode) params.set('mode', mode);

  const kickoff = await fetch(
    `http://localhost:8080/api/wallets/consolidate?${params}`
  ).then(r => r.json());

  if (!kickoff.success) {
    console.error('Kickoff failed:', kickoff.message);
    return null;
  }
  console.log(`Started task ${kickoff.task_id} (mode=${kickoff.mode})`);

  // Poll until terminal.
  while (true) {
    await new Promise(r => setTimeout(r, 2000));
    const poll = await fetch(kickoff.url).then(r => r.json());
    const p = poll.payload;
    console.log(`  status=${p.status} progress=${p.progress}% — ${p.message}`);
    if (p.status === 'success' || p.status === 'failed') {
      return p;
    }
  }
}

const result = await consolidateWallet('tidy');
if (result.status === 'success') {
  const d = result.data;
  console.log(`Joined ${d.coins_joined} coins into ${d.coins_created} larger coins.`);
  console.log('Starting:', d.starting_totals);
  console.log('Ending:  ', d.ending_totals);
  if (d.rounds_failed) {
    console.warn(`${d.rounds_failed} round(s) failed; see rounds[].`);
  }
}
# Default: tidy mode on the Default wallet, sweep all denominations
curl "http://localhost:8080/api/wallets/consolidate"

# Maximally consolidate
curl "http://localhost:8080/api/wallets/consolidate?mode=complete"

# Only consolidate denomination 0, with a small max_rounds cap
curl "http://localhost:8080/api/wallets/consolidate?denomination=0&max_rounds=5"

# Poll the task that the kickoff returned
curl "http://localhost:8080/api/system/tasks?task_id=Apr-24-26_05-43-08-pm-b44b"
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "time"
)

type ConsolidateKickoff struct {
    Command    string `json:"command"`
    Success    bool   `json:"success"`
    TaskID     string `json:"task_id"`
    URL        string `json:"url"`
    WalletPath string `json:"wallet_path"`
    Mode       string `json:"mode"`
    MaxRounds  int    `json:"max_rounds"`
    Message    string `json:"message"`
}

type ConsolidateRound struct {
    SourceDenomination int      `json:"source_denomination"`
    TargetDenomination int      `json:"target_denomination"`
    SourceSerials      []uint32 `json:"source_serials,omitempty"`
    NewSerial          uint32   `json:"new_serial,omitempty"`
    RaidaSuccessCount  int      `json:"raida_success_count"`
    Result             string   `json:"result"`
    Error              string   `json:"error,omitempty"`
}

type ConsolidateData struct {
    Command         string             `json:"command"`
    Mode            string             `json:"mode"`
    WalletPath      string             `json:"wallet_path"`
    RoundsCompleted int                `json:"rounds_completed"`
    RoundsFailed    int                `json:"rounds_failed"`
    CoinsJoined     int                `json:"coins_joined"`
    CoinsCreated    int                `json:"coins_created"`
    StartingTotals  map[string]int     `json:"starting_totals"`
    EndingTotals    map[string]int     `json:"ending_totals"`
    Rounds          []ConsolidateRound `json:"rounds"`
}

type TaskPayload struct {
    ID       string          `json:"id"`
    Status   string          `json:"status"`
    Progress int             `json:"progress"`
    Message  string          `json:"message"`
    Data     ConsolidateData `json:"data"`
}

type TaskResponse struct {
    Status  string      `json:"status"`
    Payload TaskPayload `json:"payload"`
}

func consolidate(mode string) (*ConsolidateData, error) {
    q := url.Values{}
    if mode != "" {
        q.Set("mode", mode)
    }
    resp, err := http.Get("http://localhost:8080/api/wallets/consolidate?" + q.Encode())
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var k ConsolidateKickoff
    if err := json.NewDecoder(resp.Body).Decode(&k); err != nil {
        return nil, err
    }
    if !k.Success {
        return nil, fmt.Errorf("kickoff failed: %s", k.Message)
    }

    for {
        time.Sleep(2 * time.Second)
        r, err := http.Get(k.URL)
        if err != nil {
            return nil, err
        }
        var t TaskResponse
        if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
            r.Body.Close()
            return nil, err
        }
        r.Body.Close()
        if t.Payload.Status == "success" || t.Payload.Status == "failed" {
            if t.Payload.Status == "failed" {
                return nil, fmt.Errorf("task failed: %s", t.Payload.Message)
            }
            return &t.Payload.Data, nil
        }
    }
}

func main() {
    d, err := consolidate("tidy")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Joined %d coins into %d. Failed rounds: %d\n",
        d.CoinsJoined, d.CoinsCreated, d.RoundsFailed)
    fmt.Printf("Starting totals: %v\n", d.StartingTotals)
    fmt.Printf("Ending totals:   %v\n", d.EndingTotals)
}

Best Practices

When to Consolidate

Good Times
  • After receiving many small denomination transactions.
  • Before making large payments — fewer coins means fewer signatures and faster RAIDA round-trips.
  • When the wallet has accumulated 100+ coins.
  • During low-activity windows; each round is a 25-RAIDA TCP fan-out.
Avoid
  • If you need to make change in specific small denominations soon — tidy still leaves up to 10, but complete can leave 0–9.
  • While other wallet operations are mid-flight on the same wallet (e.g. an active deposit or transfer).
  • If the network or specific RAIDAs are known to be unhealthy — partial-success runs are safe but will leave work undone.

Choosing a Mode

Mode Loop guard Use case
tidy count > 10 Default. Keeps a usable spread of every denomination while reducing clutter.
complete (or full) count >= 10 Maximally consolidates. Use before storing a wallet long-term or before exporting.

Value Preservation

Total value is invariant

Joining 10 coins of denomination D produces 1 coin of denomination D+1 with exactly the same face value. The RAIDA side of the operation is rejected if the sums don't match.

  • Total wallet value: unchanged.
  • Coin count: reduced by 9 per successful round.
  • Serial numbers: the new coin gets a fresh SN allocated by RAIDA; the source SNs are retired.

Related Endpoints

  • Show Register — view denomination breakdown before consolidating.
  • Make Change — the inverse operation: break one coin into ten of the next-smaller denomination.
  • System TasksGET /api/system/tasks?task_id=... for polling consolidate's task to completion.
  • Authenticate — heal Fracked coins so they qualify for consolidate (which only operates on Bank).