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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to the `nirium-cli` package are documented here.

## Unreleased

## 3.2.0 - unreleased

### Changed

- `nirium report export anchors` prints each anchor's date and network (tab-separated) instead of its CID, and never prints a CID even if an older node sends one. The node's public Reporting export stopped returning CIDs and payroll run ids on 8 Nov 2026: an anchor's CID belongs to whoever made it. Piping `export anchors` into `nirium verify` no longer applies; verify the CIDs you anchored yourself with `nirium verify <cid>`.
- `nirium report summary` shows the date of the last anchor (`last: YYYY-MM-DD`) when the node sends `lastAnchoredAt`, and falls back to `latest: <cid>` for older nodes.

## 3.1.1 - 2026-11-07

### Fixed
Expand Down
13 changes: 5 additions & 8 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,15 +341,12 @@ receipts by gateway, and anchor counts for the period. `report export
<type>` pulls the underlying rows:

- Without `--csv`, rows print one per line on stdout. For `export
anchors` specifically, that line is **just the CID** - nothing else -
so it pipes straight into `nirium verify`:
anchors`, each line is the anchor's date and network, tab-separated.
There is no CID: the public Reporting export doesn't return them (an
anchor's CID belongs to whoever made it). If you anchored something,
you already have its CID - check it with `nirium verify <cid>`.

```bash
nirium report export anchors --from 2026-10-01 | xargs -n1 nirium verify
```

The row count and disclaimer print to **stderr**, not stdout, so they
never end up in that pipe.
The row count and disclaimer print to **stderr**, not stdout.
- With `--csv <file>`, the server's CSV is written to that file instead
(nothing on stdout but a confirmation).
- `--json` prints the full response (`type`, `period`, `rowCount`,
Expand Down
18 changes: 12 additions & 6 deletions packages/cli/bin/nirium.js
Original file line number Diff line number Diff line change
Expand Up @@ -2015,7 +2015,12 @@ async function executeReportSummaryCommand(options) {
console.log(` ${asset}: ${amount}`);
}
console.log(` Payments: ${data.payments?.count ?? 0} receipt(s), ${data.payments?.totalUsdc ?? '0'} USDC total`);
console.log(` Anchors: ${data.anchors?.count ?? 0} (latest: ${data.anchors?.latestCid || 'none'})`);
// Nodes since 8 Nov 2026 send lastAnchoredAt instead of latestCid (the latest
// anchor is usually someone else's evidence). Older nodes still send latestCid.
const lastAnchor = data.anchors?.lastAnchoredAt
? `last: ${String(data.anchors.lastAnchoredAt).slice(0, 10)}`
: `latest: ${data.anchors?.latestCid || 'none'}`;
console.log(` Anchors: ${data.anchors?.count ?? 0} (${lastAnchor})`);
if (Array.isArray(data.warnings) && data.warnings.length > 0) {
console.log(' ⚠ Warnings:');
for (const w of data.warnings) console.log(` - ${w}`);
Expand Down Expand Up @@ -2090,12 +2095,13 @@ async function executeReportExportCommand(type, options) {
return;
}

// `export anchors` prints exactly one CID per line on stdout - nothing
// else - so it pipes straight into `nirium verify` (xargs -n1 or a
// while-read loop). Row count and the disclaimer go to stderr instead
// of stdout for the same reason: stdout must stay pure for piping.
// `export anchors` prints one line per anchor: when and on which network,
// tab-separated. No CID: the public Reporting export stopped returning them
// on 8 Nov 2026 (an anchor's CID belongs to whoever made it), and this never
// prints one even if an older node still sends it. Row count and the
// disclaimer go to stderr so stdout stays clean for piping.
for (const row of data.rows || []) {
console.log(type === 'anchors' ? (row.cid || '') : JSON.stringify(row));
console.log(type === 'anchors' ? `${row.anchored_at || ''}\t${row.network || ''}` : JSON.stringify(row));
}
console.error(`# ${data.rowCount ?? (data.rows || []).length} row(s). ${data.disclaimer || ''}`);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nirium-cli",
"version": "3.1.1",
"version": "3.2.0",
"description": "Nirium CLI \u2014 scaffold Stellar agent projects and check agent status",
"main": "bin/nirium.js",
"type": "module",
Expand Down
24 changes: 12 additions & 12 deletions packages/cli/test/report.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ const SUMMARY = {
network: 'all',
payroll: { settledRuns: 2, recipientsPaid: 5, totalsByAsset: { USDC: '120.0000000' }, lastSettledAt: '2026-11-01T00:00:00.000Z' },
payments: { count: 10, totalUsdc: '1.23', byGateway: { x402: { count: 8, totalUsdc: 1.0 }, mpp: { count: 2, totalUsdc: 0.23 } } },
anchors: { count: 3, latestCid: 'QmLatestFake' },
anchors: { count: 3, lastAnchoredAt: '2026-11-01T00:00:00.000Z' },
generatedAt: '2026-11-08T00:00:00.000Z',
disclaimer: DISCLAIMER,
};

const ANCHOR_ROWS = [
// An older node may still send cid; the CLI must never print it.
{ anchored_at: '2026-11-01T00:00:00.000Z', source: 'AUDIT_NODE', cid: 'QmAnchorOne', network: 'testnet' },
{ anchored_at: '2026-10-31T00:00:00.000Z', source: 'AUDIT_NODE', cid: 'QmAnchorTwo', network: 'testnet' },
{ anchored_at: '2026-10-31T00:00:00.000Z', source: 'AUDIT_NODE', network: 'mainnet' },
];

let lastRequest = null;
Expand Down Expand Up @@ -62,7 +63,7 @@ const server = http.createServer((req, res) => {
const format = u.searchParams.get('format') || 'csv';
if (format === 'csv') {
res.writeHead(200, { 'content-type': 'text/csv; charset=utf-8', 'content-disposition': `attachment; filename="nirium-${type}.csv"` });
res.end('anchored_at,source,cid,network\r\n2026-11-01T00:00:00.000Z,AUDIT_NODE,QmAnchorOne,testnet\r\n');
res.end('anchored_at,source,network\r\n2026-11-01T00:00:00.000Z,AUDIT_NODE,testnet\r\n');
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
Expand Down Expand Up @@ -117,7 +118,7 @@ test('summary human output reports payroll, payments, anchors, and the disclaime
assert.match(r.stdout, /2 settled run\(s\), 5 recipient\(s\) paid/);
assert.match(r.stdout, /USDC: 120\.0000000/);
assert.match(r.stdout, /10 receipt\(s\), 1\.23 USDC total/);
assert.match(r.stdout, /3 \(latest: QmLatestFake\)/);
assert.match(r.stdout, /3 \(last: 2026-11-01\)/);
assert.match(r.stdout, /not certified regulatory reporting/);
});

Expand All @@ -140,11 +141,11 @@ test('summary surfaces a server error as a failure', async () => {

// ─── export ────────────────────────────────────────────────────

test('export anchors prints exactly one CID per line on stdout, nothing else', async () => {
test('export anchors prints one line per anchor (date and network), nothing else', async () => {
const r = await run(['export', 'anchors', '--url', base]);
assert.equal(r.code, 0, r.stdout + r.stderr);
const lines = r.stdout.trim().split('\n');
assert.deepEqual(lines, ['QmAnchorOne', 'QmAnchorTwo']);
assert.deepEqual(lines, ['2026-11-01T00:00:00.000Z\ttestnet', '2026-10-31T00:00:00.000Z\tmainnet']);
});

test('export anchors footer (row count + disclaimer) goes to stderr, not stdout', async () => {
Expand All @@ -155,11 +156,10 @@ test('export anchors footer (row count + disclaimer) goes to stderr, not stdout'
assert.match(r.stderr, /not certified regulatory reporting/);
});

test('export anchors pipes straight into `nirium verify` without extra parsing', async () => {
test('export anchors never prints a CID, even if the node sends one', async () => {
const r = await run(['export', 'anchors', '--url', base]);
const cids = r.stdout.trim().split('\n').filter(Boolean);
assert.equal(cids.length, 2);
for (const cid of cids) assert.match(cid, /^Qm[A-Za-z0-9]+$/);
assert.equal(r.code, 0, r.stdout + r.stderr);
assert.doesNotMatch(r.stdout + r.stderr, /Qm[A-Za-z0-9]{10,}/);
});

test('export payments/payroll print one JSON row per line (not just anchors)', async () => {
Expand All @@ -185,8 +185,8 @@ test('export --csv writes the file, not stdout', async () => {
assert.equal(r.code, 0, r.stdout + r.stderr);
assert.match(r.stdout, /Wrote/);
const content = fs.readFileSync(out, 'utf8');
assert.match(content, /anchored_at,source,cid,network/);
assert.match(content, /QmAnchorOne/);
assert.match(content, /anchored_at,source,network/);
assert.doesNotMatch(content, /Qm[A-Za-z0-9]{10,}/);
} finally {
fs.rmSync(out, { force: true });
}
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk-python/src/nirium/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,10 @@ async def submit_payout_onboard(self, signed_xdr: str) -> Dict[str, Any]:
return await self._post("/api/payroll/onboard/submit", {"signedXdr": signed_xdr})

async def get_payout_runs(self) -> Dict[str, Any]:
"""Payout history for the current network, with tx hashes and IPFS receipt CIDs."""
"""Public payout history for the current network: amount, asset, recipient count and tx hash.

It does not include run ids or IPFS receipt CIDs; your own run's CID comes back
from submit_payout()."""
return await self._get("/api/payroll/runs")

async def get_payout_terms(self) -> Dict[str, Any]:
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to the `nirium` package are documented here.

## Unreleased

## 0.19.0 - unreleased

### Changed

- `getPayoutRuns()` now returns `{ runs: PublicPayoutRun[] }` instead of `PayoutRun[]`. The node's public `GET /api/payroll/runs` no longer includes `runId` or the IPFS receipt `cid`: a run's receipt is the paying company's evidence, so the public history keeps only what is already public on-chain (amount, asset, recipient count, tx hash). Your own run's CID still comes back from `submitPayout()`. TypeScript code that read `runId` or `cid` from `getPayoutRuns()` will no longer compile; at runtime those fields are `undefined` once a node runs the new version.

## 0.18.0 - 2026-11-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nirium",
"version": "0.18.0",
"version": "0.19.0",
"description": "Autonomous treasury and agentic-payments infrastructure for Stellar (x402)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
24 changes: 22 additions & 2 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,22 @@ export interface PayoutRun {
[key: string]: unknown;
}

/**
* One entry of the public payout history (getPayoutRuns). It carries what is
* already public on-chain - amount, asset, recipient count and the tx - and
* never the run id or the IPFS receipt CID: the receipt is the paying
* company's evidence, returned only to whoever runs the payout (submitPayout).
*/
export interface PublicPayoutRun {
network?: string;
asset?: string;
totalAmount?: string;
recipientCount?: number;
txHash?: string;
explorer?: string;
signed?: boolean;
}

export interface AgentAttestationInput {
/** ed25519 public key of the agent, as a Stellar address (G...). */
key: string;
Expand Down Expand Up @@ -924,8 +940,12 @@ export class Agent {
return this.request('POST', '/api/payroll/onboard/submit', { signedXdr });
}

/** Payout history for the current network, each with tx hash and IPFS receipt CID. */
async getPayoutRuns(): Promise<{ runs: PayoutRun[] }> {
/**
* Public payout history for the current network: amount, asset, recipient
* count and tx hash. It does not include run ids or IPFS receipt CIDs; your
* own run's CID comes back from submitPayout().
*/
async getPayoutRuns(): Promise<{ runs: PublicPayoutRun[] }> {
return this.request('GET', '/api/payroll/runs');
}

Expand Down
Loading