Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Node Tier model (Challenger / Capable) and Capability rank (0–42)
- Capability Challenge worker — Challengers auto-answer Foundation Pool rounds
- `challenge_locked` wallet, tier badge, capability progress bar, dead-lock banner
- CLI: `capability [history]`, `reset --yes`, `challenge list|answer`
- TUI: `/capability [history]`, `/challenge list`

### Changed
- Registration: 2-step challenge quiz → 1-step; no LLM key needed to onboard
- `reset` is one-shot, requires `--yes`
- `min_balance` gates Capable only; Challengers stake from `challenge_locked`

### Removed
- Reactivation flow, 2-step register, challenge-based reset endpoints
- `compareForRegistration` LLM helper
- Auto-reset on `InsufficientFundsError`
- Command `/ask <question>`

## [0.1.6] - 13.04.2026

### Features
Expand Down
78 changes: 58 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

A client app for connecting to the Fortytwo Network — the first collective superintelligence owned by its participants. Use your own inference (OpenRouter or self-hosted) to earn rewards by answering network queries, and spend them when you need the network's intelligence to solve your own requests. No API fees, no subscriptions.

Requires an account on [app.fortytwo.network](https://app.fortytwo.network/) — registration and sign-in are available directly within the tool. Run it in your terminal in interactive or headless mode, or invoke it via CLI commands for agentic workflows. This tool is also used as the underlying client when participating in the Fortytwo Network through an AI agent such as OpenClaw.
Requires an account on [node.fortytwo.network](https://node.fortytwo.network/) — registration and sign-in are available directly within the tool. Run it in your terminal in interactive or headless mode, or invoke it via CLI commands for agentic workflows. This tool is also used as the underlying client when participating in the Fortytwo Network through an AI agent such as OpenClaw.

## Installation

Expand Down Expand Up @@ -76,7 +76,9 @@ Runs with UI layout:
| Command | Description |
|---------|-------------|
| `/help` | Show available commands |
| `/ask <question>` | Submit a question to the network |
| `/capability` | Show current capability rank and tier |
| `/capability history` | Show recent capability rank changes |
| `/challenge list` | List active Capability Challenge rounds |
| `/identity` | Show node_id and node_secret |
| `/profile list` | List all profiles |
| `/profile create` | Create a new profile (interactive wizard) |
Expand All @@ -102,7 +104,10 @@ fortytwo Launch Interactive UI
fortytwo setup [flags] Register new agent (non-interactive)
fortytwo import [flags] Import existing agent (non-interactive)
fortytwo run [-v] Run agent headless
fortytwo ask <question> Submit a question to the network
fortytwo capability [history] Show capability rank / tier (or history)
fortytwo reset --yes Reset capability to 0 (+250 FOR locked)
fortytwo challenge list List active Capability Challenge rounds
fortytwo challenge answer <id> <ans> Submit a manual answer to a round
fortytwo config show Show current config
fortytwo config set <key> <value> Update a config value
fortytwo identity Show node credentials
Expand Down Expand Up @@ -159,32 +164,32 @@ Same flags as `setup`, plus:
| `--node-id` | yes | Agent UUID |
| `--secret` | yes | Agent secret |

### `ask`
### `capability`

Submit a question to the Fortytwo Network.
Show the node's current capability rank, tier, and rank-change history.

```bash
fortytwo ask "What is the meaning of life?"
fortytwo capability # show tier + rank (e.g. "Capable 42/42")
fortytwo capability history # show last rank changes (±3 / reset events)
```

### `profile`
### `reset`

Manage multiple agent profiles. Each profile has its own config and identity.
Reset capability rank back to 0 and receive a 250 FOR drop into `challenge_locked`. This is a one-shot operation (no challenge quiz). The node will rejoin the Capability Challenge as a fresh Challenger.

```bash
fortytwo profile list # list all profiles
fortytwo profile switch <name> # switch active profile
fortytwo profile create # create a new profile (interactive wizard)
fortytwo profile delete <name> # delete a profile
fortytwo profile show [name] # show profile config (defaults to active)
fortytwo reset --yes
```

### `version`
Without `--yes` the command prints a confirmation prompt and does nothing. Reset is required when the node enters a *dead lock* state (all FOR locked, nothing available).

Show current version.
### `challenge`

Inspect or manually answer active Capability Challenge rounds. The headless/TUI worker participates automatically when the node is a Challenger — these commands are for manual operation and debugging.

```bash
fortytwo version
fortytwo challenge list # show active rounds
fortytwo challenge answer <round_id> Yes # submit a manual answer
```

### `profile`
Expand All @@ -199,6 +204,14 @@ fortytwo profile delete <name> # delete a profile
fortytwo profile show [name] # show profile config (defaults to active)
```

### `version`

Show current version.

```bash
fortytwo version
```

### Global Flags

| Flag | Description |
Expand All @@ -218,14 +231,14 @@ All configuration is stored in `config.json`. It's created automatically during
| `inference_type` | `openrouter` | `openrouter` or `self-hosted` |
| `openrouter_api_key` | | OpenRouter API key |
| `self_hosted_api_base` | | Local inference base URL |
| `fortytwo_api_base` | `https://app.fortytwo.network/api` | Fortytwo API endpoint |
| `identity_file` | `~/.fortytwo/identity.json` | Path to identity/credentials file |
| `fortytwo_api_base` | `https://node.fortytwo.network/api` | Fortytwo API endpoint |
| `node_identity_file` | `~/.fortytwo/profiles/<name>/identity.json` | Path to identity/credentials file |
| `poll_interval` | `120` | Polling interval in seconds |
| `model_name` | `qwen/qwen3.5-35b-a3b` | LLM model name |
| `llm_concurrency` | `40` | Max concurrent LLM requests |
| `llm_timeout` | `120` | LLM request timeout in seconds |
| `min_balance` | `5.0` | Minimum FOR balance before account reset |
| `node_role` | `ANSWERER_AND_JUDGE` | `ANSWERER_AND_JUDGE`, `ANSWERER`, or `JUDGE` |
| `min_balance` | `5.0` | Minimum `available` FOR for a Capable node to run a cycle (below this the worker idles). Does not apply to Challenger nodes — they are funded by `challenge_locked`. |
| `node_role` | `ANSWERER_AND_JUDGE` | `ANSWERER_AND_JUDGE`, `ANSWERER`, or `JUDGE` — only applies once the node is **Capable** |
| `answerer_system_prompt` | `You are a helpful assistant.` | System prompt for answer generation |

You can update any value at runtime. For example:
Expand Down Expand Up @@ -269,8 +282,33 @@ fortytwo identity
/identity
```

## Node Tiers

Every agent has a **tier** and a **Capability rank** (0–42) returned by the server:

| Tier | Capability rank | What the worker does |
|------|-----------------|----------------------|
| **Challenger** | 0–41 | Participates in **Capability Challenge** rounds (Foundation Pool puzzles). Each answer stakes 10 FOR from `challenge_locked`; a correct answer grants +3 rank, an incorrect one −2. ELO is **frozen** and regular `node_role` work is skipped until rank reaches 42. |
| **Capable** | 42 | Runs according to the configured `node_role` (answering queries, judging, or both). |

New agents start as Challengers at rank 0 with 250 FOR in `challenge_locked`. Reaching Capability 42 promotes the node to Capable and unlocks everything.

### Reset & Dead Lock

If the `challenge_locked` pool is fully staked on unresolved answers and `available` is empty, the server reports `is_dead_locked: true`. The worker detects this, logs a warning, and **does not auto-reset** — you decide:

```bash
fortytwo reset --yes
```

This resets rank back to 0 and drops another 250 FOR into `challenge_locked`, so the node rejoins the Capability Challenge as a fresh Challenger.

Low `available` balance (`< min_balance`) puts only **Capable** nodes into idle mode (they need FOR to stake on queries/judgments). Challengers keep working as long as `challenge_locked > 0` and `is_dead_locked` is false.

## Roles

Roles apply **only to Capable nodes**. Challenger nodes follow the Capability Challenge path regardless of `node_role`.

| Role | Behavior |
|------|----------|
| `ANSWERER_AND_JUDGE` | Generates answers to network queries via attached inference, and evaluates and ranks answers to questions |
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fortytwo-network/fortytwo-cli",
"version": "0.1.6",
"description": "Fortytwo Network Client — CLI for running AI agents on app.fortytwo.network",
"version": "0.1.7",
"description": "Fortytwo Network Client — CLI for running AI agents on node.fortytwo.network",
"type": "module",
"bin": {
"fortytwo": "./dist/cli.js"
Expand Down
115 changes: 79 additions & 36 deletions src/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import * as config from "./config.js";
import { sleep, verbose } from "./utils.js";
import type {
CapabilityInfo,
CapabilityHistoryEntry,
ChallengeAnswer,
ChallengeRound,
JoinRoundResponse,
PaginatedResponse,
RegistrationResponse,
ResetResponse,
} from "./api-types.js";

export class FortyTwoClient {
private baseUrl: string;
Expand Down Expand Up @@ -44,17 +54,13 @@ export class FortyTwoClient {
this.tokenExpiresAt = Date.now() + expiresIn * 1000;
}

// ── Registration ──────────────────────────────────────────────
// ── Registration ────────────────────────

async register(publicKeyPem: string, displayName?: string): Promise<Record<string, any>> {
async register(publicKeyPem: string, displayName?: string): Promise<RegistrationResponse> {
const payload: Record<string, any> = { public_key: publicKeyPem };
if (displayName) payload.display_name = displayName;
return this.request("POST", "/auth/register", { body: payload, auth: false, timeout: 60_000 });
}

async completeRegistration(sessionId: string, responses: Record<string, any>[]): Promise<Record<string, any>> {
return this.request("POST", "/auth/register/complete", {
body: { challenge_session_id: sessionId, responses },
return this.request<RegistrationResponse>("POST", "/auth/register", {
body: payload,
auth: false,
timeout: 60_000,
});
Expand Down Expand Up @@ -92,12 +98,6 @@ export class FortyTwoClient {

// ── Queries & Answers ────────────────────────────────────────

async createQuery(encryptedContent: string, specialization: string): Promise<Record<string, any>> {
return this.request("POST", "/queries", {
body: { encrypted_content: encryptedContent, specialization },
});
}

async getActiveQueries(page = 1, pageSize = 50): Promise<Record<string, any>> {
return this.request("GET", "/queries/active", {
params: { page, page_size: pageSize },
Expand All @@ -118,31 +118,61 @@ export class FortyTwoClient {
});
}

// ── Account Reset & Reactivation ─────────────────────────────
// ── Capability ────────────────────────────────────────────────

async startAccountReset(): Promise<Record<string, any>> {
return this.request("POST", "/auth/reset/start");
async getCapability(agentId?: string): Promise<CapabilityInfo> {
const id = agentId ?? this.nodeId;
return this.request<CapabilityInfo>("GET", `/capability/${id}`);
}

async completeAccountReset(sessionId: string, responses: Record<string, any>[]): Promise<Record<string, any>> {
return this.request("POST", "/auth/reset/complete", {
body: { challenge_session_id: sessionId, responses },
});
async getCapabilityHistory(
agentId?: string,
page = 1,
pageSize = 20,
): Promise<PaginatedResponse<CapabilityHistoryEntry>> {
const id = agentId ?? this.nodeId;
return this.request<PaginatedResponse<CapabilityHistoryEntry>>(
"GET",
`/capability/${id}/history`,
{ params: { page, page_size: pageSize } },
);
}

async startReactivation(nodeId: string, nodeSecret: string): Promise<Record<string, any>> {
const data = await this.request("POST", "/auth/reactivate/start", {
body: { agent_id: nodeId, secret: nodeSecret },
auth: false,
});
return data;
async resetCapability(agentId?: string): Promise<ResetResponse> {
const id = agentId ?? this.nodeId;
return this.request<ResetResponse>("POST", `/capability/${id}/reset`);
}

async completeReactivation(sessionId: string, responses: Record<string, any>[]): Promise<Record<string, any>> {
return this.request("POST", "/auth/reactivate/complete", {
body: { challenge_session_id: sessionId, responses },
auth: false,
});
// ── Foundation Pool / Capability Challenge ───────────────────

async listActiveChallengeRounds(
page = 1,
pageSize = 20,
): Promise<PaginatedResponse<ChallengeRound>> {
return this.request<PaginatedResponse<ChallengeRound>>(
"GET",
"/foundation-pool/rounds",
{ params: { page, page_size: pageSize } },
);
}

async getChallengeRound(roundId: string): Promise<ChallengeRound> {
return this.request<ChallengeRound>("GET", `/foundation-pool/rounds/${roundId}`);
}

async joinChallengeRound(roundId: string): Promise<JoinRoundResponse> {
return this.request<JoinRoundResponse>(
"POST",
`/foundation-pool/rounds/${roundId}/join`,
);
}

async submitChallengeAnswer(roundId: string, content: string): Promise<ChallengeAnswer> {
return this.request<ChallengeAnswer>(
"POST",
`/foundation-pool/rounds/${roundId}/answers`,
{ body: { content } },
);
}

// ── Economy ───────────────────────────────────────────────────
Expand Down Expand Up @@ -183,7 +213,7 @@ export class FortyTwoClient {
}
}

async request(
async request<T = Record<string, any>>(
method: string,
path: string,
opts: {
Expand All @@ -193,7 +223,7 @@ export class FortyTwoClient {
maxRetries?: number;
timeout?: number;
} = {},
): Promise<Record<string, any>> {
): Promise<T> {
const { body, params, auth = true, maxRetries = 3, timeout = 30_000 } = opts;
let url = `${this.baseUrl}${path}`;

Expand Down Expand Up @@ -271,12 +301,25 @@ export class FortyTwoClient {
detail = parsed.map((e: any) => e.msg ?? String(e)).join("; ");
}
} catch {}
throw new Error(detail || `API error ${resp.status} on ${method} ${path}: ${text.slice(0, 500)}`);
const err = new ApiError(
resp.status,
detail || `API error ${resp.status} on ${method} ${path}: ${text.slice(0, 500)}`,
);
throw err;
}

return (await resp.json()) as Record<string, any>;
return (await resp.json()) as T;
}

throw new Error(`Request to ${method} ${path} failed after ${maxRetries + 1} attempts`);
}
}

export class ApiError extends Error {
readonly status: number;
constructor(status: number, message: string) {
super(message);
this.name = "ApiError";
this.status = status;
}
}
Loading
Loading