Modular, production-ready Agent Kit for autonomous PayFi (Payment-Finance) flows on the Stellar Network.
Nodal AI empowers developers to build autonomous agents capable of handling complex financial interactions. Whether you’re automating cross-border settlements, building machine-to-machine payment gateways, or orchestrating smart contract executions, Nodal AI provides the primitives to do it securely and efficiently on Stellar.
In the era of PayFi, payments are no longer just passive transfers they are programmable, autonomous, and integrated into the global financial fabric. Nodal AI bridges the gap between AI reasoning and Stellar's high-speed, low-cost network.
- Autonomous PayFi: Built-in support for the
x402payment standard, enabling seamless machine-to-machine value exchange. - Modular Architecture: Swap in new tools, chain actions, and orchestrate complex workflows without touching core signing logic.
- Safety-First Design: Every transaction is simulated via Soroban RPC before broadcast, and all secrets remain strictly externalized.
Nodal AI is built on a clean, three-pillar separation of concerns. For a deep dive into the system design, tool dispatch, simulation gates, and state machines, please read the Architecture Guide.
/
├── backend/ # Agent orchestration (TypeScript/Node.js)
├── contracts/ # Soroban smart contracts (Rust)
└── tests/ # E2E & integration tests (Vitest)
-
Clone & Configure:
git clone https://github.com/your-username/nodal-ai.git cd nodal-ai cp .env.example .envOpen
.envand fill in at minimumAGENT_SECRET_KEY,HORIZON_URL,SOROBAN_RPC_URL, andX402_ASSET_ISSUER. See.env.examplefor the full list of variables and their descriptions. -
Install Dependencies:
npm install
-
Verify Installation:
npm run build npm run test:all
The backend/ pillar contains the "Agent Brain." Use it to define tools and manage agent state.
npm run build: Compiles the TypeScript agent core.
The contracts/ pillar holds your escrow and payment logic.
cd contracts/escrow && cargo test: Run the suite of Soroban unit tests to ensure contract safety.
We use Vitest to ensure the entire flow—from AI reasoning to network settlement—works as expected.
npm run test: Executes the/testssuite.npm run test:ui: Runs the test suite with the interactive Vitest UI.
Nodal AI includes a multi-stage Dockerfile and Docker Compose stack for local development, testing, and deployment.
-
Start the local Stellar network and Nodal agent:
docker-compose up --build
This will spin up:
stellar-quickstartathttp://localhost:8000(Horizon) andhttp://localhost:8001(Soroban RPC).agentwhich automatically connects to the quickstart services once they are healthy.
-
Stop the services and clean up containers:
docker-compose down
You can run the test suite within an isolated test runner container:
docker-compose --profile test up --buildSecurity is the foundation of PayFi. See SECURITY.md for the full responsible disclosure policy, response SLAs, core security invariants, and secret management guidelines.
To report a vulnerability privately, use GitHub Security Advisories.
PayFiAgent enforces two layers of spending limits to prevent runaway payments:
- AGENT_SPENDING_LIMIT: A configurable per-transaction ceiling (set via environment variable). Before every payment task (
stellar_paymentorx402_respond), theassertWithinSpendingLimit()function inbackend/agent.tschecks the requested amount. If it exceedsAGENT_SPENDING_LIMIT, the task fails immediately with error:"Payment amount X exceeds AGENT_SPENDING_LIMIT of Y". - Mainnet Spending Cap: A hardcoded safety ceiling of 10,000 on mainnet. Even if
AGENT_SPENDING_LIMITis misconfigured above this, mainnet transactions are blocked if they exceed 10,000, throwing:"Payment amount X exceeds mainnet spending cap of 10,000".
These limits apply to:
- Direct
stellar_paymenttasks viaStellarPaymentTool x402_respondtasks that trigger automatic payment viaX402PaymentTool
Before deploying to Stellar mainnet, verify the following:
- Set
STELLAR_NETWORK=mainnet— This enables the mainnet spending cap and enforces HTTPS-only RPC connections. - Set
AGENT_SPENDING_LIMITbelow 10,000 — The limit should reflect your acceptable per-transaction maximum (e.g.,1000for USD denominations). Values above 10,000 will be rejected on mainnet. - Verify
X402_ASSET_ISSUER— Confirm this is the canonical USDC anchor account on mainnet. Misconfiguration sends payments to the wrong issuer. - Test with
simulateOnly: truefirst — Forsoroban_invoketasks, setsimulateOnly: trueto dry-run contract logic without broadcasting. This validates gas estimation and state changes in a safe sandbox.
All four checks are enforced at startup via backend/config.ts validation and at task dispatch time via backend/agent.ts guards.
We are actively participating in the Stellar Wave program! We welcome contributions ranging from bug fixes to new tool modules.
- Check the Issues tab for tickets tagged
good first issueorhelp wanted. - Follow the CONTRIBUTING.md guide.
- Submit a Pull Request and join our community in the next Wave sprint to earn Drips points for your contributions!
Three runnable scripts in scripts/examples/ demonstrate each TaskType with real payloads. Copy .env.example to .env and fill in your values, then run any script with:
npx ts-node scripts/examples/<script>.tsSends 1 XLM to a recipient account on testnet.
npx ts-node scripts/examples/send_xlm.tsCalls get_state on a deployed escrow contract (simulate-only, no broadcast). Pass the contract address via CONTRACT_ID:
CONTRACT_ID=C... npx ts-node scripts/examples/invoke_escrow.tsResponds to a sample x402 payment challenge and prints the resulting X402PaymentProof.
npx ts-node scripts/examples/respond_x402.tsEnd-to-end tests run against the live Stellar testnet (not mocked). They require network access to Friendbot and Soroban RPC.
npm run test:e2eThe E2E suite is excluded from the default npm run test to keep CI fast. Run it separately before releases or after SDK upgrades.
Released under the MIT License.
Built for the Stellar ecosystem by [Dami24-hub].
## API Reference
### PayFiAgent
The primary integration surface for developers. Dispatch tasks to the agent via `run()` or `runSequence()`.
| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `run(task)` | `AgentTask` | `Promise<AgentResult>` | Execute a single task. Routes to the appropriate tool based on task type. |
| `runSequence(tasks)` | `AgentTask[]` | `Promise<AgentResult[]>` | Execute an ordered list of tasks sequentially, stopping on first failure. |
| `destroy()` | — | `void` | Detach all event listeners and release resources. Call when decommissioning the agent. |
### TaskType
```typescript
type TaskType = "stellar_payment" | "soroban_invoke" | "x402_respond" | "path_payment" | "fee_bump"
```
| Value | Description |
|-------|-------------|
| `stellar_payment` | Native XLM or custom asset payment via Horizon |
| `soroban_invoke` | Smart contract invocation via Soroban RPC with simulation |
| `x402_respond` | Respond to an x402 payment challenge with spending limit guard |
| `path_payment` | Cross-asset path payment strict send via the Stellar DEX |
| `fee_bump` | Wrap an existing transaction in a fee-bump envelope for sponsored retry |
### AgentTask
```typescript
interface AgentTask {
type: TaskType;
payload: unknown;
}
```
Input wrapper for task dispatch. The `payload` shape depends on `type`:
- `stellar_payment`: `{ destination: string; amount: string; assetCode?: string; assetIssuer?: string; memo?: string }`
- `soroban_invoke`: `{ contractId: string; method: string; args: SorobanValue[]; ... }`
- `x402_respond`: `{ resource: string; amount: string; assetCode?: string; assetIssuer?: string; payTo: string; nonce: string; expiresAt: string }`
### AgentResult
```typescript
interface AgentResult {
success: boolean;
taskType: TaskType;
data?: unknown;
error?: string;
}
```
Task execution result. On success, `data` contains the tool's output. On failure, `error` is populated.
### Usage Example
```typescript
import { PayFiAgent } from "./backend/agent";
const agent = new PayFiAgent();
// Execute a Stellar payment
const result = await agent.run({
type: "stellar_payment",
payload: {
destination: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
amount: "100",
assetCode: "USDC",
assetIssuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
},
});
if (result.success) {
console.log("Payment settled:", result.data);
} else {
console.error("Payment failed:", result.error);
}
// Clean up
agent.destroy();
```
---
## x402 Payment Flow
Nodal AI implements the [x402](https://github.com/x402-foundation/x402) protocol so the agent can pay for gated resources autonomously. The verified flow below covers what happens once `PayFiAgent.run()` is dispatched an `x402_respond` task — the upstream step of a resource server actually issuing the 402 challenge happens outside this codebase (in whatever client first calls `PayFiAgent.run()`), so it's described in prose rather than diagrammed.
```mermaid
sequenceDiagram
participant C as Caller
participant PA as PayFiAgent
participant X as X402PaymentTool
participant ST as StellarPaymentTool
participant H as Horizon
C->>PA: run({ type: "x402_respond", payload })
PA->>PA: assertWithinSpendingLimit(amount)
PA->>X: respond(challenge)
X->>X: X402ChallengeSchema.parse(challenge)
Note over X: reject if amount > AGENT_SPENDING_LIMIT<br/>reject if expiresAt has passed
X->>ST: execute({ destination: payTo, amount, memo })
ST->>H: sign + submit transaction
H-->>ST: txHash, ledger
ST-->>X: { txHash, ledger }
X-->>PA: X402PaymentProof
PA-->>C: AgentResult
In practice, the payload handed to run() is the parsed body of a 402 Payment Required response from a resource server, conforming to X402ChallengeSchema — but the HTTP exchange that obtains and replays that challenge is the caller's responsibility, not X402PaymentTool's.
Before PayFiAgent.run() delegates to X402PaymentTool, it calls assertWithinSpendingLimit() on the payload's amount field. If the requested amount exceeds config.AGENT_SPENDING_LIMIT, the task throws immediately and X402PaymentTool.respond() is never invoked — the agent will not even attempt to validate or pay a challenge above its configured budget.
Once past the spending check, X402PaymentTool.respond() validates the challenge against this schema before doing anything else:
| Field | Type | Description |
|---|---|---|
resource |
string (URL) |
The URL of the resource being gated. Must be a valid URL. |
amount |
string |
The amount due, as a string to avoid floating-point precision issues in transit. |
assetCode |
string |
The Stellar asset code to pay in (e.g. USDC, or XLM for native). Defaults to config.X402_ASSET_CODE. |
assetIssuer |
string |
The issuing account of the asset. Defaults to config.X402_ASSET_ISSUER. Ignored when assetCode is XLM. |
payTo |
string |
The recipient Stellar account. Must be exactly 56 characters (a valid Stellar public key). |
nonce |
string (UUID v4) |
A unique identifier for this challenge, used to correlate the resulting payment with the original request. |
expiresAt |
string (ISO datetime) |
The deadline after which the challenge is no longer valid. Checked against new Date() before any payment is attempted. |
If rawChallenge doesn't conform to this shape, X402ChallengeSchema.parse() throws and no payment is attempted.
The challenge's nonce is a UUID v4 (36 characters), but Stellar's text memo field caps at 28 bytes. X402PaymentTool truncates it directly:
memo: challenge.nonce.slice(0, 28);This embeds enough of the nonce on-chain for a resource server to correlate a settled transaction with the original challenge by memo lookup on Horizon. Note this is a string truncation, not a hash — a resource server verifying the proof should compare against the same slice(0, 28) of the nonce it issued.
Once StellarPaymentTool.execute() returns a settled txHash, respond() builds and returns:
interface X402PaymentProof {
protocol: "x402"; // protocol tag, always "x402"
network: string; // config.STELLAR_NETWORK
txHash: string; // settled Stellar transaction hash
nonce: string; // the original challenge nonce, in full
payer: string; // the agent's Stellar public key
signedAt: string; // ISO timestamp the proof was issued
}The proof carries no embedded signature of its own. Verification is delegated to whatever consumes the proof: it looks up txHash on Horizon and confirms the payment's destination, amount, and memo match what the original challenge demanded. The proof is a pointer to on-chain truth, not a self-contained credential.
One thing to flag for reviewers: StellarPaymentTool.execute() does not run a Soroban simulation pass before submission — its own comments note that Horizon has no simulation endpoint, so it validates the transaction envelope locally, signs, and submits directly. Simulation-before-broadcast is real elsewhere in this codebase (SorobanInvokeTool), but not on this payment path — worth keeping the README's general security claims scoped accordingly if they currently imply otherwise project-wide.
import { PayFiAgent } from "./backend/agent";
const agent = new PayFiAgent();
// `challenge` is the parsed JSON body of a 402 response from a resource server
const result = await agent.run({
type: "x402_respond",
payload: challenge,
});
if (result.success) {
const proof = result.data; // X402PaymentProof
// retry the original resource request, attaching `proof`
}See backend/agent.ts for task dispatch and the spending-limit guard, and backend/tools/X402PaymentTool.ts for challenge validation and proof construction.