Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5a26bea
chore(sdk): bump @stellar/stellar-sdk to ^16.3.0
Eras256 Oct 1, 2026
869c406
chore(sdk): bump @x402/fetch, @x402/stellar and @x402/core to ^2.28.0
Eras256 Oct 1, 2026
bd25e2b
docs(sdk): require Node.js >= 22, not 18
Eras256 Oct 1, 2026
30327fb
feat(sdk): pre-sign policy hook for x402Fetch
Eras256 Oct 1, 2026
075b179
docs(sdk): CHANGELOG: x402Serve guard shipped in 0.15.0, not Unreleased
Eras256 Oct 1, 2026
59714fe
fix(cli): scaffold templates pin nirium ^0.16.0
Eras256 Oct 1, 2026
6092365
feat(sdk): x402Fetch says so when core's per-payment cap blocks a pay…
Eras256 Oct 1, 2026
735ffd4
chore(sdk): declare engines.node >=22
Eras256 Oct 1, 2026
5f1c68a
fix(sdk): policy gate refuses when the clock is not a finite number
Eras256 Oct 2, 2026
8ebbdf3
fix(sdk): policy receipts say SIGNED only after the signer returned a…
Eras256 Oct 2, 2026
e84bcb3
fix(sdk): policy observer runs before the final freshness and identit…
Eras256 Oct 2, 2026
caaf23c
docs(sdk): describe the policy receipts, the clock rule and the obser…
Eras256 Oct 2, 2026
1dc5969
fix(sdk): policy observer that returns a promise cannot cause an unha…
Eras256 Oct 2, 2026
27db480
chore(sdk): require Node.js >= 22.12.0, not 22
Eras256 Oct 1, 2026
61ce4e0
fix(sdk): initMpp() builds the MPP client the way mppx documents
Eras256 Oct 1, 2026
3eeca50
docs(sdk): say plainly that MPP Charge is experimental
Eras256 Oct 1, 2026
0ea697d
docs(sdk-python): document that mpp_fetch() cannot pay an MPP server
Eras256 Oct 1, 2026
0001f4a
docs(sdk): state exactly what was tested for MPP, and that mainnet wa…
Eras256 Oct 1, 2026
849ab9d
chore(sdk): date the 0.16.0 changelog and drop MPP from the package d…
Eras256 Oct 1, 2026
04e4064
docs(sdk): mark the pre-sign policy hook as experimental and state it…
Eras256 Oct 2, 2026
77cb87f
docs(sdk): label the capacity limit of the policy hook L01, like L02
Eras256 Oct 2, 2026
cf1e721
docs(sdk): date the 0.16.0 changelog 2026-10-02
Eras256 Oct 2, 2026
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
5 changes: 3 additions & 2 deletions .github/workflows/sdk-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ jobs:
with:
node-version: 22
# --legacy-peer-deps: @stellar/mpp@0.7.1 peer-requires
# @stellar/stellar-sdk@^15.1.0 while this package pins ^14.5.0 - a
# pre-existing mismatch, unrelated to this workflow, not fixed here.
# @stellar/stellar-sdk@^15.1.0 while this package pins ^16.3.0 (what
# @x402/stellar needs) - a pre-existing mismatch, unrelated to this
# workflow, not fixed here. No @stellar/mpp release accepts 16 yet.
- run: npm install --no-audit --no-fund --legacy-peer-deps
- run: npm run build
- run: npm test
4 changes: 2 additions & 2 deletions packages/cli/bin/nirium.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ function scaffoldX402(dir, name) {
type: 'module',
scripts: { dev: 'tsx watch src/server.ts', build: 'tsc' },
dependencies: {
nirium: '^0.15.0',
nirium: '^0.16.0',
express: '^5.1.0',
'@x402/express': '^2.17.0',
'@x402/core': '^2.17.0',
Expand Down Expand Up @@ -252,7 +252,7 @@ function scaffoldTS(dir, name) {
"build": "tsc"
},
dependencies: {
"nirium": "^0.15.0",
"nirium": "^0.16.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"dotenv": "^16.4.5"
Expand Down
22 changes: 11 additions & 11 deletions packages/sdk-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Autonomous treasury and agentic-payments infrastructure for **Nirium Protocol** on Stellar/Soroban — Python client.

Nirium agents rebalance USDC ↔ CETES (tokenized Mexican T-bills via Etherfuse) 24/7 without human intervention. Built for developers who want to integrate autonomous treasury management, agentic payments (x402 + MPP), and real-time market signals into their applications.
Nirium agents rebalance USDC ↔ CETES (tokenized Mexican T-bills via Etherfuse) 24/7 without human intervention. Built for developers who want to integrate autonomous treasury management, agentic payments with x402, and real-time market signals into their applications.

## Install

Expand Down Expand Up @@ -132,15 +132,15 @@ Environment variables: `STELLAR_SECRET_KEY` (or `STELLAR_TESTNET_SECRET_KEY`), o

Runnable example: [`examples/langchain-x402-agent`](../../examples/langchain-x402-agent).

### MPP — Session-Based Budget Delegation
```python
agent.init_mpp(
secret_key="S...",
network="stellar:testnet",
)
### MPP Charge: does not work in this version

response = await agent.mpp_fetch("https://nirium-agent.fly.dev/api/v1/mpp/signals")
```
`init_mpp()` and `mpp_fetch()` in nirium 0.11.0 cannot pay an MPP Charge server. `mpp_fetch()` raises `ValueError: This is not a valid account:` before sending anything, because it:

- reads the payment challenge from the JSON body of the `402`, but MPP sends it in the `WWW-Authenticate` header;
- builds a classic Stellar payment, where MPP Charge needs a Soroban SAC transfer;
- sends its proof in an `X-PAYMENT` header, where MPP uses `Authorization: Payment`.

This is not fixed in the version published today. Use x402 (`init_x402()` / `x402_fetch()`) for paid endpoints. The TypeScript package has an MPP Charge client (experimental, see its README); Nirium's hosted testnet `/api/v1/mpp/*` endpoint also rejected MPP payments when tested on 1 October 2026 (mainnet not tested), so do not rely on the hosted endpoints from any language.

### Endpoint Access Model

Expand All @@ -150,7 +150,7 @@ response = await agent.mpp_fetch("https://nirium-agent.fly.dev/api/v1/mpp/signal
| **Protected** (API key) | `execute`, `market`, `loop/start\|stop\|scan`, `subscriptions`, `skills/install`, `webhooks` |
| **WebSocket** (JWT) | `/ws/signals` — real-time signal stream |
| **x402 Premium** | `/api/v1/premium/signals` ($0.02 USDC), `/api/v1/premium/market` ($0.05 USDC) |
| **MPP** | `/api/v1/mpp/signals`, `/api/v1/mpp/market` |
| **MPP Charge** | `/api/v1/mpp/signals`, `/api/v1/mpp/market` (testnet endpoint rejected MPP payments when tested, mainnet untested, and `mpp_fetch()` does not work in this version) |

## Payouts

Expand Down Expand Up @@ -234,7 +234,7 @@ Anchor a **hash** rather than the data itself: IPFS content cannot be deleted, s
| WebSocket | `subscribe()`, `on()` decorator |
| x402 Payments | `init_x402()`, `x402_fetch()` |
| LangChain | `NiriumX402Tool`, `create_nirium_x402_tool()` |
| MPP Payments | `init_mpp()`, `mpp_fetch()` |
| MPP Charge (does not work in this version, see above) | `init_mpp()`, `mpp_fetch()` |

## Requirements

Expand Down
18 changes: 17 additions & 1 deletion packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@

All notable changes to the `nirium` package are documented here.

## Unreleased
## 0.16.0 - 2026-10-02

### Added

- `initX402({ policy })` (experimental): pre-sign policy hook for `x402Fetch()` ([#96](https://github.com/nirium-protocol/nirium/issues/96)). The policy is asked after the Stellar authorization is built and before anything is signed; only an `ALLOW` bound to that exact authorization (`contextHash`) and still valid reaches the signer. Optional `currentVersion` re-reads the policy version right before signing and refuses a stale `ALLOW` (not atomic with signing). Refusals throw `X402PolicyError` with an `outcome`. A clock that is not a finite number refuses with `CLOCK_INVALID`. The `onDecision` receipts are `ALLOWED` (notice), then `SIGNED` only once the signer returned a signature or `SIGNER_ERROR` if it did not; the observer runs before the final clock and identity checks, and a promise it returns is not awaited and its rejection is swallowed. Without `policy`, behavior is unchanged. New exports: `X402PolicyError` and the `X402Policy*` types.
- `x402Fetch()` throws `X402SpendCapError` when `@x402/core` (2.23.0 and later) refuses a payment for being above its default per-payment cap of $1. It states the amount asked for, the cap and that the signer was not called, instead of the generic `Failed to create payment payload: ... rejected by spendControls.maxAmountPerPayment` error. Other `spendControls` rejections are unchanged. The cap itself is not exposed in `initX402()` yet. New export: `X402SpendCapError`.
- The gate accepts both auth preimage variants stellar-sdk 16 can produce, the legacy one and CAP-71 (`...WithAddress`), the latter only when bound to the signer's address.

### Fixed

- `initMpp()` threw `TypeError: Mppx.create is not a function` in 0.15.0: it called the wrong export of `mppx` with a configuration the library does not take. It now builds the client the way `mppx` and `@stellar/mpp` document (`Mppx` from `mppx/client`, `stellar.charge()` from `@stellar/mpp/charge/client`) with `polyfill: false`, so it never replaces `globalThis.fetch` (which would also have intercepted the 402s of `x402Fetch`). `MppConfig.network` is still accepted but no longer used: the network comes from the server's challenge. Checked against the agent's own MPP middleware on testnet in `pull` and `push` mode. MPP Charge is still **not** verified end to end against Nirium's hosted endpoints: the testnet one rejected the payment when tested on 2026-10-01, and the mainnet one was not tested; see the README.

### Changed

- `@stellar/stellar-sdk` ^16.3.0 and `@x402/fetch`/`@x402/stellar`/`@x402/core` ^2.28.0. Node.js >= 22.12.0 is now required, and `nirium` declares it in `engines.node` so npm warns up front. The two packages declare `>=22.0.0`, but stellar-sdk 16 pulls in the ESM-only `@noble/hashes` 2.x, and `require()` of an ES module works without a flag only from Node 22.12.0 (on 22.11 `require('nirium')` throws `ERR_REQUIRE_ESM`).

## 0.15.0 - 2026-09-24

### Added

Expand Down
88 changes: 77 additions & 11 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Autonomous treasury and agentic-payments infrastructure for **Nirium Protocol** on Stellar/Soroban.

Nirium agents rebalance USDC ↔ CETES (tokenized Mexican T-bills via Etherfuse) 24/7 without human intervention. Built for developers who want to integrate autonomous treasury management, agentic payments (x402 + MPP) — in both directions, paying for other APIs with `initX402()` and charging for your own with `x402Serve()` — and real-time market data into their applications.
Nirium agents rebalance USDC ↔ CETES (tokenized Mexican T-bills via Etherfuse) 24/7 without human intervention. Built for developers who want to integrate autonomous treasury management, agentic payments with x402 (in both directions: paying for other APIs with `initX402()` and charging for your own with `x402Serve()`), an experimental MPP Charge client, and real-time market data into their applications.

## Install

Expand Down Expand Up @@ -61,7 +61,7 @@ agent.subscribe((signal) => {
| Admin | `configureLLM()` |
| WebSocket | `subscribe()`, `onLog()`, `disconnect()` |
| x402 Payments | `initX402()`, `x402Fetch()` |
| MPP Payments | `initMpp()`, `mppFetch()` |
| MPP Charge client (experimental, see below) | `initMpp()`, `mppFetch()` |

## Authentication

Expand Down Expand Up @@ -93,18 +93,84 @@ const response = await agent.x402Fetch('https://nirium-agent.fly.dev/api/v1/prem
const data = await response.json();
```

### MPP — Session-Based Budget Delegation
#### Per-payment cap

`@x402/core` 2.23.0 and later refuse any payment above a default cap of **$1** per request (a default asset such as USDC), before anything is signed. `x402Fetch()` now says so with an `X402SpendCapError` instead of a generic error:

```typescript
agent.initMpp({
secretKey: 'S...',
network: 'stellar:testnet',
mode: 'pull',
import { X402SpendCapError } from 'nirium';

try {
await agent.x402Fetch(url);
} catch (e) {
if (e instanceof X402SpendCapError) {
console.log(e.formattedAmount, e.cap, e.signerCalled); // '2 USDC' '$1' false
}
}
```

The error carries `url`, `amount` (atomic units, the cheapest offer when the server lists several), `formattedAmount`, `asset`, `network` and `cap`, and `signerCalled` is always `false`: nothing was signed or sent. The cap is enforced by `@x402/core` and cannot be changed from `initX402()` yet. Other `spendControls` rejections (for example a non-default asset) still surface as the original error.

#### Pre-sign policy hook (experimental)

Pass `policy` to `initX402()` and every `x402Fetch()` asks your policy before anything is signed. The question is asked after the Stellar authorization is built, so the policy sees exactly what would be signed: amount, destination, asset, nonce, expiration ledger and network, decoded from the bytes.

**Experimental, new in 0.16.0.** The shape of `policy` (the context object, the outcomes, the records `onDecision` receives) can still change in a minor release before 1.0.0. Its scope is the agent-side cases that [#96](https://github.com/nirium-protocol/nirium/issues/96) groups as G1 (they come from the harness of @CodeDeityX), and nothing beyond them. What it does not cover is listed in the last paragraph of this section: code in the same process that holds the raw signer (L02 in that discussion), and pending-capacity reservation (L01: concurrent requests that observe the same remaining capacity). Neither is solved.

```typescript
import { X402PolicyError } from 'nirium';

agent.initX402({
signer: walletSigner,
network: 'stellar:pubnet',
policy: {
evaluate: async (ctx) => {
const ok = BigInt(ctx.authorization.amount) <= myLimit;
return ok
? { decision: 'ALLOW', contextHash: ctx.contextHash, policyVersion: 'v7', expiresAt: Date.now() + 2_000 }
: { decision: 'DENY', reason: 'over limit' };
},
// Optional: the policy version in force right now.
currentVersion: async () => myPolicyStore.version(),
},
});

const response = await agent.mppFetch('https://nirium-agent.fly.dev/api/v1/mpp/signals');
const data = await response.json();
try {
await agent.x402Fetch(url);
} catch (e) {
if (e instanceof X402PolicyError) console.log(e.outcome); // DENY, WAIT, TIMEOUT, STALE, ...
}
```

Only an `ALLOW` that echoes this authorization's `contextHash`, and is still valid, reaches the signer. Everything else signs nothing: `DENY`, `WAIT`, an exception, no answer before the deadline (`timeoutMs`, default 5 s, never more than half the payment's `maxTimeoutSeconds`), a malformed answer, an `ALLOW` past its `expiresAt`, a signer whose address changed while the policy was being asked, or a clock (`now`) that does not return a finite number (`CLOCK_INVALID`). Without `policy`, `x402Fetch()` behaves exactly as before.

**Stale ALLOW.** With `currentVersion` set, it is read after the `ALLOW` and right before signing, and the `ALLOW` signs only if its `policyVersion` matches. A rejected, empty or late read signs nothing. This check is **not atomic with signing**: the version can change after it is read, or while the signer runs. It narrows the window to the synchronous step between the read and the signer call; it does not close it. `expiresAt` is checked again after the read.

**Receipts (`onDecision`).** A payment whose `ALLOW` is accepted produces an `ALLOWED` record. `ALLOWED` does **not** mean every check has passed: it is a notice, sent before the policy-version read and the final clock and identity checks. What follows it is either a refusal from those checks (`STALE`, `EXPIRED`, `TIMEOUT`, `CLOCK_INVALID`, `SIGNER_CHANGED`, ...) or the signer's result: `SIGNED` (the signer returned a signature) or `SIGNER_ERROR` (it rejected, threw, or returned no `signedAuthEntry`; its own error is rethrown untouched). `SIGNED` therefore means a signature exists. The observer cannot change the outcome. A synchronous throw from it is swallowed. If it returns a promise (or any thenable), that is not awaited, so it cannot delay a payment, and its rejection is swallowed so it never becomes an unhandled rejection. It runs before the final clock and identity checks, so what it changes there is detected.

**Rejected before the policy is consulted.** An authorization that is not a single `transfer(from, to, amount)` matching the selected payment requirements (asset, destination, amount, network, no sub-invocations, `from` equal to the signer) is refused without calling `evaluate`. A CAP-71 preimage must be bound to the signer's own address.

**Signature check.** The hook only checks that the signer returned a non-empty `signedAuthEntry`; it does not verify the signature itself. That check comes from `@stellar/stellar-sdk`: `authorizeEntry` verifies the signature against sha256 of the preimage before it enters the transaction (verified in 16.3.0, the version this package requires). A signature over different bytes, or by a different key, never reaches the merchant; a test in this package pins that.

**What this does not do.** It is a check on the agent side, not account-level enforcement: code in the same process that holds the raw signer can still call it directly (the L02 limit in #96; so can code that replaces its `signAuthEntry`, which is looked up at call time), and nothing on-chain enforces the policy. It does not solve pending-capacity reservation (L01 in #96: concurrent requests that observe the same remaining capacity): two calls evaluated at the same time can each fit a limit that together they exceed, and an aggregate cap has to be held by your policy. Discussed in [#96](https://github.com/nirium-protocol/nirium/issues/96), where @CodeDeityX laid out the agent-side cases this hook is built against.

### MPP Charge (experimental)

```typescript
agent.initMpp({ secretKey: 'S...', mode: 'pull' }); // 'pull' (default) or 'push'

const response = await agent.mppFetch('https://your-mpp-server.example/resource');
```

`initMpp()` builds an MPP Charge client: the server answers `402` with a challenge, the client signs a USDC transfer on Stellar, and the server verifies and settles it. The network comes from the server's challenge, so `network` in the config is accepted but unused.

**Do not use it in production, and do not point it at Nirium's hosted endpoints yet.**

- The client works against a compliant MPP Charge server. We checked it against Nirium's own MPP middleware running locally against testnet, in `pull` and in `push` mode.
- When we tested on 1 October 2026, Nirium's hosted **testnet** endpoint (`/api/v1/mpp/*`) rejected MPP payments with `402 Verification Failed`, in `pull` and in `push` mode. We have **not tested the mainnet endpoint** (it would spend real USDC), so we cannot tell you it works there. We have not found the cause yet.
- In `push` mode the payment settles on-chain before the server verifies it, so a request the server rejects is **not refunded**.
- Until this is diagnosed, use x402 (`initX402()`) for paid endpoints. The `get_mpp_*` tools of the MCP server have the same limitation.

### x402Serve() — Charging Your Own API

`initX402()` above lets you *pay* for someone else's API. This is the other side: charging for yours.
Expand Down Expand Up @@ -135,7 +201,7 @@ Runs on **your own server**, not Nirium's — `x402Serve()` is a client-side fun
| **Protected** (API key) | `execute`, `market`, `loop/start\|stop\|scan`, `subscriptions`, `skills/install`, `webhooks` |
| **WebSocket** (JWT) | `/ws/signals` — real-time signal stream |
| **x402 Premium** | `/api/v1/premium/signals` ($0.02 USDC), `/api/v1/premium/market` ($0.05 USDC) |
| **MPP** | `/api/v1/mpp/signals`, `/api/v1/mpp/market` |
| **MPP Charge** | `/api/v1/mpp/signals`, `/api/v1/mpp/market` (testnet endpoint rejected MPP payments when tested, mainnet untested, see [MPP Charge](#mpp-charge-experimental)) |

### x402 Metrics — Observability Wrapper

Expand Down Expand Up @@ -272,7 +338,7 @@ Anchor a **hash** rather than the data itself: IPFS content cannot be deleted, s

## Requirements

- Node.js >= 18
- Node.js >= 22.12.0. `@stellar/stellar-sdk` 16 and `@x402/stellar` 2.28 declare `>=22.0.0`, but stellar-sdk 16 depends on `@noble/hashes` 2.x, which is ESM-only, and this package is published as CommonJS: `require('nirium')` only works without a flag from Node 22.12.0 (on 22.11 it throws `ERR_REQUIRE_ESM`)
- TypeScript >= 5.0

## Links
Expand Down
Loading
Loading