Skip to content

feat(chain): settlement program, node identity, vanity wallets, devnet scripts#171

Merged
RustMunkey merged 7 commits intomainfrom
feat/solana-devnet
Mar 28, 2026
Merged

feat(chain): settlement program, node identity, vanity wallets, devnet scripts#171
RustMunkey merged 7 commits intomainfrom
feat/solana-devnet

Conversation

@RustMunkey
Copy link
Copy Markdown
Owner

@RustMunkey RustMunkey commented Mar 27, 2026

Summary by CodeRabbit

  • New Features

    • On-chain settlement: config, node registration, per-node vaults, earnings recording, and payouts (70/15/10/5); events and IDL included
    • CLI/scripts: mint MACH token, create governance realm, register Helius webhook, upload assets to Arweave, devnet setup, and e2e settlement test
    • ASCII logo and token metadata asset added
  • Bug Fixes

    • Stronger validation and richer settlement error codes; overflow and balance checks
  • Tests

    • End-to-end settlement flow and tightened revenue rounding test
  • Documentation

    • Terminology change: “node operators” → “node runners”
  • Configuration

    • Expanded env entries (cluster, Helius, settlement, runtime), .gitignore updates, and devDependency additions

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Solana settlement subsystem (new program ID, PDAs, accounts, events) with Anchor instructions for config, node registration, vault init, earnings recording, and settlement; introduces TypeScript tooling (Helius webhook, token/realm setup, uploads, e2e), env/config changes, docs copy updates, and test/asset additions.

Changes

Cohort / File(s) Summary
Environment & Config
\.env.example, \.gitignore, programs/Anchor.toml, programs/Cargo.toml
Replaced SOLANA_RPC_URL with SOLANA_CLUSTER; added Helius/webhook and settlement/mint/treasury env placeholders and runtime app entries; set concrete settlement program ID and workspace in Anchor.toml; added Cargo workspace and release overflow checks; ignore wallet/keypair artifacts.
Settlement Core & API
programs/settlement/src/lib.rs, programs/settlement/src/state.rs, programs/settlement/src/errors.rs
Changed declared program ID; added global SettlementConfig and NodeIdentity accounts, VAULT_SEED, events (NodeRegistered, EarningsRecorded), new error variants, and exported new program entrypoints (initialize_config, register_node, init_node_vault, add_earnings, settle_earnings).
Settlement Instructions
programs/settlement/src/instructions/... (mod.rs, initialize_config.rs, register_node.rs, init_node_vault.rs, add_earnings.rs, settle_earnings.rs)
Added instruction modules with account constraints and handlers: config init, node registration, deterministic per-node vault init, recording earnings (CPI transfer into vault + pending accounting), and revised settle_earnings to validate recipients and perform SPL CPI payouts.
IDL & Artifacts
programs/settlement/target/idl/settlement.json
New Anchor IDL generated describing instructions, accounts, events, and error codes for the settlement program.
Scripts & Automation
scripts/setup-devnet.ts, scripts/setup-mach-token.ts, scripts/setup-realms.ts, scripts/test-settlement-e2e.ts, scripts/upload-arweave.ts
Added scripts for Helius webhook registration (devnet), MACH Token-2022 mint creation, Realms governance creation, localnet end-to-end settlement test, and Arweave uploads via Irys.
Tests & Marketplace
packages/marketplace/src/marketplace.test.ts
Augmented rounding test to assert treasuryCents = 17 for the 99¢ case (keeps existing node/developer/validator expectations).
Dependencies & Build
packages/chain/package.json, programs/settlement/Cargo.toml, package.json
Added @solana/spl-token, @solana/spl-governance, bn.js to chain package; added idl-build feature and anchor-spl/feature updates in settlement Cargo.toml; added @irys/sdk to devDependencies.
Terminology & UX copy
apps/docs/..., apps/web/src/routes/*.tsx, scripts/discord-scaffold.mjs
Replaced "node operators" → "node runners" across docs, web pages, and Discord scaffold role key.
Misc & Assets
ascii-logo.txt, assets/mach-token-metadata.json
Added ASCII logo and token metadata JSON; asset image/metadata upload flow and guidance added.

Sequence Diagram

sequenceDiagram
    participant Authority as Authority
    participant Program as SettlementProgram
    participant Config as SettlementConfig (PDA)
    participant Pool as SettlementPool (PDA)
    participant Vault as NodeVault (Token Account)
    participant Recipients as Recipients (Token Accounts)

    Authority->>Program: initialize_config(treasury, developer, validators)
    Program->>Config: create & store trusted recipient keys
    Config-->>Program: created

    Authority->>Program: register_node(node_id)
    Program->>Pool: create NodeIdentity PDA
    Pool-->>Program: registered

    Authority->>Program: init_node_vault(node_id)
    Program->>Vault: init USDC token account owned by Pool PDA
    Vault-->>Program: initialized

    Authority->>Program: add_earnings(node_id, run_id, amounts)
    Program->>Vault: CPI transfer total USDC from authority -> Vault
    Program->>Pool: update pending earnings
    Program-->>Authority: emit EarningsRecorded

    Authority->>Program: settle_earnings(node_id)
    Program->>Config: verify recipients' owner keys
    Program->>Vault: CPI transfers Vault -> Recipients (operator/developer/treasury/validators)
    Program->>Pool: zero pending payouts
    Recipients-->>Program: balances updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰
Vaults take root where tiny numbers hop,
Runners register, ledgers tick and stop,
Splits are counted, pennies find their place,
I twitch my nose — settlements fall into grace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: it adds a settlement program with node identity registration, introduces vanity wallet setup, and includes devnet-specific scripts. All major components are referenced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/solana-devnet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
content/docs/platform/economics.mdx (1)

44-55: ⚠️ Potential issue | 🟡 Minor

Terminology rename is incomplete within this page.

This section uses “Node Runners”, but the same page still uses “Node operators/operators” at Line 85, Line 87, and Line 118.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/docs/platform/economics.mdx` around lines 44 - 55, The page mixes the
terms "Node Runners" and "Node operators/operators"; standardize terminology by
replacing every instance of "Node operators" and "operators" with "Node Runners"
(matching capitalization) so the table header, headings, and all inline
references use the single agreed term; update any plural/singular forms and
related possessives to "Node Runner(s)" and scan nearby JSX/text (e.g., the
table row labeled "Node Runners" and the inline uses currently reading "Node
operators" and "operators") to ensure consistency throughout the document.
programs/settlement/src/instructions/settle_earnings.rs (1)

23-71: ⚠️ Potential issue | 🔴 Critical

Authenticate the sweep and its payout accounts.

authority is only required to sign, and developer_usdc, treasury_usdc, and validators_usdc are only mint-checked. That means any caller can sweep a funded vault and route those three payouts to their own token accounts. The operator share is protected by pool.operator; the other 30% is not.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/settle_earnings.rs` around lines 23 -
71, The SettleEarnings context allows anyone with a signer to sweep funds
because developer_usdc, treasury_usdc, and validators_usdc are only
mint-checked; fix by authenticating the sweep authority and validating payout
account owners: add or require a specific settlement authority account (e.g., a
new account like settlement_authority: Signer or AccountInfo) and assert
authority.key == settlement_authority.key (or compare to a stored
pool.settlement_authority), and enforce owner constraints for the payout
accounts by checking developer_usdc.owner == pool.developer (or the expected
developer PDA), treasury_usdc.owner == expected_treasury_owner, and
validators_usdc.owner == expected_validators_owner (or add those expected owner
fields to SettlementPool and reference them); implement these checks either as
Anchor account constraints on SettleEarnings or explicit runtime checks in the
handler that reference SettleEarnings::pool, authority, developer_usdc,
treasury_usdc, and validators_usdc.
🧹 Nitpick comments (3)
.releaserc.json (1)

18-18: Consider aligning breaking-change policy with Semantic Versioning.

Line 18 maps breaking changes to minor releases, which contradicts semantic-release defaults and standard SemVer practice requiring major for breaking changes. This can hide compatibility breaks from consumers. Either document this as intentional policy (e.g., for pre-1.0 development) or align with SemVer:

SemVer-aligned alternative
-          { "breaking": true, "release": "minor" }
+          { "breaking": true, "release": "major" }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.releaserc.json at line 18, Update the .releaserc.json entry that maps
breaking changes (the object with "breaking": true and "release": "minor") to
follow SemVer by changing its "release" value from "minor" to "major", or
alternatively add a comment/README note in the repo explaining this is an
intentional deviation (e.g., pre-1.0 policy) so reviewers know it’s deliberate;
locate the object matching "breaking": true to apply the change.
scripts/discord-scaffold.mjs (1)

124-125: Rename the map key to match updated role semantics.

roles.nodeOperator now stores "Node Runner", which is misleading for future maintenance.

Small naming cleanup
-  roles.nodeOperator = await upsertRole(existing, {
+  roles.nodeRunner = await upsertRole(existing, {
     name: "Node Runner",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/discord-scaffold.mjs` around lines 124 - 125, The map key
roles.nodeOperator is misleading because upsertRole(existing, { name: "Node
Runner", ... }) actually stores the "Node Runner" role; rename the key to
roles.nodeRunner (and update every reference to roles.nodeOperator) so the
variable name matches the role semantics, and update any usages (reads/writes)
that reference roles.nodeOperator to use roles.nodeRunner; ensure the
upsertRole(...) call and any role checks or permissions logic (e.g., role
comparisons) are updated accordingly.
packages/marketplace/src/marketplace.test.ts (1)

66-72: Add an explicit treasury assertion in rounding test.

Line 67 documents treasury=17, but Line 72 checks it only implicitly through sum. A direct assertion would make regressions easier to pinpoint.

Diff
   it("treasury absorbs rounding (not validator)", () => {
     // 99 cents: node=69, dev=9, validator=4, treasury=remainder=17
     const r = calcExecutionRevenue(99);
     expect(r.nodeCents).toBe(69);
     expect(r.developerCents).toBe(9);
     expect(r.validatorCents).toBe(4);
+    expect(r.treasuryCents).toBe(17);
     expect(r.nodeCents + r.developerCents + r.treasuryCents + r.validatorCents).toBe(99);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/marketplace/src/marketplace.test.ts` around lines 66 - 72, The test
for rounding in the marketplace suite documents treasury=17 but only asserts the
total sum; update the test that calls calcExecutionRevenue(99) in
marketplace.test.ts to add a direct expectation for r.treasuryCents toBe(17) so
the treasury rounding is checked explicitly alongside r.nodeCents,
r.developerCents, and r.validatorCents.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.releaserc.json:
- Line 17: The semantic-release rule { "type": "release", "release": "major" }
in .releaserc.json is unreachable because commitlint.config.js's type-enum does
not include "release"; either remove that rule from .releaserc.json (delete the
{ "type": "release", "release": "major" } entry) or make the configs consistent
by adding "release" to the type-enum array in commitlint.config.js so commits
with type "release" pass commitlint and reach semantic-release.

In `@content/docs/platform/economics.mdx`:
- Line 152: Update the documented fee distribution percentages to match the
settlement logic in anchor_receipt.rs: change the "Compute Rewards (Node
Runners)" entry (currently "40") and the corresponding line at ~160 from the
40/30/20/10 split to the actual 65/20/10/5 split (node runners 65%, developer
20%, treasury 10%, validators 5%); edit the two entries in
content/docs/platform/economics.mdx so they reflect 65/20/10/5 before
publishing.

In `@programs/settlement/src/instructions/add_earnings.rs`:
- Around line 16-28: The AddEarnings instruction currently only records run_id
in the event, allowing duplicate processing; fix by persisting a PDA receipt
keyed by run_id and rejecting duplicates before moving funds: add a PDA account
(e.g., "earnings_receipt" + run_id) to the AddEarnings accounts struct, derive
the PDA from the provided AddEarningsArgs.run_id, check if the receipt account
already exists (and return an error if so), and create/initialize the receipt
account (marking it filled) atomically before performing any USDC transfers in
the handler that processes AddEarningsArgs/run_id so retries are detected and
prevented.
- Around line 35-68: The add_earnings instruction currently accepts any signer
as authority; require the signer to be the configured settlement authority
instead. Update the account constraint on the authority account (and/or add a
runtime check in the add_earnings handler) so authority.key() must equal the
settlement authority recorded in the Pool (e.g., compare authority.key() ==
pool.settlement_authority or use #[account(address =
pool.settlement_authority)]), and return an Unauthorized error if it doesn't
match; this ensures only the Maschina multisig can emit EarningsRecorded and
mutate pool.run_count and balances.

In `@programs/settlement/src/instructions/init_node_vault.rs`:
- Around line 30-45: The vault can be initialized against any mint because
usdc_mint is caller-controlled; lock it to the canonical USDC mint by removing
caller control and enforcing the canonical mint in the account constraint:
replace or supplement the caller-provided usdc_mint with a canonical_mint
Account<'info, Mint> (or add a canonical_mint parameter) and change the vault
attribute from token::mint = usdc_mint to token::mint = canonical_mint (or if
you must keep usdc_mint, add an explicit runtime check in the init handler
asserting usdc_mint.key() == CANONICAL_USDC_PUBKEY and fail early); update
references to usdc_mint / vault and use VAULT_SEED and args.node_id as before so
the PDA remains deterministic.

In `@scripts/discord-scaffold.mjs`:
- Line 125: Renaming "Node Runner" breaks idempotence because existing guilds
may still have legacy roles/categories named "Node Operator" or "NODE
OPERATORS"; before the create-or-skip step for the new name (the block where
name: "Node Runner" is used and any createOrSkipRole/createOrSkipCategory helper
is invoked), add a migration pass that looks up roles and categories by the
legacy names ("Node Operator", "NODE OPERATORS"), if found rename them to "Node
Runner" (and update any stored IDs/mappings) instead of creating new ones; apply
the same legacy-rename logic to the other identical name-change occurrences in
the file so the script preserves existing objects rather than duplicating them.

In `@scripts/setup-devnet.ts`:
- Around line 11-13: The header comment in scripts/setup-devnet.ts incorrectly
claims the script "Verifies the settlement program is deployed at
SETTLEMENT_PROGRAM_ID" while no RPC/account check exists; update the header to
accurately reflect behavior or add an actual on-chain check. Either (A) change
the comment text to remove "Verifies the settlement program is deployed at
SETTLEMENT_PROGRAM_ID" (e.g., state that the script assumes
SETTLEMENT_PROGRAM_ID is set) or (B) implement a runtime verification that
queries the RPC for the account/program at SETTLEMENT_PROGRAM_ID (using your
existing RPC client code) and fails/logs if not found, referencing the
identifier SETTLEMENT_PROGRAM_ID so reviewers can locate the relevant code path.
- Line 41: The webhookUrl is built from apiBaseUrl and can produce a
double-slash when API_BASE_URL ends with a slash; update the code that computes
webhookUrl (the apiBaseUrl value derived from the API_BASE_URL env and the
webhookUrl constant) to normalize apiBaseUrl first by trimming any trailing
slash (or otherwise ensuring exactly one separator) before concatenating
"/webhooks/helius", so webhookUrl is always a single-correct URL without
duplicate slashes.

In `@scripts/setup-mach-token.ts`:
- Around line 66-71: Update loadKeypair to prefer SOLANA_WALLET_PATH, fall back
to ANCHOR_WALLET, then default; expand a leading tilde before reading the file
so paths like ~/... work on Node (use os.homedir() replacement), and then read
and parse the JSON as before; reference the loadKeypair function to locate where
to change the environment variable checks and path handling.
- Around line 52-64: Add runtime validation for the CLUSTER value and make
explorer/.env hints reflect the actual cluster: validate the environment
variable assigned to CLUSTER (the variable named CLUSTER) at startup and throw a
clear error if it is not exactly "devnet" or "mainnet-beta", so the type
assertion is not relied on; keep getRpcUrl() as-is but assume CLUSTER is
validated beforehand; update the explorer link and the .env hint strings (the
code that constructs the explorer URL and the printed .env example) to use
CLUSTER dynamically (choose the appropriate explorer host for "mainnet-beta" vs
"devnet") so messages are consistent with the validated cluster.

In `@services/daemon/src/orchestrator/analyze.rs`:
- Around line 384-387: The earnings split currently hardcodes 70/10/15/5 and the
comment is wrong; add a developer attribution flag to JobToRun (e.g.,
developer_id: Option<Uuid> or is_first_party: bool), populate it from the
agent/run metadata where JobToRun is constructed, pass that field through to
record_node_earnings, and change the split logic in analyze.rs: if first-party
use 70/25/0/5 (node/treasury/developer/validator) else use 70/15/10/5
(node/treasury/developer/validator); also update the function comment to match
the implemented conditional behavior.

---

Outside diff comments:
In `@content/docs/platform/economics.mdx`:
- Around line 44-55: The page mixes the terms "Node Runners" and "Node
operators/operators"; standardize terminology by replacing every instance of
"Node operators" and "operators" with "Node Runners" (matching capitalization)
so the table header, headings, and all inline references use the single agreed
term; update any plural/singular forms and related possessives to "Node
Runner(s)" and scan nearby JSX/text (e.g., the table row labeled "Node Runners"
and the inline uses currently reading "Node operators" and "operators") to
ensure consistency throughout the document.

In `@programs/settlement/src/instructions/settle_earnings.rs`:
- Around line 23-71: The SettleEarnings context allows anyone with a signer to
sweep funds because developer_usdc, treasury_usdc, and validators_usdc are only
mint-checked; fix by authenticating the sweep authority and validating payout
account owners: add or require a specific settlement authority account (e.g., a
new account like settlement_authority: Signer or AccountInfo) and assert
authority.key == settlement_authority.key (or compare to a stored
pool.settlement_authority), and enforce owner constraints for the payout
accounts by checking developer_usdc.owner == pool.developer (or the expected
developer PDA), treasury_usdc.owner == expected_treasury_owner, and
validators_usdc.owner == expected_validators_owner (or add those expected owner
fields to SettlementPool and reference them); implement these checks either as
Anchor account constraints on SettleEarnings or explicit runtime checks in the
handler that reference SettleEarnings::pool, authority, developer_usdc,
treasury_usdc, and validators_usdc.

---

Nitpick comments:
In @.releaserc.json:
- Line 18: Update the .releaserc.json entry that maps breaking changes (the
object with "breaking": true and "release": "minor") to follow SemVer by
changing its "release" value from "minor" to "major", or alternatively add a
comment/README note in the repo explaining this is an intentional deviation
(e.g., pre-1.0 policy) so reviewers know it’s deliberate; locate the object
matching "breaking": true to apply the change.

In `@packages/marketplace/src/marketplace.test.ts`:
- Around line 66-72: The test for rounding in the marketplace suite documents
treasury=17 but only asserts the total sum; update the test that calls
calcExecutionRevenue(99) in marketplace.test.ts to add a direct expectation for
r.treasuryCents toBe(17) so the treasury rounding is checked explicitly
alongside r.nodeCents, r.developerCents, and r.validatorCents.

In `@scripts/discord-scaffold.mjs`:
- Around line 124-125: The map key roles.nodeOperator is misleading because
upsertRole(existing, { name: "Node Runner", ... }) actually stores the "Node
Runner" role; rename the key to roles.nodeRunner (and update every reference to
roles.nodeOperator) so the variable name matches the role semantics, and update
any usages (reads/writes) that reference roles.nodeOperator to use
roles.nodeRunner; ensure the upsertRole(...) call and any role checks or
permissions logic (e.g., role comparisons) are updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0118aff8-3921-44e3-b826-c82d87137d3c

📥 Commits

Reviewing files that changed from the base of the PR and between 1083159 and ceb00bf.

📒 Files selected for processing (24)
  • .env.example
  • .releaserc.json
  • ascii-logo.txt
  • content/docs/platform/economics.mdx
  • content/docs/platform/nodes.mdx
  • content/docs/platform/overview.mdx
  • content/docs/platform/roadmap.mdx
  • packages/chain/src/settlement.ts
  • packages/db/src/schema/pg/nodes.ts
  • packages/marketplace/src/index.ts
  • packages/marketplace/src/marketplace.test.ts
  • programs/Anchor.toml
  • programs/settlement/Cargo.toml
  • programs/settlement/src/errors.rs
  • programs/settlement/src/instructions/add_earnings.rs
  • programs/settlement/src/instructions/init_node_vault.rs
  • programs/settlement/src/instructions/mod.rs
  • programs/settlement/src/instructions/settle_earnings.rs
  • programs/settlement/src/lib.rs
  • programs/settlement/src/state.rs
  • scripts/discord-scaffold.mjs
  • scripts/setup-devnet.ts
  • scripts/setup-mach-token.ts
  • services/daemon/src/orchestrator/analyze.rs

{ "type": "chore", "release": false },
{ "type": "ci", "release": false },
{ "type": "test", "release": false },
{ "type": "release", "release": "major" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking semantic-release rule:"
rg -n '"type"\s*:\s*"release"' .releaserc.json

echo
echo "Checking commitlint allowed types:"
rg -n -A30 -B2 '"type-enum"' commitlint.config.js

Repository: RustMunkey/maschina

Length of output: 892


Remove unreachable release commit type rule from semantic-release configuration.

Line 17 defines a major bump for type: "release", but commitlint.config.js (lines 7-24) does not include "release" in its type-enum, so this rule is unreachable—commits with this type will be rejected by commitlint before reaching semantic-release.

Either remove this rule from .releaserc.json or add "release" to the type-enum in commitlint.config.js to make the configuration consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.releaserc.json at line 17, The semantic-release rule { "type": "release",
"release": "major" } in .releaserc.json is unreachable because
commitlint.config.js's type-enum does not include "release"; either remove that
rule from .releaserc.json (delete the { "type": "release", "release": "major" }
entry) or make the configs consistent by adding "release" to the type-enum array
in commitlint.config.js so commits with type "release" pass commitlint and reach
semantic-release.

```mermaid
pie title Network Fee Distribution
"Compute Rewards (Node Operators)" : 40
"Compute Rewards (Node Runners)" : 40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare settlement split references across chain/program/docs.
rg -n -C2 '70/15/10/5|40/30/20/10|Compute Rewards|Protocol Treasury|validator|node runners' \
  packages/chain/src/settlement.ts \
  content/docs/platform/economics.mdx \
  programs/settlement/src

Repository: RustMunkey/maschina

Length of output: 12518


Update fee distribution percentages to match current settlement implementation.

The documentation claims a 40/30/20/10 split, but the actual anchor_receipt.rs settlement code implements 65/20/10/5 (node runners 65%, developer 20%, treasury 10%, validators 5%). Update lines 152 and 160 to reflect the correct percentages before publication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/docs/platform/economics.mdx` at line 152, Update the documented fee
distribution percentages to match the settlement logic in anchor_receipt.rs:
change the "Compute Rewards (Node Runners)" entry (currently "40") and the
corresponding line at ~160 from the 40/30/20/10 split to the actual 65/20/10/5
split (node runners 65%, developer 20%, treasury 10%, validators 5%); edit the
two entries in content/docs/platform/economics.mdx so they reflect 65/20/10/5
before publishing.

Comment on lines +16 to +28
pub struct AddEarningsArgs {
pub node_id: [u8; 16],
/// Run ID — stored in the event for off-chain reconciliation.
pub run_id: [u8; 16],
/// Node runner's share in USDC lamports (70%).
pub node_amount: u64,
/// Developer royalty in USDC lamports (10%, 0 for first-party agents).
pub developer_amount: u64,
/// Treasury share in USDC lamports (15%, or 25% when developer_amount == 0).
pub treasury_amount: u64,
/// Validators share in USDC lamports (5%).
pub validator_amount: u64,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make run_id an on-chain dedupe key.

run_id is only written to the event. If the settlement worker retries or replays the same run, this instruction transfers USDC again and increments the pool again. For real-money settlement, persist a PDA/receipt keyed by run_id and reject duplicates before moving funds.

Also applies to: 73-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/add_earnings.rs` around lines 16 - 28,
The AddEarnings instruction currently only records run_id in the event, allowing
duplicate processing; fix by persisting a PDA receipt keyed by run_id and
rejecting duplicates before moving funds: add a PDA account (e.g.,
"earnings_receipt" + run_id) to the AddEarnings accounts struct, derive the PDA
from the provided AddEarningsArgs.run_id, check if the receipt account already
exists (and return an error if so), and create/initialize the receipt account
(marking it filled) atomically before performing any USDC transfers in the
handler that processes AddEarningsArgs/run_id so retries are detected and
prevented.

Comment on lines +35 to +68
/// Settlement authority (Maschina multisig) — signs and funds the transfer.
#[account(mut)]
pub authority: Signer<'info>,

/// USDC mint.
pub usdc_mint: Account<'info, Mint>,

/// Settlement authority's USDC token account — source of funds.
#[account(
mut,
token::mint = usdc_mint,
token::authority = authority,
)]
pub authority_usdc: Account<'info, TokenAccount>,

/// Pool PDA — accumulates pending earnings.
#[account(
mut,
seeds = [b"pool", args.node_id.as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, SettlementPool>,

/// Per-node USDC vault — receives the total earnings.
#[account(
mut,
seeds = [VAULT_SEED, args.node_id.as_ref()],
bump,
token::mint = usdc_mint,
token::authority = pool,
)]
pub vault: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

add_earnings is missing its authority check.

Right now this only requires “some signer with some token account”. Any user can emit EarningsRecorded, mutate pending balances, and bump run_count, even though the split is supposed to be computed by the Maschina settlement authority.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/add_earnings.rs` around lines 35 - 68,
The add_earnings instruction currently accepts any signer as authority; require
the signer to be the configured settlement authority instead. Update the account
constraint on the authority account (and/or add a runtime check in the
add_earnings handler) so authority.key() must equal the settlement authority
recorded in the Pool (e.g., compare authority.key() == pool.settlement_authority
or use #[account(address = pool.settlement_authority)]), and return an
Unauthorized error if it doesn't match; this ensures only the Maschina multisig
can emit EarningsRecorded and mutate pool.run_count and balances.

Comment on lines +30 to +45
/// USDC mint.
/// On devnet use: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
/// On mainnet use: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
pub usdc_mint: Account<'info, Mint>,

/// Per-node USDC vault. Authority = pool PDA so only the program can move funds.
/// Seeded with [VAULT_SEED, node_id] to keep it deterministic and auditable.
#[account(
init,
payer = payer,
seeds = [VAULT_SEED, args.node_id.as_ref()],
bump,
token::mint = usdc_mint,
token::authority = pool,
)]
pub vault: Account<'info, TokenAccount>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Lock this vault to canonical USDC before first init.

usdc_mint is fully caller-controlled here. Because this PDA is initialized only once, the first caller can create the vault against any mint; every later add_earnings / settle_earnings call is then forced to use that mint or fail, which effectively bricks USDC settlement for the node.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/init_node_vault.rs` around lines 30 -
45, The vault can be initialized against any mint because usdc_mint is
caller-controlled; lock it to the canonical USDC mint by removing caller control
and enforcing the canonical mint in the account constraint: replace or
supplement the caller-provided usdc_mint with a canonical_mint Account<'info,
Mint> (or add a canonical_mint parameter) and change the vault attribute from
token::mint = usdc_mint to token::mint = canonical_mint (or if you must keep
usdc_mint, add an explicit runtime check in the init handler asserting
usdc_mint.key() == CANONICAL_USDC_PUBKEY and fail early); update references to
usdc_mint / vault and use VAULT_SEED and args.node_id as before so the PDA
remains deterministic.

Comment on lines +11 to +13
* 1. Verifies the settlement program is deployed at SETTLEMENT_PROGRAM_ID
* 2. Registers a Helius webhook for settlement events → API_BASE_URL/webhooks/helius
* 3. Prints the webhook ID — save to .env as HELIUS_WEBHOOK_ID
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Header comment currently overstates behavior.

Line 11 says the script verifies deployment at SETTLEMENT_PROGRAM_ID, but no RPC/account check is performed. Either add that check or reword the comment to avoid false confidence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-devnet.ts` around lines 11 - 13, The header comment in
scripts/setup-devnet.ts incorrectly claims the script "Verifies the settlement
program is deployed at SETTLEMENT_PROGRAM_ID" while no RPC/account check exists;
update the header to accurately reflect behavior or add an actual on-chain
check. Either (A) change the comment text to remove "Verifies the settlement
program is deployed at SETTLEMENT_PROGRAM_ID" (e.g., state that the script
assumes SETTLEMENT_PROGRAM_ID is set) or (B) implement a runtime verification
that queries the RPC for the account/program at SETTLEMENT_PROGRAM_ID (using
your existing RPC client code) and fails/logs if not found, referencing the
identifier SETTLEMENT_PROGRAM_ID so reviewers can locate the relevant code path.

console.log(`API base URL: ${apiBaseUrl}`);

const helius = getHeliusClient();
const webhookUrl = `${apiBaseUrl}/webhooks/helius`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize API_BASE_URL before building webhookUrl.

Line 41 can create //webhooks/helius when API_BASE_URL ends with /. That may break idempotency matching in webhook registration and create duplicate webhooks.

Diff
-  const apiBaseUrl = process.env.API_BASE_URL;
+  const apiBaseUrl = process.env.API_BASE_URL?.replace(/\/+$/, "");
   if (!apiBaseUrl) {
@@
-  const webhookUrl = `${apiBaseUrl}/webhooks/helius`;
+  const webhookUrl = new URL("/webhooks/helius", `${apiBaseUrl}/`).toString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-devnet.ts` at line 41, The webhookUrl is built from apiBaseUrl
and can produce a double-slash when API_BASE_URL ends with a slash; update the
code that computes webhookUrl (the apiBaseUrl value derived from the
API_BASE_URL env and the webhookUrl constant) to normalize apiBaseUrl first by
trimming any trailing slash (or otherwise ensuring exactly one separator) before
concatenating "/webhooks/helius", so webhookUrl is always a single-correct URL
without duplicate slashes.

Comment on lines +52 to +64
const CLUSTER = (process.env.SOLANA_CLUSTER ?? "devnet") as "devnet" | "mainnet-beta";

function getRpcUrl(): string {
const apiKey = process.env.HELIUS_API_KEY;
if (apiKey) {
return CLUSTER === "mainnet-beta"
? `https://mainnet.helius-rpc.com/?api-key=${apiKey}`
: `https://devnet.helius-rpc.com/?api-key=${apiKey}`;
}
return CLUSTER === "mainnet-beta"
? "https://api.mainnet-beta.solana.com"
: "https://api.devnet.solana.com";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In TypeScript, does const x = process.env.SOLANA_CLUSTER as "devnet" | "mainnet-beta" validate the runtime value, or does it only narrow the static type?

💡 Result:

In TypeScript, const x = process.env.SOLANA_CLUSTER as "devnet" | "mainnet-beta" only narrows the static type at compile time. It does not validate the runtime value of process.env.SOLANA_CLUSTER. Type assertions using as are a compile-time-only feature that tell the TypeScript compiler to treat the value as the specified type without performing any runtime checks or changes to the value. The emitted JavaScript is identical to const x = process.env.SOLANA_CLUSTER;, so if the environment variable is "testnet", undefined, or any other value not in the union, x will hold that invalid value at runtime, potentially causing errors later when using x. For runtime validation of environment variables like this, you need explicit checks, such as: const cluster = process.env.SOLANA_CLUSTER; if (!cluster || (cluster !== "devnet" && cluster !== "mainnet-beta")) { throw new Error(Invalid SOLANA_CLUSTER: ${cluster}); } const x: "devnet" | "mainnet-beta" = cluster; // Now safe to assert Or use libraries like Zod for schema validation with type inference.

Citations:


🏁 Script executed:

# First, check if the file exists and get its line count
if [ -f "scripts/setup-mach-token.ts" ]; then
  wc -l scripts/setup-mach-token.ts
  echo "---"
  # Read the specific lines mentioned in the review
  echo "Lines 52-64:"
  sed -n '52,64p' scripts/setup-mach-token.ts
  echo "---"
  echo "Lines 164-167:"
  sed -n '164,167p' scripts/setup-mach-token.ts
fi

Repository: RustMunkey/maschina

Length of output: 818


Add runtime validation for SOLANA_CLUSTER environment variable.

The type assertion on line 52 only narrows the static type; it does not validate the runtime value. Any invalid string (e.g., "testnet", "staging") will silently fall through to devnet URLs in getRpcUrl(). Additionally, the explorer link and .env hints at lines 164–167 are hardcoded to devnet, which is inconsistent when CLUSTER is "mainnet-beta" and creates a confusing user experience.

Implement explicit runtime validation that throws an error for invalid cluster values, and update the explorer link and .env hints to reflect the actual cluster:

Proposed fix
-const CLUSTER = (process.env.SOLANA_CLUSTER ?? "devnet") as "devnet" | "mainnet-beta";
+function parseCluster(): "devnet" | "mainnet-beta" {
+  const value = (process.env.SOLANA_CLUSTER ?? "devnet").trim();
+  if (value !== "devnet" && value !== "mainnet-beta") {
+    throw new Error(`Invalid SOLANA_CLUSTER: ${value}`);
+  }
+  return value;
+}
+
+const CLUSTER = parseCluster();
@@
-  console.log(`  Explorer:  https://explorer.solana.com/address/${mintAddress}?cluster=devnet`);
+  const explorerSuffix = CLUSTER === "mainnet-beta" ? "" : `?cluster=${CLUSTER}`;
+  console.log(`  Explorer:  https://explorer.solana.com/address/${mintAddress}${explorerSuffix}`);
   console.log("\nAdd to .env:");
   console.log(`  MACH_MINT_ADDRESS=${mintAddress}`);
-  console.log("  SOLANA_CLUSTER=devnet");
+  console.log(`  SOLANA_CLUSTER=${CLUSTER}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 52 - 64, Add runtime validation for
the CLUSTER value and make explorer/.env hints reflect the actual cluster:
validate the environment variable assigned to CLUSTER (the variable named
CLUSTER) at startup and throw a clear error if it is not exactly "devnet" or
"mainnet-beta", so the type assertion is not relied on; keep getRpcUrl() as-is
but assume CLUSTER is validated beforehand; update the explorer link and the
.env hint strings (the code that constructs the explorer URL and the printed
.env example) to use CLUSTER dynamically (choose the appropriate explorer host
for "mainnet-beta" vs "devnet") so messages are consistent with the validated
cluster.

Comment on lines +66 to +71
function loadKeypair(): Keypair {
const walletPath =
process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
const raw = fs.readFileSync(walletPath, "utf-8");
const bytes = JSON.parse(raw) as number[];
return Keypair.fromSecretKey(Uint8Array.from(bytes));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Read SOLANA_WALLET_PATH here too.

The new .env.example advertises SOLANA_WALLET_PATH, but this script ignores it and only checks ANCHOR_WALLET. On any machine that does not use the default Solana CLI path, this will mint under the wrong authority or fail outright; Node also will not expand the ~/... example for you.

🛠️ Proposed fix
+function resolveWalletPath(): string {
+  const configured =
+    process.env.ANCHOR_WALLET ??
+    process.env.SOLANA_WALLET_PATH ??
+    path.join(os.homedir(), ".config", "solana", "id.json");
+
+  return configured.startsWith("~/")
+    ? path.join(os.homedir(), configured.slice(2))
+    : configured;
+}
+
 function loadKeypair(): Keypair {
-  const walletPath =
-    process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
+  const walletPath = resolveWalletPath();
   const raw = fs.readFileSync(walletPath, "utf-8");
   const bytes = JSON.parse(raw) as number[];
   return Keypair.fromSecretKey(Uint8Array.from(bytes));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function loadKeypair(): Keypair {
const walletPath =
process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
const raw = fs.readFileSync(walletPath, "utf-8");
const bytes = JSON.parse(raw) as number[];
return Keypair.fromSecretKey(Uint8Array.from(bytes));
function resolveWalletPath(): string {
const configured =
process.env.ANCHOR_WALLET ??
process.env.SOLANA_WALLET_PATH ??
path.join(os.homedir(), ".config", "solana", "id.json");
return configured.startsWith("~/")
? path.join(os.homedir(), configured.slice(2))
: configured;
}
function loadKeypair(): Keypair {
const walletPath = resolveWalletPath();
const raw = fs.readFileSync(walletPath, "utf-8");
const bytes = JSON.parse(raw) as number[];
return Keypair.fromSecretKey(Uint8Array.from(bytes));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 66 - 71, Update loadKeypair to
prefer SOLANA_WALLET_PATH, fall back to ANCHOR_WALLET, then default; expand a
leading tilde before reading the file so paths like ~/... work on Node (use
os.homedir() replacement), and then read and parse the JSON as before; reference
the loadKeypair function to locate where to change the environment variable
checks and path handling.

Comment on lines +384 to 387
let node_cents = (total as f64 * 0.70).floor() as i64;
let developer_cents = (total as f64 * 0.10).floor() as i64;
let treasury_cents = (total as f64 * 0.15).floor() as i64;
let validator_cents = total - node_cents - developer_cents - treasury_cents;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether run metadata exposes a first-party/developer signal,
# and where the earnings split is currently computed.

set -euo pipefail

echo "== Locate JobToRun definition =="
fd -i "scan_compat.rs" | xargs -r sed -n '1,260p'

echo
echo "== Search for developer/first-party attribution fields in daemon orchestrator =="
rg -n --type=rust -C3 '\b(first[_-]?party|developer|template_author|author_id|with_developer)\b' services/daemon/src

echo
echo "== Confirm all split computations in analyze path =="
rg -n --type=rust -C4 'record_node_earnings|node_cents|developer_cents|treasury_cents|validator_cents' services/daemon/src/orchestrator

Repository: RustMunkey/maschina

Length of output: 5922


🏁 Script executed:

#!/bin/bash

echo "== Find QueuedRun struct definition =="
rg -n --type=rust 'struct QueuedRun' -A 20

echo
echo "== Check packages/db schema for first-party documentation =="
if [ -f packages/db/src/schema/pg/nodes.ts ]; then
  sed -n '200,210p' packages/db/src/schema/pg/nodes.ts
else
  fd -i nodes.ts | head -5
fi

echo
echo "== Check marketplace test for first-party test case =="
if [ -f packages/marketplace/src/marketplace.test.ts ]; then
  sed -n '50,60p' packages/marketplace/src/marketplace.test.ts
else
  fd -i marketplace.test.ts | head -5
fi

echo
echo "== Search for any first-party or developer_id handling in analyze.rs =="
rg -n --type=rust -B5 -A10 'first.party|developer_id|template_author' services/daemon/src/orchestrator/analyze.rs

Repository: RustMunkey/maschina

Length of output: 1314


🏁 Script executed:

#!/bin/bash

echo "== Find QueuedRun with broader search =="
rg -n --type=rust 'QueuedRun' -B2 -A15 | head -80

echo
echo "== Check if developer_id exists anywhere in run context =="
rg -n --type=rust 'developer_id|is_first_party|first_party' services/daemon/src

echo
echo "== Look at full record_node_earnings signature and context =="
sed -n '361,420p' services/daemon/src/orchestrator/analyze.rs

Repository: RustMunkey/maschina

Length of output: 7631


Pass developer_id signal through JobToRun and apply conditional split for first-party agents.

Line 385 and 386 unconditionally allocate 10% to developer and 15% to treasury. However, JobToRun lacks any developer attribution field, making it impossible to distinguish first-party agents. For first-party agents, the split should be 70/25/0/5 (node/treasury/developer/validator) per the schema documentation and test expectations (packages/db/src/schema/pg/nodes.ts and packages/marketplace/src/marketplace.test.ts Line 51–56), not 70/15/10/5.

Additionally, the function comment (line 362) incorrectly states "70/15/10/5" when the code implements 70/10/15/5.

To fix: (1) extend JobToRun with a developer_id or is_first_party field from the agent/run metadata, (2) thread this through to record_node_earnings, and (3) apply conditional split logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/daemon/src/orchestrator/analyze.rs` around lines 384 - 387, The
earnings split currently hardcodes 70/10/15/5 and the comment is wrong; add a
developer attribution flag to JobToRun (e.g., developer_id: Option<Uuid> or
is_first_party: bool), populate it from the agent/run metadata where JobToRun is
constructed, pass that field through to record_node_earnings, and change the
split logic in analyze.rs: if first-party use 70/25/0/5
(node/treasury/developer/validator) else use 70/15/10/5
(node/treasury/developer/validator); also update the function comment to match
the implemented conditional behavior.

@RustMunkey RustMunkey enabled auto-merge (squash) March 27, 2026 21:14
@RustMunkey RustMunkey changed the title feat(chain): complete SPL token settlement with real USDC transfers feat(chain): settlement program, node identity, vanity wallets, devnet scripts Mar 28, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (8)
programs/settlement/src/instructions/init_node_vault.rs (1)

29-44: ⚠️ Potential issue | 🔴 Critical

Critical: Validate usdc_mint against canonical USDC to prevent bricking settlement.

The usdc_mint account is fully caller-controlled. Since this PDA is initialized only once, a malicious or mistaken first caller can create the vault against any mint. All subsequent add_earnings/settle_earnings calls would then fail or be forced to use that incorrect mint, effectively bricking USDC settlement for the node.

Proposed fix: Add canonical USDC validation

Option 1: Add a runtime check in the handler:

+use crate::errors::SettlementError;
+
+// Canonical USDC mints
+pub const USDC_MINT_DEVNET: &str = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
+pub const USDC_MINT_MAINNET: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

 pub fn handler(ctx: Context<InitNodeVault>, _args: InitNodeVaultArgs) -> Result<()> {
+    // Validate usdc_mint is canonical USDC (devnet or mainnet)
+    let mint_key = ctx.accounts.usdc_mint.key().to_string();
+    require!(
+        mint_key == USDC_MINT_DEVNET || mint_key == USDC_MINT_MAINNET,
+        SettlementError::InvalidUsdcMint
+    );
+
     // The account is fully initialised by the #[account(init)] constraint.

Option 2: Store canonical USDC mint in SettlementConfig and validate against it:

 #[derive(Accounts)]
 #[instruction(args: InitNodeVaultArgs)]
 pub struct InitNodeVault<'info> {
+    /// Global config — contains canonical USDC mint for this cluster.
+    #[account(
+        seeds = [SettlementConfig::SEED],
+        bump = config.bump,
+    )]
+    pub config: Account<'info, SettlementConfig>,
+
     /// USDC mint — must match config.usdc_mint.
-    pub usdc_mint: Account<'info, Mint>,
+    #[account(
+        constraint = usdc_mint.key() == config.usdc_mint @ SettlementError::InvalidUsdcMint
+    )]
+    pub usdc_mint: Account<'info, Mint>,

Option 2 is preferred as it keeps the canonical mint configurable per-cluster via initialize_config.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/init_node_vault.rs` around lines 29 -
44, The vault init currently accepts a caller-provided usdc_mint allowing a
malicious first initializer to bind the vault to a wrong mint; add validation to
ensure the mint equals the canonical USDC mint stored in your config: extend
SettlementConfig to include a canonical_usdc_mint field (set via
initialize_config), then in the init_node_vault handler check that the provided
usdc_mint.key() matches SettlementConfig::canonical_usdc_mint and return an
error if not; update any callers/tests to supply the canonical mint and keep
add_earnings/settle_earnings behavior unchanged but now safe because
vault/usdc_mint are validated.
scripts/setup-mach-token.ts (3)

67-73: ⚠️ Potential issue | 🟠 Major

Read SOLANA_WALLET_PATH and expand tilde paths.

The .env.example advertises SOLANA_WALLET_PATH, but this script only checks ANCHOR_WALLET. Paths like ~/.config/solana/id.json won't expand correctly in Node.js.

Proposed fix
 function loadKeypair(): Keypair {
-  const walletPath =
-    process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
+  const configuredPath =
+    process.env.ANCHOR_WALLET ??
+    process.env.SOLANA_WALLET_PATH ??
+    path.join(os.homedir(), ".config", "solana", "id.json");
+  
+  const walletPath = configuredPath.startsWith("~/")
+    ? path.join(os.homedir(), configuredPath.slice(2))
+    : configuredPath;
+
   const raw = fs.readFileSync(walletPath, "utf-8");
   const bytes = JSON.parse(raw) as number[];
   return Keypair.fromSecretKey(Uint8Array.from(bytes));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 67 - 73, The loadKeypair function
currently only reads ANCHOR_WALLET and doesn't expand tilde paths; update
loadKeypair to prefer SOLANA_WALLET_PATH (fall back to ANCHOR_WALLET then the
existing default), and ensure any path starting with "~" is expanded to the user
home directory (e.g., replace leading "~" with os.homedir()) before passing to
fs.readFileSync/Path APIs so "~/.config/solana/id.json" resolves correctly; keep
the rest of the logic (JSON parse and Keypair.fromSecretKey) unchanged.

52-65: ⚠️ Potential issue | 🟡 Minor

Add runtime validation for SOLANA_CLUSTER.

The CLUSTER variable accepts any string at runtime. Invalid values like "testnet" or "staging" will silently fall through to devnet URLs in getRpcUrl(). The code also supports "localnet" (line 55) which isn't documented.

Proposed fix
-const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet";
+function parseCluster(): "devnet" | "mainnet-beta" | "localnet" {
+  const value = (process.env.SOLANA_CLUSTER ?? "devnet").trim();
+  if (value !== "devnet" && value !== "mainnet-beta" && value !== "localnet") {
+    throw new Error(`Invalid SOLANA_CLUSTER: ${value}. Must be "devnet", "mainnet-beta", or "localnet".`);
+  }
+  return value;
+}
+
+const CLUSTER = parseCluster();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 52 - 65, The CLUSTER value (from
SOLANA_CLUSTER) is not validated and can silently map unknown strings to devnet;
update getRpcUrl (or the initialization of CLUSTER) to validate the environment
value against an explicit allowed set (e.g., "localnet", "devnet",
"mainnet-beta"), and if the value is invalid either throw an Error or log a
clear message and exit so misconfigured deployments fail fast; make sure to
reference CLUSTER and getRpcUrl in the message and state the supported values so
callers know the valid options.

172-176: ⚠️ Potential issue | 🟡 Minor

Make explorer link and .env hints dynamic based on CLUSTER.

The explorer URL and .env guidance are hardcoded to devnet. When running with CLUSTER=mainnet-beta, this creates a misleading user experience.

Proposed fix
-  console.log(`  Explorer:  https://explorer.solana.com/address/${mintAddress}?cluster=devnet`);
+  const explorerSuffix = CLUSTER === "mainnet-beta" ? "" : `?cluster=${CLUSTER}`;
+  console.log(`  Explorer:  https://explorer.solana.com/address/${mintAddress}${explorerSuffix}`);
   console.log("\nAdd to .env:");
   console.log(`  MACH_MINT_ADDRESS=${mintAddress}`);
-  console.log("  SOLANA_CLUSTER=devnet");
+  console.log(`  SOLANA_CLUSTER=${CLUSTER}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 172 - 176, The printed explorer URL
and .env hints are hardcoded to devnet; update the console output to use the
CLUSTER value (read from process.env.CLUSTER or the existing cluster variable)
and fall back to "devnet" if unset, so the Explorer link (constructed with
mintAddress and the cluster query param) and the suggested SOLANA_CLUSTER value
reflect the actual cluster (e.g., "devnet" or "mainnet-beta"); change the three
console.log lines that reference mintAddress and the hardcoded "devnet" to
interpolate the resolved cluster variable instead.
scripts/setup-devnet.ts (2)

28-41: ⚠️ Potential issue | 🟠 Major

Normalize API_BASE_URL to prevent double slashes in webhook URL.

When API_BASE_URL ends with /, the concatenation creates //webhooks/helius, which may break idempotency matching and create duplicate webhooks.

Proposed fix
-  const apiBaseUrl = process.env.API_BASE_URL;
+  const apiBaseUrl = process.env.API_BASE_URL?.replace(/\/+$/, "");
   if (!apiBaseUrl) {
     console.error(
       "API_BASE_URL is not set. Set it to your public API URL (e.g. https://api.maschina.ai)",
     );
     process.exit(1);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-devnet.ts` around lines 28 - 41, The webhook URL construction
can produce a double slash when API_BASE_URL ends with a slash; normalize
apiBaseUrl before building webhookUrl by trimming any trailing slash (operate on
the apiBaseUrl variable obtained from process.env.API_BASE_URL) or use a
URL-join approach when composing the webhookUrl variable; update the logic where
const apiBaseUrl is validated and where const webhookUrl =
`${apiBaseUrl}/webhooks/helius` is defined (and keep getHeliusClient() usages
unchanged) so webhookUrl always becomes "<origin>/webhooks/helius" without
duplicated slashes.

10-14: ⚠️ Potential issue | 🟡 Minor

Header comment overstates behavior.

Line 11 states the script "Verifies the settlement program is deployed" but no RPC/account check is performed. Either add actual verification or reword the comment.

Proposed fix (update comment)
 * What it does:
-*   1. Verifies the settlement program is deployed at SETTLEMENT_PROGRAM_ID
-*   2. Registers a Helius webhook for settlement events → API_BASE_URL/webhooks/helius
-*   3. Prints the webhook ID — save to .env as HELIUS_WEBHOOK_ID
+*   1. Registers a Helius webhook for settlement events → API_BASE_URL/webhooks/helius
+*   2. Prints the webhook ID — save to .env as HELIUS_WEBHOOK_ID
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-devnet.ts` around lines 10 - 14, The header comment claims the
script "Verifies the settlement program is deployed at SETTLEMENT_PROGRAM_ID"
but no RPC/account check exists; either remove or reword that claim or implement
an actual verification: locate the SETTLEMENT_PROGRAM_ID constant in
scripts/setup-devnet.ts and either change the header to say the script "assumes
the settlement program is deployed at SETTLEMENT_PROGRAM_ID" or add a
verification step using the Solana Connection (e.g., connection.getAccountInfo
or getProgramAccounts) to check for an account/executable at
SETTLEMENT_PROGRAM_ID and fail/log if missing before proceeding to the Helius
webhook registration and printing of HELIUS_WEBHOOK_ID.
programs/settlement/src/instructions/add_earnings.rs (2)

16-28: ⚠️ Potential issue | 🔴 Critical

run_id still lacks on-chain deduplication—replay vulnerability persists.

The run_id is only emitted in the event (line 118) but not persisted. If the settlement worker retries or replays a run, this instruction:

  1. Transfers USDC again from the authority
  2. Increments pending_* balances again
  3. Bumps run_count again

For real-money settlement, persist a receipt PDA keyed by run_id and reject duplicates before moving funds.

🛡️ Sketch of dedupe approach

Add a receipt account to prevent replay:

/// Ensures this run_id hasn't been processed before.
#[account(
    init,
    payer = authority,
    seeds = [b"earnings_receipt", args.run_id.as_ref()],
    bump,
    space = 8 + 1, // discriminator + processed flag
)]
pub receipt: Account<'info, EarningsReceipt>,

If the receipt already exists, Anchor's init constraint will fail with AccountAlreadyInUse, providing automatic idempotency.

Also applies to: 116-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/add_earnings.rs` around lines 16 - 28,
The AddEarnings instruction currently only emits run_id in an event and is
vulnerable to replay because run_id is not persisted; add a receipt PDA keyed by
args.run_id to enforce idempotency by rejecting duplicate runs before moving
funds. Modify the AddEarnings handler to require an Account<'info',
EarningsReceipt> (initialized with seeds [b"earnings_receipt",
args.run_id.as_ref()] and payer authority) so Anchor will fail on duplicate
receipts, and check/mark the receipt as processed (EarningsReceipt struct with a
processed flag or just existence) before performing transfers, balance updates,
and bumping run_count.

35-48: ⚠️ Potential issue | 🔴 Critical

add_earnings still missing authority validation against SettlementConfig.

Unlike settle_earnings which validates authority.key() == config.authority, this instruction accepts any signer. This allows arbitrary callers to fund the vault and manipulate pending_* balances, potentially inflating payouts or emitting fraudulent EarningsRecorded events.

Add a config account and constrain the authority:

🔒 Proposed fix
 #[derive(Accounts)]
 #[instruction(args: AddEarningsArgs)]
 pub struct AddEarnings<'info> {
-    /// Settlement authority (Maschina multisig) — signs and funds the transfer.
-    #[account(mut)]
+    /// Settlement authority — must match config.authority.
+    #[account(
+        mut,
+        constraint = authority.key() == config.authority @ SettlementError::UnauthorisedSettlement,
+    )]
     pub authority: Signer<'info>,
+
+    /// Global settlement config — holds the trusted authority key.
+    #[account(
+        seeds = [SettlementConfig::SEED],
+        bump = config.bump,
+    )]
+    pub config: Account<'info, SettlementConfig>,

This requires importing SettlementConfig in line 5.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/add_earnings.rs` around lines 35 - 48,
The add_earnings instruction is missing validation that the signer `authority`
matches the configured multisig; add a `config: Account<'info,
SettlementConfig>` to the instruction context and enforce the constraint that
`config.authority == authority.key()` (mirroring settle_earnings). Update the
account attributes for `config` to be read-only (or as required) and adjust the
handler to use `config` instead of accepting any signer; also import
`SettlementConfig` at the top of the file so the type is available. Ensure
existing logic that updates `pending_*` and emits `EarningsRecorded` remains
unchanged but now only executes when `authority` is validated against `config`.
🧹 Nitpick comments (5)
programs/settlement/src/instructions/initialize_config.rs (2)

3-3: Remove unused import.

SettlementError is imported but not used in this file.

Proposed fix
 use anchor_lang::prelude::*;

-use crate::errors::SettlementError;
 use crate::state::SettlementConfig;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/initialize_config.rs` at line 3, The
import SettlementError is unused in this file; remove the use stmt referencing
SettlementError (the line "use crate::errors::SettlementError;") from
initialize_config.rs so only actually used imports remain (verify functions like
initialize_config or structs referenced in the file still compile and remove any
other stale imports if flagged by the compiler).

41-48: Consider validating that payout pubkeys are not the default/system program.

Accidentally passing Pubkey::default() or the system program ID for treasury/developer/validators keys would brick settlement. A simple validation could prevent misconfiguration.

Proposed validation
 pub fn handler(ctx: Context<InitializeConfig>, args: InitializeConfigArgs) -> Result<()> {
+    // Sanity check: payout keys should not be default or system program
+    require!(
+        args.treasury_key != Pubkey::default() && args.treasury_key != System::id(),
+        SettlementError::InvalidPayoutKey
+    );
+    require!(
+        args.developer_key != Pubkey::default() && args.developer_key != System::id(),
+        SettlementError::InvalidPayoutKey
+    );
+    require!(
+        args.validators_key != Pubkey::default() && args.validators_key != System::id(),
+        SettlementError::InvalidPayoutKey
+    );
+
     let config = &mut ctx.accounts.config;

This would require keeping the SettlementError import and adding InvalidPayoutKey to the error enum.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/initialize_config.rs` around lines 41 -
48, In handler (fn handler(ctx: Context<InitializeConfig>, args:
InitializeConfigArgs)) add validations that args.treasury_key,
args.developer_key, and args.validators_key are not Pubkey::default() and not
equal to solana_program::system_program::ID; if any are invalid return
Err(SettlementError::InvalidPayoutKey.into()). Keep existing assignments to
config.authority and config.bump but perform these checks before setting
config.* fields; ensure the SettlementError enum includes InvalidPayoutKey and
that the SettlementError import is retained.
scripts/setup-realms.ts (1)

61-82: Extract shared utilities to reduce duplication.

CLUSTER, getRpcUrl(), and loadKeypair() are duplicated from setup-mach-token.ts with identical logic. This also inherits the same issues: no runtime validation for CLUSTER, no SOLANA_WALLET_PATH support, and no tilde expansion.

Consider extracting these to a shared module (e.g., scripts/lib/config.ts) to fix bugs in one place and maintain consistency.

Example shared module
// scripts/lib/config.ts
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Keypair } from "@solana/web3.js";

export type Cluster = "devnet" | "mainnet-beta" | "localnet";

export function getCluster(): Cluster {
  const value = (process.env.SOLANA_CLUSTER ?? "devnet").trim();
  if (value !== "devnet" && value !== "mainnet-beta" && value !== "localnet") {
    throw new Error(`Invalid SOLANA_CLUSTER: ${value}`);
  }
  return value;
}

export function getRpcUrl(cluster: Cluster = getCluster()): string {
  if (cluster === "localnet") return "http://127.0.0.1:8899";
  const apiKey = process.env.HELIUS_API_KEY;
  if (apiKey) {
    return cluster === "mainnet-beta"
      ? `https://mainnet.helius-rpc.com/?api-key=${apiKey}`
      : `https://devnet.helius-rpc.com/?api-key=${apiKey}`;
  }
  return cluster === "mainnet-beta"
    ? "https://api.mainnet-beta.solana.com"
    : "https://api.devnet.solana.com";
}

export function loadKeypair(): Keypair {
  const configuredPath =
    process.env.ANCHOR_WALLET ??
    process.env.SOLANA_WALLET_PATH ??
    path.join(os.homedir(), ".config", "solana", "id.json");
  
  const walletPath = configuredPath.startsWith("~/")
    ? path.join(os.homedir(), configuredPath.slice(2))
    : configuredPath;

  const raw = fs.readFileSync(walletPath, "utf-8");
  const bytes = JSON.parse(raw) as number[];
  return Keypair.fromSecretKey(Uint8Array.from(bytes));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-realms.ts` around lines 61 - 82, Extract the duplicated
CLUSTER, getRpcUrl(), and loadKeypair() logic into a shared module (e.g.,
scripts/lib/config.ts) and replace the in-file definitions with imports; in the
shared module implement getCluster() that validates SOLANA_CLUSTER values
("devnet" | "mainnet-beta" | "localnet") and use that in getRpcUrl(cluster) to
preserve existing behavior, update loadKeypair() to prefer SOLANA_WALLET_PATH
then ANCHOR_WALLET, perform tilde (~) expansion for home dir, and throw clear
errors on invalid/missing configs so fixes apply consistently across
setup-realms.ts and setup-mach-token.ts.
scripts/setup-mach-token.ts (1)

117-121: Move constant definitions before usage for clarity.

TYPE_SIZE and LENGTH_SIZE are used at line 117 but defined at lines 179-180. While JavaScript hoisting makes this work, it harms readability.

Proposed fix

Move these constants to the Config section near the top:

 // ─── Config ───────────────────────────────────────────────────────────────────

+// Token-2022 metadata size constants
+const TYPE_SIZE = 2;
+const LENGTH_SIZE = 4;
+
 const MACH_TOKEN = {

And remove them from the bottom of the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 117 - 121, The constants TYPE_SIZE
and LENGTH_SIZE are used when computing metadataExtension and mintLamports but
are defined much later, harming readability; move the constant definitions for
TYPE_SIZE and LENGTH_SIZE up into the Config/top section near other
configuration constants so they are declared before they are referenced
(affecting the computation of metadataExtension, mintLen via
getMintLen([ExtensionType.MetadataPointer]) and the call to
connection.getMinimumBalanceForRentExemption), and remove the duplicate
definitions from the bottom of the file so there’s a single, top-level
declaration.
programs/settlement/src/instructions/settle_earnings.rs (1)

120-122: Verify signer seeds match the pool PDA derivation.

The pool PDA is derived with seeds = [b"pool", args.node_id.as_ref()] (line 43). Here, &args.node_id yields &[u8; 16] which coerces to &[u8], so this is correct. However, for consistency and clarity with the accounts constraint, consider using .as_ref():

-    let pool_seeds: &[&[u8]] = &[b"pool", &args.node_id, &[pool.bump]];
+    let pool_seeds: &[&[u8]] = &[b"pool", args.node_id.as_ref(), &[pool.bump]];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/settle_earnings.rs` around lines 120 -
122, The PDA signer seeds in settle_earnings.rs use &args.node_id which works
but should be replaced with args.node_id.as_ref() for clarity and to match the
PDA derivation used elsewhere; update the pool_seeds declaration (and the
signer_seeds usage) to use &[b"pool", args.node_id.as_ref(), &[pool.bump]] so
the seeds and signer_seeds (pool_seeds, signer_seeds) clearly mirror the
original PDA derivation and types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/setup-realms.ts`:
- Around line 114-127: The call to withCreateRealm is missing the required
connection parameter and so all subsequent arguments are shifted; update the
call to pass your Connection instance as the second argument (so the order
becomes: instructions, connection, GOVERNANCE_PROGRAM_ID, programVersion,
REALM_NAME, realmAuthority (authority.publicKey), communityMint, payer
(authority.publicKey), ...), and verify MIN_TOKENS_TO_CREATE_GOVERNANCE and
MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION are passed in their correct
positions per the withCreateRealm signature to restore proper argument
alignment.

In `@scripts/test-settlement-e2e.ts`:
- Around line 336-338: The log message uses the SOL-specific term "lamports";
update the console.log that prints `${totalOk ? "✓" : "✗"} Total distributed:
${total} / ${EARNINGS_AMOUNT} lamports` to use USDC terminology instead (e.g.,
"USDC (base units)" or "micro‑USDC") so it correctly reflects USDC's 6-decimal
base unit; change the literal "lamports" to the chosen USDC label and keep the
existing variables total, totalOk, and EARNINGS_AMOUNT unchanged.
- Around line 256-257: The operatorProgram is being constructed without an
explicit program ID, causing it to differ from the main program instance; update
the instantiation of operatorProgram (where Program, idl, and operatorProvider
are used) to pass the same explicit program ID as the main program (e.g., use
program.programId or the shared PROGRAM_ID constant as the second argument) and
move operatorProvider to the provider/third argument so operatorProgram uses the
exact same program ID as program.
- Around line 129-142: The Program constructor is not using the programId loaded
from SETTLEMENT_PROGRAM_ID so transactions may target the IDL's embedded
address; update the IDL's address field or override it before constructing the
Anchor Program: set idl.address = programId.toString() (or otherwise load/verify
settlement.json contains the same address) and then call new Program(idl as any,
provider) (also apply the same fix for operatorProgram created at the later
block). Additionally, add a runtime check that idl.address matches programId and
bail with an error if they differ to prevent silent mis-targeting.
- Around line 173-174: Replace the deprecated confirmTransaction(signature) call
by using the blockheight-based confirmation strategy: after obtaining airdropSig
from connection.requestAirdrop, call connection.getLatestBlockhash() to retrieve
{blockhash, lastValidBlockHeight} and then call connection.confirmTransaction({
signature: airdropSig, blockhash, lastValidBlockHeight }) (optionally with a
commitment) so the code uses the
BlockheightBasedTransactionConfirmationStrategy; update references around
airdropSig, connection.requestAirdrop, and connection.confirmTransaction
accordingly.

---

Duplicate comments:
In `@programs/settlement/src/instructions/add_earnings.rs`:
- Around line 16-28: The AddEarnings instruction currently only emits run_id in
an event and is vulnerable to replay because run_id is not persisted; add a
receipt PDA keyed by args.run_id to enforce idempotency by rejecting duplicate
runs before moving funds. Modify the AddEarnings handler to require an
Account<'info', EarningsReceipt> (initialized with seeds [b"earnings_receipt",
args.run_id.as_ref()] and payer authority) so Anchor will fail on duplicate
receipts, and check/mark the receipt as processed (EarningsReceipt struct with a
processed flag or just existence) before performing transfers, balance updates,
and bumping run_count.
- Around line 35-48: The add_earnings instruction is missing validation that the
signer `authority` matches the configured multisig; add a `config:
Account<'info, SettlementConfig>` to the instruction context and enforce the
constraint that `config.authority == authority.key()` (mirroring
settle_earnings). Update the account attributes for `config` to be read-only (or
as required) and adjust the handler to use `config` instead of accepting any
signer; also import `SettlementConfig` at the top of the file so the type is
available. Ensure existing logic that updates `pending_*` and emits
`EarningsRecorded` remains unchanged but now only executes when `authority` is
validated against `config`.

In `@programs/settlement/src/instructions/init_node_vault.rs`:
- Around line 29-44: The vault init currently accepts a caller-provided
usdc_mint allowing a malicious first initializer to bind the vault to a wrong
mint; add validation to ensure the mint equals the canonical USDC mint stored in
your config: extend SettlementConfig to include a canonical_usdc_mint field (set
via initialize_config), then in the init_node_vault handler check that the
provided usdc_mint.key() matches SettlementConfig::canonical_usdc_mint and
return an error if not; update any callers/tests to supply the canonical mint
and keep add_earnings/settle_earnings behavior unchanged but now safe because
vault/usdc_mint are validated.

In `@scripts/setup-devnet.ts`:
- Around line 28-41: The webhook URL construction can produce a double slash
when API_BASE_URL ends with a slash; normalize apiBaseUrl before building
webhookUrl by trimming any trailing slash (operate on the apiBaseUrl variable
obtained from process.env.API_BASE_URL) or use a URL-join approach when
composing the webhookUrl variable; update the logic where const apiBaseUrl is
validated and where const webhookUrl = `${apiBaseUrl}/webhooks/helius` is
defined (and keep getHeliusClient() usages unchanged) so webhookUrl always
becomes "<origin>/webhooks/helius" without duplicated slashes.
- Around line 10-14: The header comment claims the script "Verifies the
settlement program is deployed at SETTLEMENT_PROGRAM_ID" but no RPC/account
check exists; either remove or reword that claim or implement an actual
verification: locate the SETTLEMENT_PROGRAM_ID constant in
scripts/setup-devnet.ts and either change the header to say the script "assumes
the settlement program is deployed at SETTLEMENT_PROGRAM_ID" or add a
verification step using the Solana Connection (e.g., connection.getAccountInfo
or getProgramAccounts) to check for an account/executable at
SETTLEMENT_PROGRAM_ID and fail/log if missing before proceeding to the Helius
webhook registration and printing of HELIUS_WEBHOOK_ID.

In `@scripts/setup-mach-token.ts`:
- Around line 67-73: The loadKeypair function currently only reads ANCHOR_WALLET
and doesn't expand tilde paths; update loadKeypair to prefer SOLANA_WALLET_PATH
(fall back to ANCHOR_WALLET then the existing default), and ensure any path
starting with "~" is expanded to the user home directory (e.g., replace leading
"~" with os.homedir()) before passing to fs.readFileSync/Path APIs so
"~/.config/solana/id.json" resolves correctly; keep the rest of the logic (JSON
parse and Keypair.fromSecretKey) unchanged.
- Around line 52-65: The CLUSTER value (from SOLANA_CLUSTER) is not validated
and can silently map unknown strings to devnet; update getRpcUrl (or the
initialization of CLUSTER) to validate the environment value against an explicit
allowed set (e.g., "localnet", "devnet", "mainnet-beta"), and if the value is
invalid either throw an Error or log a clear message and exit so misconfigured
deployments fail fast; make sure to reference CLUSTER and getRpcUrl in the
message and state the supported values so callers know the valid options.
- Around line 172-176: The printed explorer URL and .env hints are hardcoded to
devnet; update the console output to use the CLUSTER value (read from
process.env.CLUSTER or the existing cluster variable) and fall back to "devnet"
if unset, so the Explorer link (constructed with mintAddress and the cluster
query param) and the suggested SOLANA_CLUSTER value reflect the actual cluster
(e.g., "devnet" or "mainnet-beta"); change the three console.log lines that
reference mintAddress and the hardcoded "devnet" to interpolate the resolved
cluster variable instead.

---

Nitpick comments:
In `@programs/settlement/src/instructions/initialize_config.rs`:
- Line 3: The import SettlementError is unused in this file; remove the use stmt
referencing SettlementError (the line "use crate::errors::SettlementError;")
from initialize_config.rs so only actually used imports remain (verify functions
like initialize_config or structs referenced in the file still compile and
remove any other stale imports if flagged by the compiler).
- Around line 41-48: In handler (fn handler(ctx: Context<InitializeConfig>,
args: InitializeConfigArgs)) add validations that args.treasury_key,
args.developer_key, and args.validators_key are not Pubkey::default() and not
equal to solana_program::system_program::ID; if any are invalid return
Err(SettlementError::InvalidPayoutKey.into()). Keep existing assignments to
config.authority and config.bump but perform these checks before setting
config.* fields; ensure the SettlementError enum includes InvalidPayoutKey and
that the SettlementError import is retained.

In `@programs/settlement/src/instructions/settle_earnings.rs`:
- Around line 120-122: The PDA signer seeds in settle_earnings.rs use
&args.node_id which works but should be replaced with args.node_id.as_ref() for
clarity and to match the PDA derivation used elsewhere; update the pool_seeds
declaration (and the signer_seeds usage) to use &[b"pool",
args.node_id.as_ref(), &[pool.bump]] so the seeds and signer_seeds (pool_seeds,
signer_seeds) clearly mirror the original PDA derivation and types.

In `@scripts/setup-mach-token.ts`:
- Around line 117-121: The constants TYPE_SIZE and LENGTH_SIZE are used when
computing metadataExtension and mintLamports but are defined much later, harming
readability; move the constant definitions for TYPE_SIZE and LENGTH_SIZE up into
the Config/top section near other configuration constants so they are declared
before they are referenced (affecting the computation of metadataExtension,
mintLen via getMintLen([ExtensionType.MetadataPointer]) and the call to
connection.getMinimumBalanceForRentExemption), and remove the duplicate
definitions from the bottom of the file so there’s a single, top-level
declaration.

In `@scripts/setup-realms.ts`:
- Around line 61-82: Extract the duplicated CLUSTER, getRpcUrl(), and
loadKeypair() logic into a shared module (e.g., scripts/lib/config.ts) and
replace the in-file definitions with imports; in the shared module implement
getCluster() that validates SOLANA_CLUSTER values ("devnet" | "mainnet-beta" |
"localnet") and use that in getRpcUrl(cluster) to preserve existing behavior,
update loadKeypair() to prefer SOLANA_WALLET_PATH then ANCHOR_WALLET, perform
tilde (~) expansion for home dir, and throw clear errors on invalid/missing
configs so fixes apply consistently across setup-realms.ts and
setup-mach-token.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4d5d22a-1c8a-4c3f-b886-e8a6d7fdb57d

📥 Commits

Reviewing files that changed from the base of the PR and between ceb00bf and 99dd9a7.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • programs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (31)
  • .env.example
  • .gitignore
  • apps/docs/src/content/introduction.mdx
  • apps/docs/src/routes/introduction.tsx
  • apps/web/src/routes/about.tsx
  • apps/web/src/routes/careers.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/routes/marketplace.tsx
  • apps/web/src/routes/press-kit.tsx
  • apps/web/src/routes/security.tsx
  • ascii-logo.txt
  • packages/chain/package.json
  • packages/marketplace/src/marketplace.test.ts
  • programs/Anchor.toml
  • programs/Cargo.toml
  • programs/settlement/Cargo.toml
  • programs/settlement/src/errors.rs
  • programs/settlement/src/instructions/add_earnings.rs
  • programs/settlement/src/instructions/init_node_vault.rs
  • programs/settlement/src/instructions/initialize_config.rs
  • programs/settlement/src/instructions/mod.rs
  • programs/settlement/src/instructions/register_node.rs
  • programs/settlement/src/instructions/settle_earnings.rs
  • programs/settlement/src/lib.rs
  • programs/settlement/src/state.rs
  • programs/settlement/target/idl/settlement.json
  • scripts/discord-scaffold.mjs
  • scripts/setup-devnet.ts
  • scripts/setup-mach-token.ts
  • scripts/setup-realms.ts
  • scripts/test-settlement-e2e.ts
✅ Files skipped from review due to trivial changes (13)
  • ascii-logo.txt
  • .gitignore
  • apps/docs/src/content/introduction.mdx
  • apps/web/src/routes/careers.tsx
  • apps/web/src/routes/press-kit.tsx
  • apps/web/src/routes/about.tsx
  • apps/docs/src/routes/introduction.tsx
  • apps/web/src/routes/security.tsx
  • apps/web/src/routes/marketplace.tsx
  • programs/Cargo.toml
  • apps/web/src/routes/index.tsx
  • packages/chain/package.json
  • programs/settlement/target/idl/settlement.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • programs/settlement/Cargo.toml
  • programs/settlement/src/instructions/mod.rs
  • programs/settlement/src/errors.rs
  • programs/Anchor.toml
  • packages/marketplace/src/marketplace.test.ts
  • scripts/discord-scaffold.mjs

Comment on lines +114 to +127
const instructions: Parameters<typeof withCreateRealm>[0] = [];

const realmAddress = await withCreateRealm(
instructions,
GOVERNANCE_PROGRAM_ID,
programVersion,
REALM_NAME,
authority.publicKey, // realm authority
communityMint,
authority.publicKey, // payer
undefined, // no council mint — MACH-only governance
MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION, // all MACH supply counts for quorum
MIN_TOKENS_TO_CREATE_GOVERNANCE,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the installed version of `@solana/spl-governance`
cat package.json | jq '.dependencies["@solana/spl-governance"] // .devDependencies["@solana/spl-governance"]'

# Search for withCreateRealm usage patterns in the codebase
rg -n "withCreateRealm" --type=ts -C3

Repository: RustMunkey/maschina

Length of output: 921


🏁 Script executed:

#!/bin/bash
# Read the actual package.json to see all dependencies
head -100 package.json

# Check for lock files that might contain version info
ls -la | grep -E "package-lock|yarn.lock|pnpm-lock"

# Check if there's a workspace configuration
if [ -f "pnpm-workspace.yaml" ] || [ -f "lerna.json" ]; then
  echo "Found workspace config"
fi

Repository: RustMunkey/maschina

Length of output: 5554


🏁 Script executed:

#!/bin/bash
# Look for `@solana/spl-governance` in lock files
if [ -f "package-lock.json" ]; then
  rg -o '"@solana/spl-governance"[^}]*"version"[^,]*' package-lock.json | head -5
elif [ -f "yarn.lock" ]; then
  rg '@solana/spl-governance@' yarn.lock | head -5
elif [ -f "pnpm-lock.yaml" ]; then
  rg '@solana/spl-governance' pnpm-lock.yaml | head -5
fi

Repository: RustMunkey/maschina

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Try to find the actual type signature of withCreateRealm
# by searching for .d.ts files or looking at node_modules
find . -name "*.d.ts" -path "*spl-governance*" 2>/dev/null | head -5

# If node_modules exists, check the types directly
if [ -d "node_modules/@solana/spl-governance" ]; then
  ls node_modules/@solana/spl-governance/ | head -20
fi

Repository: RustMunkey/maschina

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for any TypeScript type definitions or imports related to withCreateRealm
rg -n "withCreateRealm" -B5 -A10 scripts/setup-realms.ts

# Look for any tsconfig or type configuration
find . -maxdepth 2 -name "tsconfig.json" -o -name "tsconfig*.json" 2>/dev/null | head -5

# Search for any type stub files or .d.ts files in the repo
find . -name "*.d.ts" -not -path "*/node_modules/*" 2>/dev/null | head -10

Repository: RustMunkey/maschina

Length of output: 1401


🌐 Web query:

@solana/spl-governance 0.3.28 withCreateRealm function signature

💡 Result:

The function withCreateRealm in @solana/spl-governance 0.3.28 is an instruction builder that appends a createRealm instruction to a provided array of TransactionInstruction[]. It returns the PDA address of the created realm. Signature (TypeScript): withCreateRealm( instructions: TransactionInstruction[], connection: Connection, programId: PublicKey, programVersion: 'v1' | 'legacy', name: string, realmAuthority: PublicKey, communityMint: PublicKey, payer: PublicKey, councilMint?: PublicKey, communityMintMaxVoteWeightSource?: MintMaxVoteWeightSource, additionalAccounts?: GoverningTokenConfigAccountArgs[] ): Promise Example usage pattern from sources: const instructions: TransactionInstruction[] = []; const realmAddress = await withCreateRealm(instructions, connection, programId, 'v1', 'MyDAO', authority, communityMint, payer); This follows the SDK's builder pattern where functions prefixed with with modify an instructions array. Confirmed present in package exports via Deno issue listing default exports including withCreateRealm. Similar Realms SDK documentation shows exact usage for realm creation. NPM file listing shows related createRealm functions in lib/governance.

Citations:


Fix withCreateRealm function call: missing connection parameter and misaligned arguments.

The function signature in @solana/spl-governance 0.3.28 expects withCreateRealm(instructions, connection, programId, programVersion, name, realmAuthority, communityMint, payer, ...), but the call at lines 116–126 is missing the connection parameter and has all subsequent arguments shifted. The connection should be the second argument. Additionally, the type annotation Parameters<typeof withCreateRealm>[0] only validates the first parameter; it does not catch the misalignment of the remaining arguments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-realms.ts` around lines 114 - 127, The call to withCreateRealm
is missing the required connection parameter and so all subsequent arguments are
shifted; update the call to pass your Connection instance as the second argument
(so the order becomes: instructions, connection, GOVERNANCE_PROGRAM_ID,
programVersion, REALM_NAME, realmAuthority (authority.publicKey), communityMint,
payer (authority.publicKey), ...), and verify MIN_TOKENS_TO_CREATE_GOVERNANCE
and MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION are passed in their correct
positions per the withCreateRealm signature to restore proper argument
alignment.

Comment on lines +129 to +142
const programId = new PublicKey(
process.env.SETTLEMENT_PROGRAM_ID ?? "11111111111111111111111111111111",
);

if (programId.equals(new PublicKey("11111111111111111111111111111111"))) {
console.error("SETTLEMENT_PROGRAM_ID not set. Deploy the program first.");
process.exit(1);
}

// biome-ignore lint/suspicious/noExplicitAny: Anchor IDL requires any
const program = new Program(idl as any, provider);

console.log(`Authority: ${authority.publicKey.toBase58()}`);
console.log(`Program ID: ${programId.toBase58()}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the Program constructor signature in @coral-xyz/anchor version 0.30.0 and how do you pass a custom program ID?

💡 Result:

In @coral-xyz/anchor version 0.30.0, the Program constructor signature is: new Program(idl: IDL, provider?: Provider, coder?: Coder<string, string>, getCustomResolver?: (instruction: IdlInstruction) => undefined | CustomAccountResolver): Program To pass a custom program ID, use the static Program.at method: await Program.at(address: PublicKey | string, provider?: Provider): Promise<Program> This fetches the IDL from the given program address on-chain (requires prior anchor idl init) and creates the Program instance. For local/offline use with a custom ID, manually set the "address" field in the IDL JSON to the base58-encoded PublicKey string before passing to the constructor, as the separate programId parameter was removed in 0.30.0 and the ID is now read from idl.address.

Citations:


🏁 Script executed:

wc -l scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 97


🏁 Script executed:

sed -n '1,50p' scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 1948


🏁 Script executed:

sed -n '125,165p' scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 1583


🏁 Script executed:

sed -n '250,270p' scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 988


🏁 Script executed:

sed -n '330,345p' scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 729


🏁 Script executed:

sed -n '60,120p' scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 2032


Program ID from environment is not used by the Anchor Program constructor.

The programId is loaded from the SETTLEMENT_PROGRAM_ID environment variable but is only used for logging (line 142). The Program constructor on line 139 does not receive it. In Anchor 0.30.0, the program ID is read from the IDL's address field instead.

If the IDL's address field differs from the environment variable, transactions will target the IDL's address rather than the intended deployment, silently breaking the test on devnet/mainnet.

Correct solutions for Anchor 0.30.0:

  1. Ensure the IDL's address field is set correctly: The loaded IDL should contain an address field matching the deployed program ID. Verify that settlement.json includes this field.

  2. Programmatically override the IDL address: Before passing to the constructor, set the IDL address to match the environment variable:

    +  idl.address = programId.toBase58();
       const program = new Program(idl as any, provider);
  3. Validate address consistency: Verify the IDL address matches the environment variable before proceeding:

    +  if (idl.address && !new PublicKey(idl.address).equals(programId)) {
    +    console.error(`IDL address ${idl.address} does not match SETTLEMENT_PROGRAM_ID`);
    +    process.exit(1);
    +  }

The same issue exists at line 257 with operatorProgram.

Minor: Line 337 refers to "lamports" but these are USDC amounts with 6 decimal places, not SOL. Consider updating the label for clarity.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const programId = new PublicKey(
process.env.SETTLEMENT_PROGRAM_ID ?? "11111111111111111111111111111111",
);
if (programId.equals(new PublicKey("11111111111111111111111111111111"))) {
console.error("SETTLEMENT_PROGRAM_ID not set. Deploy the program first.");
process.exit(1);
}
// biome-ignore lint/suspicious/noExplicitAny: Anchor IDL requires any
const program = new Program(idl as any, provider);
console.log(`Authority: ${authority.publicKey.toBase58()}`);
console.log(`Program ID: ${programId.toBase58()}`);
const programId = new PublicKey(
process.env.SETTLEMENT_PROGRAM_ID ?? "11111111111111111111111111111111",
);
if (programId.equals(new PublicKey("11111111111111111111111111111111"))) {
console.error("SETTLEMENT_PROGRAM_ID not set. Deploy the program first.");
process.exit(1);
}
// biome-ignore lint/suspicious/noExplicitAny: Anchor IDL requires any
idl.address = programId.toBase58();
const program = new Program(idl as any, provider);
console.log(`Authority: ${authority.publicKey.toBase58()}`);
console.log(`Program ID: ${programId.toBase58()}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-settlement-e2e.ts` around lines 129 - 142, The Program
constructor is not using the programId loaded from SETTLEMENT_PROGRAM_ID so
transactions may target the IDL's embedded address; update the IDL's address
field or override it before constructing the Anchor Program: set idl.address =
programId.toString() (or otherwise load/verify settlement.json contains the same
address) and then call new Program(idl as any, provider) (also apply the same
fix for operatorProgram created at the later block). Additionally, add a runtime
check that idl.address matches programId and bail with an error if they differ
to prevent silent mis-targeting.

Comment on lines +173 to +174
const airdropSig = await connection.requestAirdrop(operatorKeypair.publicKey, 1e9);
await connection.confirmTransaction(airdropSig);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What is the recommended way to confirm a transaction in @solana/web3.js 1.90+ and is the single signature overload deprecated?

💡 Result:

In @solana/web3.js version 1.90+, the recommended way to confirm a transaction is to use connection.confirmTransaction with a BlockheightBasedTransactionConfirmationStrategy object, which includes the transaction signature, the blockhash, and lastValidBlockHeight from the transaction. This replaced the older string-only signature overload, which is deprecated. Example: const latestBlockhash = await connection.getLatestBlockhash('confirmed'); const signature = await connection.sendTransaction(transaction, signers, options); // Confirm using strategy await connection.confirmTransaction({ signature, blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, }, 'confirmed'); For convenience, use sendAndConfirmTransaction(connection, transaction, signers, options), which internally handles signing, sending, and confirming using the above strategy if the transaction has recentBlockhash and lastValidBlockHeight set. The single-signature string overload confirmTransaction(signature, commitment) is deprecated; it lacks blockhash info for proper expiration checking and may fail or hang for older/recently confirmed transactions. Use the strategy overload instead.

Citations:


🏁 Script executed:

# Find and check package.json or similar files for `@solana/web3.js` version
fd -e package.json -e package-lock.json | head -5

Repository: RustMunkey/maschina

Length of output: 45


🏁 Script executed:

# Check the actual code at lines 173-174 in the file
cat -n scripts/test-settlement-e2e.ts | sed -n '170,180p'

Repository: RustMunkey/maschina

Length of output: 644


🏁 Script executed:

# Use git ls-files to find package.json
git ls-files | grep package.json

Repository: RustMunkey/maschina

Length of output: 1938


🏁 Script executed:

# Also search for any lock files
git ls-files | grep -E "(package-lock\.json|yarn\.lock|pnpm-lock\.yaml)"

Repository: RustMunkey/maschina

Length of output: 77


🏁 Script executed:

# Find the test-settlement-e2e.ts file
git ls-files | grep test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 93


🏁 Script executed:

# Check if there's a scripts directory at root or in a specific package
ls -la scripts/ 2>/dev/null || echo "No scripts at root"

Repository: RustMunkey/maschina

Length of output: 1110


🏁 Script executed:

# Check root package.json for `@solana/web3.js`
cat package.json | grep -A 50 dependencies | grep -E "(solana|@solana)"

Repository: RustMunkey/maschina

Length of output: 45


🏁 Script executed:

# If not in root, check pnpm-lock.yaml for `@solana/web3.js`
grep "@solana/web3.js" pnpm-lock.yaml | head -10

Repository: RustMunkey/maschina

Length of output: 724


🏁 Script executed:

# Get full root package.json to see all dependencies
head -200 package.json

Repository: RustMunkey/maschina

Length of output: 11218


🏁 Script executed:

# Also check if scripts have a specific tsconfig or package setup
cat scripts/tsconfig.json 2>/dev/null || echo "No tsconfig in scripts"

Repository: RustMunkey/maschina

Length of output: 85


🏁 Script executed:

# Check imports in test-settlement-e2e.ts to see where Connection comes from
head -50 scripts/test-settlement-e2e.ts

Repository: RustMunkey/maschina

Length of output: 1948


🏁 Script executed:

# Check which packages declare `@solana/web3.js` as dependency
grep -r "@solana/web3.js" package.json | head -20

Repository: RustMunkey/maschina

Length of output: 45


🏁 Script executed:

# Also check if there's a tests package that might own this script
cat tests/package.json 2>/dev/null | head -100 || echo "No tests/package.json"

Repository: RustMunkey/maschina

Length of output: 525


🏁 Script executed:

# Search all package.json files for `@solana/web3.js` dependency
find . -name package.json -exec grep -l "@solana/web3.js" {} \;

Repository: RustMunkey/maschina

Length of output: 92


🏁 Script executed:

# Check packages/chain/package.json for `@solana/web3.js` version
cat packages/chain/package.json | grep -A 50 dependencies

Repository: RustMunkey/maschina

Length of output: 474


Update transaction confirmation to use the BlockheightBasedTransactionConfirmationStrategy pattern.

The confirmTransaction(signature) overload is deprecated in @solana/web3.js 1.90+ (this project uses ^1.98.0). Use the recommended pattern with blockhash and lastValidBlockHeight instead.

♻️ Recommended update
   const airdropSig = await connection.requestAirdrop(operatorKeypair.publicKey, 1e9);
-  await connection.confirmTransaction(airdropSig);
+  const latestBlockhash = await connection.getLatestBlockhash();
+  await connection.confirmTransaction({
+    signature: airdropSig,
+    ...latestBlockhash,
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const airdropSig = await connection.requestAirdrop(operatorKeypair.publicKey, 1e9);
await connection.confirmTransaction(airdropSig);
const airdropSig = await connection.requestAirdrop(operatorKeypair.publicKey, 1e9);
const latestBlockhash = await connection.getLatestBlockhash();
await connection.confirmTransaction({
signature: airdropSig,
...latestBlockhash,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-settlement-e2e.ts` around lines 173 - 174, Replace the
deprecated confirmTransaction(signature) call by using the blockheight-based
confirmation strategy: after obtaining airdropSig from
connection.requestAirdrop, call connection.getLatestBlockhash() to retrieve
{blockhash, lastValidBlockHeight} and then call connection.confirmTransaction({
signature: airdropSig, blockhash, lastValidBlockHeight }) (optionally with a
commitment) so the code uses the
BlockheightBasedTransactionConfirmationStrategy; update references around
airdropSig, connection.requestAirdrop, and connection.confirmTransaction
accordingly.

Comment on lines +256 to +257
// biome-ignore lint/suspicious/noExplicitAny: Anchor IDL requires any
const operatorProgram = new Program(idl as any, operatorProvider);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same program ID issue applies here.

The operatorProgram also needs to use the same explicit program ID to ensure consistency with the main program instance.

🔧 Proposed fix
-  const operatorProgram = new Program(idl as any, operatorProvider);
+  const operatorProgram = new Program(idl as any, programId, operatorProvider);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// biome-ignore lint/suspicious/noExplicitAny: Anchor IDL requires any
const operatorProgram = new Program(idl as any, operatorProvider);
// biome-ignore lint/suspicious/noExplicitAny: Anchor IDL requires any
const operatorProgram = new Program(idl as any, programId, operatorProvider);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-settlement-e2e.ts` around lines 256 - 257, The operatorProgram
is being constructed without an explicit program ID, causing it to differ from
the main program instance; update the instantiation of operatorProgram (where
Program, idl, and operatorProvider are used) to pass the same explicit program
ID as the main program (e.g., use program.programId or the shared PROGRAM_ID
constant as the second argument) and move operatorProvider to the provider/third
argument so operatorProgram uses the exact same program ID as program.

Comment on lines +336 to +338
console.log(
`\n ${totalOk ? "✓" : "✗"} Total distributed: ${total} / ${EARNINGS_AMOUNT} lamports`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Terminology: "lamports" is incorrect for USDC.

USDC uses 6 decimal places; the smallest unit is typically called "micro-USDC" or just "USDC base units." "Lamports" specifically refers to SOL's smallest unit (1 SOL = 10⁹ lamports).

📝 Suggested fix
-    `\n  ${totalOk ? "✓" : "✗"} Total distributed: ${total} / ${EARNINGS_AMOUNT} lamports`,
+    `\n  ${totalOk ? "✓" : "✗"} Total distributed: ${total} / ${EARNINGS_AMOUNT} (USDC base units)`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(
`\n ${totalOk ? "✓" : "✗"} Total distributed: ${total} / ${EARNINGS_AMOUNT} lamports`,
);
console.log(
`\n ${totalOk ? "✓" : "✗"} Total distributed: ${total} / ${EARNINGS_AMOUNT} (USDC base units)`,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-settlement-e2e.ts` around lines 336 - 338, The log message uses
the SOL-specific term "lamports"; update the console.log that prints `${totalOk
? "✓" : "✗"} Total distributed: ${total} / ${EARNINGS_AMOUNT} lamports` to use
USDC terminology instead (e.g., "USDC (base units)" or "micro‑USDC") so it
correctly reflects USDC's 6-decimal base unit; change the literal "lamports" to
the chosen USDC label and keep the existing variables total, totalOk, and
EARNINGS_AMOUNT unchanged.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (2)
scripts/setup-mach-token.ts (2)

67-70: ⚠️ Potential issue | 🟠 Major

Use SOLANA_WALLET_PATH and expand ~/ before reading keypair file.

The script still ignores SOLANA_WALLET_PATH from .env.example, and Node won’t expand a leading tilde automatically.

Proposed fix
+function resolveWalletPath(): string {
+  const configured =
+    process.env.SOLANA_WALLET_PATH ??
+    process.env.ANCHOR_WALLET ??
+    path.join(os.homedir(), ".config", "solana", "id.json");
+
+  return configured.startsWith("~/")
+    ? path.join(os.homedir(), configured.slice(2))
+    : configured;
+}
+
 function loadKeypair(): Keypair {
-  const walletPath =
-    process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
+  const walletPath = resolveWalletPath();
   const raw = fs.readFileSync(walletPath, "utf-8");
   const bytes = JSON.parse(raw) as number[];
   return Keypair.fromSecretKey(Uint8Array.from(bytes));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 67 - 70, The loadKeypair() function
currently reads walletPath from ANCHOR_WALLET and does not handle
SOLANA_WALLET_PATH or a leading tilde; update loadKeypair() to prefer
process.env.SOLANA_WALLET_PATH if set, and expand a leading "~" to the user's
home directory (e.g., replace a starting "~/" with os.homedir() + "/") before
calling fs.readFileSync so the path resolves correctly on Node.

52-65: ⚠️ Potential issue | 🟠 Major

Validate SOLANA_CLUSTER and stop hardcoding devnet in user-facing output.

Invalid cluster values currently fall through and the final explorer/.env output still advertises devnet even when another cluster is used.

Proposed fix
-const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet";
+function parseCluster(): "devnet" | "mainnet-beta" | "localnet" {
+  const value = (process.env.SOLANA_CLUSTER ?? "devnet").trim();
+  if (value !== "devnet" && value !== "mainnet-beta" && value !== "localnet") {
+    throw new Error(`Invalid SOLANA_CLUSTER: ${value}`);
+  }
+  return value;
+}
+
+const CLUSTER = parseCluster();
@@
-  if (balance < 0.1 * 1e9) {
-    console.error("Insufficient SOL. Run: solana airdrop 2 --url devnet");
+  if (balance < 0.1 * 1e9) {
+    const topUpHint =
+      CLUSTER === "mainnet-beta"
+        ? "Fund this wallet with mainnet SOL."
+        : `Run: solana airdrop 2 --url ${CLUSTER}`;
+    console.error(`Insufficient SOL. ${topUpHint}`);
     process.exit(1);
   }
@@
-  console.log(`  Explorer:  https://explorer.solana.com/address/${mintAddress}?cluster=devnet`);
+  const explorerSuffix = CLUSTER === "mainnet-beta" ? "" : `?cluster=${CLUSTER}`;
+  console.log(`  Explorer:  https://explorer.solana.com/address/${mintAddress}${explorerSuffix}`);
@@
-  console.log("  SOLANA_CLUSTER=devnet");
+  console.log(`  SOLANA_CLUSTER=${CLUSTER}`);

Also applies to: 87-89, 172-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 52 - 65, Validate the
SOLANA_CLUSTER value and stop hardcoding "devnet" in outputs: add an explicit
whitelist check for CLUSTER (accept only "localnet", "devnet", "mainnet-beta")
and either default to "devnet" or throw a clear error if the env is invalid,
update getRpcUrl() to use the validated CLUSTER value, and replace any hardcoded
"devnet" strings in the user-facing explorer/.env output blocks (the sections
around lines 87-89 and 172-175) so they print the actual CLUSTER value rather
than always showing devnet; reference the CLUSTER constant and getRpcUrl when
adjusting those output generation sites.
🧹 Nitpick comments (1)
.env.example (1)

164-164: Keep PORT aligned with API_PORT if it is an alias.

This line sets PORT=3000 while API_PORT is already 4000, which creates conflicting defaults for services that read different keys.

Proposed fix
-PORT=3000                            # API listen port (alias for API_PORT in some services)
+PORT=4000                            # API listen port (alias for API_PORT in some services)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example at line 164, PORT is currently set to 3000 but API_PORT is
4000, causing conflicting defaults; change the PORT default to match API_PORT
(set PORT=4000) or remove the duplicate and add a comment that PORT is an alias
of API_PORT so both keys stay consistent (update the PORT line and/or comment
referencing PORT and API_PORT).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 46-48: The .env example entries (e.g., HELIUS_API_KEY,
HELIUS_WEBHOOK_ID, HELIUS_WEBHOOK_SECRET) use extra spacing around the '=' which
triggers dotenv-linter; normalize them to the standard dotenv assignment format
with no spaces around '=', e.g., KEY=VALUE or KEY= (empty value) and keep any
explanatory comments after a single space following the value; apply the same
normalization to the other flagged blocks (lines around the other HELIUS entries
and the ranges noted: 56-58, 64, 162-166).

In `@scripts/setup-mach-token.ts`:
- Around line 117-121: The code computes metadataExtension and mintLen but only
uses mintLen when allocating the mint account, causing insufficient
space/lamports; create a single fullMintLen = mintLen + metadataExtension (or
reuse metadataExtension when calling createAccount) and use fullMintLen both
when calling getMinimumBalanceForRentExemption (mintLamports) and when passing
the space/size argument to createAccount so the created account has room for the
metadata extension; update any places that referenced mintLen for allocation to
use fullMintLen (symbols: metadataExtension, mintLen, mintLamports,
createAccount).
- Around line 94-100: Replace the use of new URL(import.meta.url).pathname with
fileURLToPath(import.meta.url) to get a correct platform path and ensure the
target wallets directory exists before writing: compute mintKeypairPath using
fileURLToPath(import.meta.url), call fs.mkdirSync(path.dirname(mintKeypairPath),
{ recursive: true }) (or equivalent) to create the wallets directory if missing,
then call fs.writeFileSync(mintKeypairPath,
JSON.stringify(Array.from(mintKeypair.secretKey))). Ensure references to
mintKeypairPath, import.meta.url, fileURLToPath, fs.mkdirSync, and
fs.writeFileSync are updated accordingly.

In `@scripts/upload-arweave.ts`:
- Around line 52-54: Update the keypair loader to honor SOLANA_WALLET_PATH
(fallback to ANCHOR_WALLET) and expand leading "~/" to the user home directory;
specifically, in loadKeypairPath (and the similar loader around lines 69-71)
check process.env.SOLANA_WALLET_PATH first, then process.env.ANCHOR_WALLET, and
if the chosen path starts with "~/" replace that prefix with os.homedir() (using
path.join to construct the remainder) so both env names and tilde-prefixed paths
are handled correctly.
- Around line 26-34: The code allows invalid SOLANA_CLUSTER values and prints a
hardcoded "devnet" in summary; validate and normalize CLUSTER by checking
process.env.SOLANA_CLUSTER against allowed values ("mainnet-beta", "devnet",
"localnet" or your supported set) in the CLUSTER constant initialization (or
throw/exit on unknown values), update getIrysUrl() to explicitly handle only
allowed clusters and throw or error for unsupported ones, and change any
summary/print logic that currently hardcodes "devnet" to use the CLUSTER
variable so the printed command reflects the actual selected cluster (refer to
CLUSTER, getIrysUrl, and the summary print site).
- Around line 105-109: The code assumes metadata.properties.files[0] exists
before mutating it; add a guard that validates the parsed metadata shape after
JSON.parse(metadataRaw) — ensure metadata is an object, metadata.properties is
an object, metadata.properties.files is an array and files[0] exists (or
initialize properties/files to sensible defaults) before setting metadata.image
and metadata.properties.files[0].uri; if validation fails, throw a clear error
that includes METADATA_PATH and the missing field so callers know the JSON is
malformed.
- Around line 28-30: The current ESM path resolution using path.dirname(new
URL(import.meta.url).pathname) is fragile on Windows; import fileURLToPath from
'url' and replace that expression with
path.dirname(fileURLToPath(import.meta.url)) so ASSETS_DIR, IMAGE_PATH, and
METADATA_PATH are computed using a platform-native path; also add the import {
fileURLToPath } from 'url' at the top of the module so the new call resolves
correctly.

---

Duplicate comments:
In `@scripts/setup-mach-token.ts`:
- Around line 67-70: The loadKeypair() function currently reads walletPath from
ANCHOR_WALLET and does not handle SOLANA_WALLET_PATH or a leading tilde; update
loadKeypair() to prefer process.env.SOLANA_WALLET_PATH if set, and expand a
leading "~" to the user's home directory (e.g., replace a starting "~/" with
os.homedir() + "/") before calling fs.readFileSync so the path resolves
correctly on Node.
- Around line 52-65: Validate the SOLANA_CLUSTER value and stop hardcoding
"devnet" in outputs: add an explicit whitelist check for CLUSTER (accept only
"localnet", "devnet", "mainnet-beta") and either default to "devnet" or throw a
clear error if the env is invalid, update getRpcUrl() to use the validated
CLUSTER value, and replace any hardcoded "devnet" strings in the user-facing
explorer/.env output blocks (the sections around lines 87-89 and 172-175) so
they print the actual CLUSTER value rather than always showing devnet; reference
the CLUSTER constant and getRpcUrl when adjusting those output generation sites.

---

Nitpick comments:
In @.env.example:
- Line 164: PORT is currently set to 3000 but API_PORT is 4000, causing
conflicting defaults; change the PORT default to match API_PORT (set PORT=4000)
or remove the duplicate and add a comment that PORT is an alias of API_PORT so
both keys stay consistent (update the PORT line and/or comment referencing PORT
and API_PORT).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7981f1b-11c8-4eb4-aed8-c9f161426167

📥 Commits

Reviewing files that changed from the base of the PR and between 0416a68 and ae407bd.

⛔ Files ignored due to path filters (2)
  • assets/mach-token.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • .env.example
  • assets/mach-token-metadata.json
  • package.json
  • scripts/setup-mach-token.ts
  • scripts/setup-realms.ts
  • scripts/upload-arweave.ts
✅ Files skipped from review due to trivial changes (2)
  • package.json
  • assets/mach-token-metadata.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/setup-realms.ts

Comment on lines +46 to +48
HELIUS_API_KEY= # Required for prod; falls back to public RPC
HELIUS_WEBHOOK_ID= # Set after running scripts/setup-devnet.ts
HELIUS_WEBHOOK_SECRET= # Random secret for webhook auth header
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize .env assignments to satisfy dotenv-linter.

The newly added block introduces spacing/format patterns that are currently flagged by dotenv-linter. If linting is enforced, this will keep warning noise (or fail checks).

Proposed cleanup (example)
-HELIUS_API_KEY=                                    # Required for prod; falls back to public RPC
-HELIUS_WEBHOOK_ID=                                 # Set after running scripts/setup-devnet.ts
-HELIUS_WEBHOOK_SECRET=                             # Random secret for webhook auth header
+HELIUS_API_KEY= # Required for prod; falls back to public RPC
+HELIUS_WEBHOOK_ID= # Set after running scripts/setup-devnet.ts
+HELIUS_WEBHOOK_SECRET= # Random secret for webhook auth header
@@
-SETTLEMENT_TREASURY_PUBKEY=                          # Receives 15% of all earnings
-SETTLEMENT_DEVELOPER_PUBKEY=                         # Receives 10% marketplace fee
-SETTLEMENT_VALIDATORS_PUBKEY=                        # Receives 5% validators pool
+SETTLEMENT_TREASURY_PUBKEY= # Receives 15% of all earnings
+SETTLEMENT_DEVELOPER_PUBKEY= # Receives 10% marketplace fee
+SETTLEMENT_VALIDATORS_PUBKEY= # Receives 5% validators pool
@@
-REALM_ADDRESS=                                       # Maschina DAO realm address
+REALM_ADDRESS= # Maschina DAO realm address

Also applies to: 56-58, 64-64, 162-166

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 46-46: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 46-46: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 47-47: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 47-47: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 48-48: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 48-48: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 46 - 48, The .env example entries (e.g.,
HELIUS_API_KEY, HELIUS_WEBHOOK_ID, HELIUS_WEBHOOK_SECRET) use extra spacing
around the '=' which triggers dotenv-linter; normalize them to the standard
dotenv assignment format with no spaces around '=', e.g., KEY=VALUE or KEY=
(empty value) and keep any explanatory comments after a single space following
the value; apply the same normalization to the other flagged blocks (lines
around the other HELIUS entries and the ranges noted: 56-58, 64, 162-166).

Comment on lines +94 to +100
const mintKeypairPath = path.join(
path.dirname(new URL(import.meta.url).pathname),
"..",
"wallets",
"mach-mint.json",
);
fs.writeFileSync(mintKeypairPath, JSON.stringify(Array.from(mintKeypair.secretKey)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/setup-mach-token.ts | head -110 | tail -40

Repository: RustMunkey/maschina

Length of output: 1725


🏁 Script executed:

head -20 scripts/setup-mach-token.ts

Repository: RustMunkey/maschina

Length of output: 637


🌐 Web query:

fileURLToPath vs new URL import.meta.url pathname Node.js best practices

💡 Result:

In Node.js ES modules (ESM), fileURLToPath(import.meta.url) from the 'node:url' module converts the file:// URL provided by import.meta.url into a native filesystem path equivalent to filename from CommonJS. This is necessary because import.meta.url is a URL string (e.g., 'file:///path/to/file.mjs'), while most Node.js APIs like fs and path expect filesystem paths. new URL(import.meta.url).pathname extracts just the path component from the URL but keeps it URL-encoded and uses forward slashes, which can cause issues on Windows (e.g., drive letters like /C:/ become invalid paths). It is not recommended for filesystem operations. Best practices (as of Node.js v25+ in 2026): 1. For Node.js >= v21.2.0 (stable since v24): Use import.meta.filename (equivalent to fileURLToPath(import.meta.url)) and import.meta.dirname directly. These provide decoded, OS-native paths (backslashes on Windows) and are the modern, concise standard. Example: const filename = import.meta.filename; const dirname = import.meta.dirname; 2. For compatibility or older Node.js: Use fileURLToPath + path.dirname. Example: import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const filename = fileURLToPath(import.meta.url); const dirname = dirname(filename); Prefer URL-based resolution for relative files where possible (e.g., new URL('./data.json', import.meta.url)) as it avoids paths entirely and works cross-platform, but convert to paths with fileURLToPath when needed for legacy APIs. Avoid new URL(import.meta.url).pathname due to cross-platform bugs.

Citations:


🏁 Script executed:

# Check if wallets directory exists in the repository
find . -type d -name "wallets" 2>/dev/null

Repository: RustMunkey/maschina

Length of output: 45


Use fileURLToPath for cross-platform path handling and ensure the wallets directory exists before writing.

Using new URL(import.meta.url).pathname has known cross-platform issues—it keeps URL encoding and forward slashes, which breaks on Windows (drive letters become /C:/). Additionally, writing to wallets/ without ensuring the directory exists will fail if the path hasn't been created yet.

Proposed fix
 import fs from "node:fs";
 import os from "node:os";
 import path from "node:path";
+import { fileURLToPath } from "node:url";
@@
-  const mintKeypairPath = path.join(
-    path.dirname(new URL(import.meta.url).pathname),
+  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
+  const mintKeypairPath = path.join(
+    scriptDir,
     "..",
     "wallets",
     "mach-mint.json",
   );
-  fs.writeFileSync(mintKeypairPath, JSON.stringify(Array.from(mintKeypair.secretKey)));
+  fs.mkdirSync(path.dirname(mintKeypairPath), { recursive: true });
+  fs.writeFileSync(
+    mintKeypairPath,
+    JSON.stringify(Array.from(mintKeypair.secretKey)),
+    { mode: 0o600 },
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mintKeypairPath = path.join(
path.dirname(new URL(import.meta.url).pathname),
"..",
"wallets",
"mach-mint.json",
);
fs.writeFileSync(mintKeypairPath, JSON.stringify(Array.from(mintKeypair.secretKey)));
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const mintKeypairPath = path.join(
scriptDir,
"..",
"wallets",
"mach-mint.json",
);
fs.mkdirSync(path.dirname(mintKeypairPath), { recursive: true });
fs.writeFileSync(
mintKeypairPath,
JSON.stringify(Array.from(mintKeypair.secretKey)),
{ mode: 0o600 },
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 94 - 100, Replace the use of new
URL(import.meta.url).pathname with fileURLToPath(import.meta.url) to get a
correct platform path and ensure the target wallets directory exists before
writing: compute mintKeypairPath using fileURLToPath(import.meta.url), call
fs.mkdirSync(path.dirname(mintKeypairPath), { recursive: true }) (or equivalent)
to create the wallets directory if missing, then call
fs.writeFileSync(mintKeypairPath,
JSON.stringify(Array.from(mintKeypair.secretKey))). Ensure references to
mintKeypairPath, import.meta.url, fileURLToPath, fs.mkdirSync, and
fs.writeFileSync are updated accordingly.

Comment on lines +117 to +121
const metadataExtension = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const mintLamports = await connection.getMinimumBalanceForRentExemption(
mintLen + metadataExtension,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/setup-mach-token.ts | sed -n '110,140p'

Repository: RustMunkey/maschina

Length of output: 1366


🏁 Script executed:

rg -n "createAccount" scripts/setup-mach-token.ts

Repository: RustMunkey/maschina

Length of output: 100


Allocate full mint account size including metadata extension.

lamports are computed for mintLen + metadataExtension, but createAccount allocates only mintLen. This can break metadata initialization due to insufficient account space.

Proposed fix
     SystemProgram.createAccount({
       fromPubkey: authority.publicKey,
       newAccountPubkey: mintKeypair.publicKey,
-      space: mintLen,
+      space: mintLen + metadataExtension,
       lamports: mintLamports,
       programId: TOKEN_2022_PROGRAM_ID,
     }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const metadataExtension = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const mintLamports = await connection.getMinimumBalanceForRentExemption(
mintLen + metadataExtension,
);
SystemProgram.createAccount({
fromPubkey: authority.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLen + metadataExtension,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-mach-token.ts` around lines 117 - 121, The code computes
metadataExtension and mintLen but only uses mintLen when allocating the mint
account, causing insufficient space/lamports; create a single fullMintLen =
mintLen + metadataExtension (or reuse metadataExtension when calling
createAccount) and use fullMintLen both when calling
getMinimumBalanceForRentExemption (mintLamports) and when passing the space/size
argument to createAccount so the created account has room for the metadata
extension; update any places that referenced mintLen for allocation to use
fullMintLen (symbols: metadataExtension, mintLen, mintLamports, createAccount).

Comment on lines +26 to +34
const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet";

const ASSETS_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "assets");
const IMAGE_PATH = path.join(ASSETS_DIR, "mach-token.png");
const METADATA_PATH = path.join(ASSETS_DIR, "mach-token-metadata.json");

function getIrysUrl(): string {
return CLUSTER === "mainnet-beta" ? "https://node1.irys.xyz" : "https://devnet.irys.xyz";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate SOLANA_CLUSTER and avoid hardcoded devnet follow-up instructions.

Unknown cluster values currently fall through network selection logic, and the summary command always prints SOLANA_CLUSTER=devnet.

Proposed fix
-const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet";
+function parseCluster(): "devnet" | "mainnet-beta" {
+  const value = (process.env.SOLANA_CLUSTER ?? "devnet").trim();
+  if (value !== "devnet" && value !== "mainnet-beta") {
+    throw new Error(`Invalid SOLANA_CLUSTER: ${value}`);
+  }
+  return value;
+}
+
+const CLUSTER = parseCluster();
@@
-  console.log("  2. Run: SOLANA_CLUSTER=devnet npx tsx scripts/setup-mach-token.ts");
+  console.log(`  2. Run: SOLANA_CLUSTER=${CLUSTER} npx tsx scripts/setup-mach-token.ts`);

Also applies to: 36-50, 131-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 26 - 34, The code allows invalid
SOLANA_CLUSTER values and prints a hardcoded "devnet" in summary; validate and
normalize CLUSTER by checking process.env.SOLANA_CLUSTER against allowed values
("mainnet-beta", "devnet", "localnet" or your supported set) in the CLUSTER
constant initialization (or throw/exit on unknown values), update getIrysUrl()
to explicitly handle only allowed clusters and throw or error for unsupported
ones, and change any summary/print logic that currently hardcodes "devnet" to
use the CLUSTER variable so the printed command reflects the actual selected
cluster (refer to CLUSTER, getIrysUrl, and the summary print site).

Comment on lines +28 to +30
const ASSETS_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "assets");
const IMAGE_PATH = path.join(ASSETS_DIR, "mach-token.png");
const METADATA_PATH = path.join(ASSETS_DIR, "mach-token-metadata.json");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "upload-arweave.ts" | head -20

Repository: RustMunkey/maschina

Length of output: 90


🏁 Script executed:

cat -n scripts/upload-arweave.ts | head -40

Repository: RustMunkey/maschina

Length of output: 1661


🌐 Web query:

fileURLToPath vs new URL import.meta.url pathname Windows path resolution

💡 Result:

In Node.js ES modules, fileURLToPath(import.meta.url) and new URL(import.meta.url).pathname both convert the file:// URL of the current module to a filesystem path, but they differ significantly on Windows for correct path resolution. import.meta.url provides a file:// URL (e.g., 'file:///C:/path/to/file.js' on Windows). - new URL(import.meta.url).pathname returns a URL-standard pathname starting with a leading slash: '/C:/path/to/file.js'. This is invalid for Windows fs APIs like path.join or fs.readFileSync, causing errors like double drive letters (e.g., 'C:\C:\path') because path.join treats the leading '/' specially on Windows. - url.fileURLToPath(import.meta.url) returns the correct platform-native path: 'C:\path\to\file.js' (with backslashes on Windows, no leading slash before drive letter, percent-decoding applied). This works properly with fs and path modules cross-platform. Official Node.js docs explicitly warn: new URL('file:///C:/path/').pathname is INCORRECT (/C:/path/), while fileURLToPath('file:///C:/path/') is CORRECT (C:\path\ on Windows). Use fileURLToPath for reliable path resolution, especially for emulating dirname/filename in ESM or relative file access. Modern Node (v21+) also offers import.meta.dirname directly. Example: import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const filename = fileURLToPath(import.meta.url); const dirname = dirname(filename); // C:\path\to on Windows

Citations:


Use fileURLToPath for robust ESM path resolution.

new URL(import.meta.url).pathname breaks on Windows systems. The .pathname property returns a URL-standard path with a leading slash (e.g., /C:/path/to/file.js), which causes path resolution errors and encoded path segment mishandling. fileURLToPath correctly returns the platform-native path and is the official Node.js recommendation.

Proposed fix
 import fs from "node:fs";
 import os from "node:os";
 import path from "node:path";
+import { fileURLToPath } from "node:url";
@@
-const ASSETS_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "assets");
+const ASSETS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ASSETS_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "assets");
const IMAGE_PATH = path.join(ASSETS_DIR, "mach-token.png");
const METADATA_PATH = path.join(ASSETS_DIR, "mach-token-metadata.json");
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ASSETS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets");
const IMAGE_PATH = path.join(ASSETS_DIR, "mach-token.png");
const METADATA_PATH = path.join(ASSETS_DIR, "mach-token-metadata.json");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 28 - 30, The current ESM path
resolution using path.dirname(new URL(import.meta.url).pathname) is fragile on
Windows; import fileURLToPath from 'url' and replace that expression with
path.dirname(fileURLToPath(import.meta.url)) so ASSETS_DIR, IMAGE_PATH, and
METADATA_PATH are computed using a platform-native path; also add the import {
fileURLToPath } from 'url' at the top of the module so the new call resolves
correctly.

Comment on lines +52 to +54
function loadKeypairPath(): string {
return process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Support SOLANA_WALLET_PATH and expand ~/ for keypair loading.

The loader still only checks ANCHOR_WALLET; this is inconsistent with .env.example and breaks common ~/... paths in Node.

Proposed fix
 function loadKeypairPath(): string {
-  return process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
+  const configured =
+    process.env.SOLANA_WALLET_PATH ??
+    process.env.ANCHOR_WALLET ??
+    path.join(os.homedir(), ".config", "solana", "id.json");
+  return configured.startsWith("~/")
+    ? path.join(os.homedir(), configured.slice(2))
+    : configured;
 }

Also applies to: 69-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 52 - 54, Update the keypair loader to
honor SOLANA_WALLET_PATH (fallback to ANCHOR_WALLET) and expand leading "~/" to
the user home directory; specifically, in loadKeypairPath (and the similar
loader around lines 69-71) check process.env.SOLANA_WALLET_PATH first, then
process.env.ANCHOR_WALLET, and if the chosen path starts with "~/" replace that
prefix with os.homedir() (using path.join to construct the remainder) so both
env names and tilde-prefixed paths are handled correctly.

Comment on lines +105 to +109
const metadataRaw = fs.readFileSync(METADATA_PATH, "utf-8");
const metadata = JSON.parse(metadataRaw);
metadata.image = imageUri;
metadata.properties.files[0].uri = imageUri;
const metadataPatched = JSON.stringify(metadata, null, 2);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard metadata shape before mutating nested fields.

This block assumes metadata.properties.files[0] exists. A malformed JSON file will throw at runtime without a clear error.

Proposed fix
-  const metadata = JSON.parse(metadataRaw);
+  const metadata = JSON.parse(metadataRaw) as {
+    image?: string;
+    properties?: { files?: Array<{ uri?: string }> };
+  };
+  if (!metadata.properties?.files?.[0]) {
+    throw new Error("Invalid metadata schema: expected properties.files[0]");
+  }
   metadata.image = imageUri;
   metadata.properties.files[0].uri = imageUri;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 105 - 109, The code assumes
metadata.properties.files[0] exists before mutating it; add a guard that
validates the parsed metadata shape after JSON.parse(metadataRaw) — ensure
metadata is an object, metadata.properties is an object,
metadata.properties.files is an array and files[0] exists (or initialize
properties/files to sensible defaults) before setting metadata.image and
metadata.properties.files[0].uri; if validation fails, throw a clear error that
includes METADATA_PATH and the missing field so callers know the JSON is
malformed.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (10)
scripts/upload-arweave.ts (4)

28-30: ⚠️ Potential issue | 🟠 Major

Use fileURLToPath for cross-platform ESM path resolution.

Line 28 uses new URL(import.meta.url).pathname, which is not robust on Windows path semantics.

Proposed fix
 import fs from "node:fs";
 import os from "node:os";
 import path from "node:path";
+import { fileURLToPath } from "node:url";
@@
-const ASSETS_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "assets");
+const ASSETS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets");
#!/bin/bash
# Verify current path-conversion usage in this script.
rg -n 'new URL\(import\.meta\.url\)\.pathname|fileURLToPath' scripts/upload-arweave.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 28 - 30, Replace the brittle new
URL(import.meta.url).pathname usage with Node's fileURLToPath conversion:
compute ASSETS_DIR by converting import.meta.url to a file path via
fileURLToPath(import.meta.url) and then use path.join on that directory to build
ASSETS_DIR, leaving IMAGE_PATH and METADATA_PATH as they are; update the top
imports to include fileURLToPath from 'url' if not already present and adjust
ASSETS_DIR, IMAGE_PATH, and METADATA_PATH definitions to reference the converted
file path.

105-109: ⚠️ Potential issue | 🟠 Major

Guard metadata shape before mutating nested fields.

Line 108 assumes metadata.properties.files[0] exists; malformed metadata JSON will throw with an unclear error.

Proposed fix
-  const metadata = JSON.parse(metadataRaw);
+  const metadata = JSON.parse(metadataRaw) as {
+    image?: string;
+    properties?: { files?: Array<{ uri?: string }> };
+  };
+  if (!metadata.properties?.files?.[0]) {
+    throw new Error(`Invalid metadata schema in ${METADATA_PATH}: expected properties.files[0]`);
+  }
   metadata.image = imageUri;
   metadata.properties.files[0].uri = imageUri;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 105 - 109, The code assumes
metadata.properties.files[0] exists before mutating it; update the logic after
parsing metadata (metadataRaw → metadata) to validate and guard the shape: check
that metadata is an object, metadata.properties exists,
metadata.properties.files is an array and has at least one entry (or create the
nested structure if missing), and only then set metadata.image and
metadata.properties.files[0].uri; if the shape is invalid, throw or log a clear
error mentioning the metadata path and include the raw content (or write a
fallback files array) before serializing to metadataPatched to prevent unclear
runtime exceptions.

26-34: ⚠️ Potential issue | 🟠 Major

Validate SOLANA_CLUSTER and avoid hardcoded devnet follow-up output.

Line 26-34 accepts unsupported cluster values and silently falls back to devnet behavior; Line 131 always prints devnet even when running mainnet-beta.

Proposed fix
-const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet";
+function parseCluster(): "devnet" | "mainnet-beta" {
+  const value = (process.env.SOLANA_CLUSTER ?? "devnet").trim();
+  if (value !== "devnet" && value !== "mainnet-beta") {
+    throw new Error(`Invalid SOLANA_CLUSTER: ${value}. Must be "devnet" or "mainnet-beta".`);
+  }
+  return value;
+}
+const CLUSTER = parseCluster();
@@
-function getIrysUrl(): string {
-  return CLUSTER === "mainnet-beta" ? "https://node1.irys.xyz" : "https://devnet.irys.xyz";
-}
+function getIrysUrl(): string {
+  if (CLUSTER === "mainnet-beta") return "https://node1.irys.xyz";
+  if (CLUSTER === "devnet") return "https://devnet.irys.xyz";
+  throw new Error(`Unsupported SOLANA_CLUSTER: ${CLUSTER}`);
+}
@@
-  console.log("  2. Run: SOLANA_CLUSTER=devnet npx tsx scripts/setup-mach-token.ts");
+  console.log(`  2. Run: SOLANA_CLUSTER=${CLUSTER} npx tsx scripts/setup-mach-token.ts`);

Also applies to: 131-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 26 - 34, Validate the SOLANA_CLUSTER
value before using it: replace the current CLUSTER assignment with a check
against an allowed set (e.g., "devnet", "mainnet-beta", "testnet") and if the
env value is invalid log a warning and fall back to "devnet"; update any
hardcoded prints that show the cluster (the status/logging statement that
currently prints "devnet" at the end of the script) to use the CLUSTER variable
so output reflects the actual validated value; ensure getIrysUrl() continues to
derive its URL from the validated CLUSTER constant.

52-54: ⚠️ Potential issue | 🟠 Major

Honor SOLANA_WALLET_PATH and expand ~/ in wallet path loading.

Line 52-54 only supports ANCHOR_WALLET, which is inconsistent with .env.example and fails for common tilde-prefixed paths.

Proposed fix
 function loadKeypairPath(): string {
-  return process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json");
+  const configured =
+    process.env.SOLANA_WALLET_PATH ??
+    process.env.ANCHOR_WALLET ??
+    path.join(os.homedir(), ".config", "solana", "id.json");
+  return configured.startsWith("~/")
+    ? path.join(os.homedir(), configured.slice(2))
+    : configured;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/upload-arweave.ts` around lines 52 - 54, Update loadKeypairPath to
first read SOLANA_WALLET_PATH (falling back to ANCHOR_WALLET) and expand a
leading "~/" to the user's home directory; specifically, in the loadKeypairPath
function replace the single env var lookup with logic like: prefer
process.env.SOLANA_WALLET_PATH || process.env.ANCHOR_WALLET, and if the chosen
path starts with "~/" replace that prefix with os.homedir() (then
normalize/resolve with path.join/path.resolve) so tilde-prefixed paths are
correctly expanded.
scripts/setup-devnet.ts (2)

11-13: ⚠️ Potential issue | 🟡 Minor

Header comment overstates runtime behavior.

Line 11 says the script verifies deployment at SETTLEMENT_PROGRAM_ID, but no RPC/program-account check is performed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-devnet.ts` around lines 11 - 13, The header comment incorrectly
claims the script verifies the settlement program at SETTLEMENT_PROGRAM_ID but
no such check exists; either remove/soften that line or implement a runtime
verification: create or use the existing RPC Connection and call
connection.getAccountInfo(new PublicKey(SETTLEMENT_PROGRAM_ID)) (or
connection.getProgramAccounts) and fail/log if the account/program is not found.
Update the comment if you choose to remove the claim, or add the getAccountInfo
check and a clear error message referencing SETTLEMENT_PROGRAM_ID if you choose
to implement verification.

41-41: ⚠️ Potential issue | 🟠 Major

Normalize API_BASE_URL before building webhookUrl.

Line 41 can produce double slashes when API_BASE_URL ends with /, which can break webhook deduplication in registerSettlementWebhook.

Proposed fix
-  const apiBaseUrl = process.env.API_BASE_URL;
+  const apiBaseUrl = process.env.API_BASE_URL?.replace(/\/+$/, "");
   if (!apiBaseUrl) {
@@
-  const webhookUrl = `${apiBaseUrl}/webhooks/helius`;
+  const webhookUrl = new URL("/webhooks/helius", `${apiBaseUrl}/`).toString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/setup-devnet.ts` at line 41, The webhookUrl concatenation can produce
double slashes when API_BASE_URL (apiBaseUrl) ends with a trailing slash; update
the code that builds webhookUrl (const webhookUrl =
`${apiBaseUrl}/webhooks/helius`;) to normalize apiBaseUrl first — e.g., remove
any trailing slash from API_BASE_URL or use a URL-joining approach — so that
webhookUrl is always a single-slash path; ensure registerSettlementWebhook
receives the normalized webhookUrl.
.env.example (1)

46-48: ⚠️ Potential issue | 🟡 Minor

Normalize .env assignments to remove persistent linter noise.

Line 46-48, Line 56-58, and Line 64 still use padded assignment formatting (KEY= # ...), which keeps dotenv-linter warnings alive.

Proposed cleanup
-HELIUS_API_KEY=                                    # Required for prod; falls back to public RPC
-HELIUS_WEBHOOK_ID=                                 # Set after running scripts/setup-devnet.ts
-HELIUS_WEBHOOK_SECRET=                             # Random secret for webhook auth header
+HELIUS_API_KEY= # Required for prod; falls back to public RPC
+HELIUS_WEBHOOK_ID= # Set after running scripts/setup-devnet.ts
+HELIUS_WEBHOOK_SECRET= # Random secret for webhook auth header
@@
-SETTLEMENT_TREASURY_PUBKEY=                          # Receives 15% of all earnings
-SETTLEMENT_DEVELOPER_PUBKEY=                         # Receives 10% marketplace fee
-SETTLEMENT_VALIDATORS_PUBKEY=                        # Receives 5% validators pool
+SETTLEMENT_TREASURY_PUBKEY= # Receives 15% of all earnings
+SETTLEMENT_DEVELOPER_PUBKEY= # Receives 10% marketplace fee
+SETTLEMENT_VALIDATORS_PUBKEY= # Receives 5% validators pool
@@
-REALM_ADDRESS=                                       # Maschina DAO realm address
+REALM_ADDRESS= # Maschina DAO realm address

Also applies to: 56-58, 64-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 46 - 48, Normalize the .env assignments by
removing the padded spaces after the equals sign so the comment follows
immediately (single space) — e.g., change HELIUS_API_KEY=                       
# ... to HELIUS_API_KEY= # ...; do the same for HELIUS_WEBHOOK_ID and
HELIUS_WEBHOOK_SECRET and apply the same cleanup pattern to the other
occurrences mentioned (the blocks around lines 56-58 and 64) to silence
dotenv-linter warnings.
programs/settlement/src/instructions/add_earnings.rs (2)

16-19: ⚠️ Potential issue | 🔴 Critical

Make run_id idempotent on-chain.

run_id only reaches the event today, so a retry or replay transfers tokens and increments the pool again. Reuse ExecutionReceipt or add a dedicated PDA keyed by run_id, and reject duplicates before the CPI transfer.

Also applies to: 73-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/add_earnings.rs` around lines 16 - 19,
Add on-chain idempotency for AddEarningsArgs.run_id by storing and checking a
PDA or reusing ExecutionReceipt before performing the CPI token transfer: when
handling the AddEarnings instruction, derive a PDA keyed by run_id (or load the
existing ExecutionReceipt tied to run_id), check if it already exists and abort
with an error if so, and only proceed to the CPI transfer and increment the pool
if the PDA/receipt is absent; ensure the PDA is created/marked as consumed
atomically within the same transaction that does the transfer to prevent races.

35-68: ⚠️ Potential issue | 🔴 Critical

Gate add_earnings behind the configured settlement authority.

Right now any signer with a matching token account can transfer funds into the vault and mutate pool.pending_* / run_count. Mirror the config.authority constraint already used in programs/settlement/src/instructions/settle_earnings.rs so only the trusted settlement authority can record earnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/add_earnings.rs` around lines 35 - 68,
The add_earnings handler currently accepts any Signer that controls the matching
token account; mirror the settle_earnings authority check by adding the program
Config account to the context (e.g., pub config: Account<'info, Config>) and
restrict the authority signer to the configured settlement authority (change the
authority account annotation to require address = config.authority or equivalent
Anchor constraint used in settle_earnings.rs). Update the account list for
add_earnings to include config and replace the unrestricted #[account(mut)] pub
authority: Signer<'info> with the constrained form that enforces
config.authority so only the trusted settlement authority can mutate
pool.pending_* and run_count.
programs/settlement/src/instructions/init_node_vault.rs (1)

29-43: ⚠️ Potential issue | 🔴 Critical

Still missing canonical mint enforcement.

usdc_mint is caller-controlled on the one-time vault init, so the first initializer can bind the vault PDA to the wrong mint and break every later USDC settlement flow for that node. Pin this to a canonical mint constant or a mint stored in SettlementConfig before the init runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/init_node_vault.rs` around lines 29 -
43, The usdc_mint Account<'info, Mint> passed to the init for the per-node vault
(the account tied to VAULT_SEED and args.node_id in init_node_vault.rs) must be
canonicalized — don't allow the initializer to pick any mint. Fix by pinning the
mint to a known source: either replace the caller-supplied usdc_mint with the
canonical mint stored on the program (e.g., SettlementConfig.usdc_mint) and
change the account constraint/token::mint to reference that stored mint, or
validate the provided usdc_mint against a constant canonical pubkey
(require_keys_eq or an explicit check) before performing the init of the vault;
update the account definition and the init precondition logic (referencing
usdc_mint, VAULT_SEED, args.node_id, and SettlementConfig) so the vault is
always created with the canonical mint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 176: There is a duplicate APP_URL environment key; remove the redundant
APP_URL entry so only one APP_URL variable remains (keep the canonical
definition already present and delete the later redefinition) to prevent dotenv
from overriding the intended public URL for magic links and other features.

In `@programs/settlement/src/instructions/initialize_config.rs`:
- Around line 22-34: The initialize_config flow currently allows any signer to
init the singleton config PDA and set themselves as authority; restrict this by
enforcing the caller equals a known governance/deployment key before allowing
the init. Add a constraint on the `authority` signer in the `initialize_config`
context (or validate inside the `initialize_config` handler) that checks
`authority.key()` against the expected governance public key (or a governance
PDA) and fail with an unauthorized error if it does not match; apply the same
check for the other identical init block referenced (lines 41-47). Reference the
`authority` Signer, the `config:
Account<SettlementConfig>`/`SettlementConfig::SEED`, and the `initialize_config`
entrypoint when implementing the change.

---

Duplicate comments:
In @.env.example:
- Around line 46-48: Normalize the .env assignments by removing the padded
spaces after the equals sign so the comment follows immediately (single space) —
e.g., change HELIUS_API_KEY=                                    # ... to
HELIUS_API_KEY= # ...; do the same for HELIUS_WEBHOOK_ID and
HELIUS_WEBHOOK_SECRET and apply the same cleanup pattern to the other
occurrences mentioned (the blocks around lines 56-58 and 64) to silence
dotenv-linter warnings.

In `@programs/settlement/src/instructions/add_earnings.rs`:
- Around line 16-19: Add on-chain idempotency for AddEarningsArgs.run_id by
storing and checking a PDA or reusing ExecutionReceipt before performing the CPI
token transfer: when handling the AddEarnings instruction, derive a PDA keyed by
run_id (or load the existing ExecutionReceipt tied to run_id), check if it
already exists and abort with an error if so, and only proceed to the CPI
transfer and increment the pool if the PDA/receipt is absent; ensure the PDA is
created/marked as consumed atomically within the same transaction that does the
transfer to prevent races.
- Around line 35-68: The add_earnings handler currently accepts any Signer that
controls the matching token account; mirror the settle_earnings authority check
by adding the program Config account to the context (e.g., pub config:
Account<'info, Config>) and restrict the authority signer to the configured
settlement authority (change the authority account annotation to require address
= config.authority or equivalent Anchor constraint used in settle_earnings.rs).
Update the account list for add_earnings to include config and replace the
unrestricted #[account(mut)] pub authority: Signer<'info> with the constrained
form that enforces config.authority so only the trusted settlement authority can
mutate pool.pending_* and run_count.

In `@programs/settlement/src/instructions/init_node_vault.rs`:
- Around line 29-43: The usdc_mint Account<'info, Mint> passed to the init for
the per-node vault (the account tied to VAULT_SEED and args.node_id in
init_node_vault.rs) must be canonicalized — don't allow the initializer to pick
any mint. Fix by pinning the mint to a known source: either replace the
caller-supplied usdc_mint with the canonical mint stored on the program (e.g.,
SettlementConfig.usdc_mint) and change the account constraint/token::mint to
reference that stored mint, or validate the provided usdc_mint against a
constant canonical pubkey (require_keys_eq or an explicit check) before
performing the init of the vault; update the account definition and the init
precondition logic (referencing usdc_mint, VAULT_SEED, args.node_id, and
SettlementConfig) so the vault is always created with the canonical mint.

In `@scripts/setup-devnet.ts`:
- Around line 11-13: The header comment incorrectly claims the script verifies
the settlement program at SETTLEMENT_PROGRAM_ID but no such check exists; either
remove/soften that line or implement a runtime verification: create or use the
existing RPC Connection and call connection.getAccountInfo(new
PublicKey(SETTLEMENT_PROGRAM_ID)) (or connection.getProgramAccounts) and
fail/log if the account/program is not found. Update the comment if you choose
to remove the claim, or add the getAccountInfo check and a clear error message
referencing SETTLEMENT_PROGRAM_ID if you choose to implement verification.
- Line 41: The webhookUrl concatenation can produce double slashes when
API_BASE_URL (apiBaseUrl) ends with a trailing slash; update the code that
builds webhookUrl (const webhookUrl = `${apiBaseUrl}/webhooks/helius`;) to
normalize apiBaseUrl first — e.g., remove any trailing slash from API_BASE_URL
or use a URL-joining approach — so that webhookUrl is always a single-slash
path; ensure registerSettlementWebhook receives the normalized webhookUrl.

In `@scripts/upload-arweave.ts`:
- Around line 28-30: Replace the brittle new URL(import.meta.url).pathname usage
with Node's fileURLToPath conversion: compute ASSETS_DIR by converting
import.meta.url to a file path via fileURLToPath(import.meta.url) and then use
path.join on that directory to build ASSETS_DIR, leaving IMAGE_PATH and
METADATA_PATH as they are; update the top imports to include fileURLToPath from
'url' if not already present and adjust ASSETS_DIR, IMAGE_PATH, and
METADATA_PATH definitions to reference the converted file path.
- Around line 105-109: The code assumes metadata.properties.files[0] exists
before mutating it; update the logic after parsing metadata (metadataRaw →
metadata) to validate and guard the shape: check that metadata is an object,
metadata.properties exists, metadata.properties.files is an array and has at
least one entry (or create the nested structure if missing), and only then set
metadata.image and metadata.properties.files[0].uri; if the shape is invalid,
throw or log a clear error mentioning the metadata path and include the raw
content (or write a fallback files array) before serializing to metadataPatched
to prevent unclear runtime exceptions.
- Around line 26-34: Validate the SOLANA_CLUSTER value before using it: replace
the current CLUSTER assignment with a check against an allowed set (e.g.,
"devnet", "mainnet-beta", "testnet") and if the env value is invalid log a
warning and fall back to "devnet"; update any hardcoded prints that show the
cluster (the status/logging statement that currently prints "devnet" at the end
of the script) to use the CLUSTER variable so output reflects the actual
validated value; ensure getIrysUrl() continues to derive its URL from the
validated CLUSTER constant.
- Around line 52-54: Update loadKeypairPath to first read SOLANA_WALLET_PATH
(falling back to ANCHOR_WALLET) and expand a leading "~/" to the user's home
directory; specifically, in the loadKeypairPath function replace the single env
var lookup with logic like: prefer process.env.SOLANA_WALLET_PATH ||
process.env.ANCHOR_WALLET, and if the chosen path starts with "~/" replace that
prefix with os.homedir() (then normalize/resolve with path.join/path.resolve) so
tilde-prefixed paths are correctly expanded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e85c595-ddb9-4268-bb3c-f4df4365f4cf

📥 Commits

Reviewing files that changed from the base of the PR and between ae407bd and 466ae2f.

⛔ Files ignored due to path filters (3)
  • assets/mach-token.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • programs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • .env.example
  • .gitignore
  • apps/docs/src/content/introduction.mdx
  • apps/docs/src/routes/introduction.tsx
  • apps/web/src/routes/about.tsx
  • apps/web/src/routes/careers.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/routes/marketplace.tsx
  • apps/web/src/routes/press-kit.tsx
  • apps/web/src/routes/security.tsx
  • apps/web/src/routes/terms.tsx
  • ascii-logo.txt
  • assets/mach-token-metadata.json
  • package.json
  • packages/chain/package.json
  • packages/marketplace/src/marketplace.test.ts
  • programs/Anchor.toml
  • programs/Cargo.toml
  • programs/settlement/Cargo.toml
  • programs/settlement/src/errors.rs
  • programs/settlement/src/instructions/add_earnings.rs
  • programs/settlement/src/instructions/init_node_vault.rs
  • programs/settlement/src/instructions/initialize_config.rs
  • programs/settlement/src/instructions/mod.rs
  • programs/settlement/src/instructions/register_node.rs
  • programs/settlement/src/instructions/settle_earnings.rs
  • programs/settlement/src/lib.rs
  • programs/settlement/src/state.rs
  • programs/settlement/target/idl/settlement.json
  • scripts/discord-scaffold.mjs
  • scripts/setup-devnet.ts
  • scripts/setup-mach-token.ts
  • scripts/setup-realms.ts
  • scripts/test-settlement-e2e.ts
  • scripts/upload-arweave.ts
✅ Files skipped from review due to trivial changes (19)
  • apps/docs/src/routes/introduction.tsx
  • apps/web/src/routes/careers.tsx
  • .gitignore
  • apps/docs/src/content/introduction.mdx
  • apps/web/src/routes/security.tsx
  • apps/web/src/routes/about.tsx
  • ascii-logo.txt
  • apps/web/src/routes/index.tsx
  • programs/Cargo.toml
  • apps/web/src/routes/terms.tsx
  • apps/web/src/routes/press-kit.tsx
  • packages/marketplace/src/marketplace.test.ts
  • package.json
  • packages/chain/package.json
  • assets/mach-token-metadata.json
  • programs/settlement/Cargo.toml
  • programs/settlement/src/instructions/mod.rs
  • apps/web/src/routes/marketplace.tsx
  • programs/settlement/target/idl/settlement.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • scripts/discord-scaffold.mjs
  • programs/Anchor.toml
  • scripts/setup-realms.ts
  • programs/settlement/src/instructions/register_node.rs
  • scripts/setup-mach-token.ts
  • scripts/test-settlement-e2e.ts
  • programs/settlement/src/lib.rs

NODE_ENV=development

# ─── App ─────────────────────────────────────────────────────────────────────
APP_URL=http://localhost:3000 # public-facing URL for magic link emails etc.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the duplicate APP_URL key to avoid config override bugs.

Line 176 redefines APP_URL (already defined at Line 143) with a different value. In dotenv, the last assignment wins, so app links can silently point to the wrong host.

Proposed fix
-APP_URL=http://localhost:3000        # public-facing URL for magic link emails etc.
+# APP_URL is already defined above (Line 143). Avoid redefining it here.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
APP_URL=http://localhost:3000 # public-facing URL for magic link emails etc.
# APP_URL is already defined above (Line 143). Avoid redefining it here.
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 176-176: [DuplicatedKey] The APP_URL key is duplicated

(DuplicatedKey)


[warning] 176-176: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example at line 176, There is a duplicate APP_URL environment key;
remove the redundant APP_URL entry so only one APP_URL variable remains (keep
the canonical definition already present and delete the later redefinition) to
prevent dotenv from overriding the intended public URL for magic links and other
features.

Comment on lines +22 to +34
/// Maschina settlement authority — becomes the only key allowed to settle.
#[account(mut)]
pub authority: Signer<'info>,

/// Global config PDA — created once, never re-initialised.
#[account(
init,
payer = authority,
space = SettlementConfig::LEN,
seeds = [SettlementConfig::SEED],
bump,
)]
pub config: Account<'info, SettlementConfig>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

initialize_config is currently first-caller-wins.

Any signer can create the singleton config PDA and set themselves as config.authority, so the first external caller permanently controls settlement authorization and the developer/treasury/validators payout keys. This needs to be gated to a known governance/deployment authority before the init executes.

Also applies to: 41-47

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/settlement/src/instructions/initialize_config.rs` around lines 22 -
34, The initialize_config flow currently allows any signer to init the singleton
config PDA and set themselves as authority; restrict this by enforcing the
caller equals a known governance/deployment key before allowing the init. Add a
constraint on the `authority` signer in the `initialize_config` context (or
validate inside the `initialize_config` handler) that checks `authority.key()`
against the expected governance public key (or a governance PDA) and fail with
an unauthorized error if it does not match; apply the same check for the other
identical init block referenced (lines 41-47). Reference the `authority` Signer,
the `config: Account<SettlementConfig>`/`SettlementConfig::SEED`, and the
`initialize_config` entrypoint when implementing the change.

@RustMunkey RustMunkey merged commit 98b83a0 into main Mar 28, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant