Skip to content
Β 
Β 

Repository files navigation

Fortexa

Fortexa logo

Policy-Controlled Payment Firewall for Autonomous Agent Actions on Stellar

Fortexa is a policy-controlled payment firewall for autonomous agent actions on Stellar. It sits between agent intent and economic execution, applies governance/risk checks, and keeps an auditable decision trail.

This document reflects the current implementation in this repository.


1) ⚠️ Why This Matters

Agentic systems can now trigger real payments. That creates a new risk layer: high-speed model decisions can become high-impact economic actions.

Fortexa adds a control plane between intent and money movement:

  • Policy checks before execution
  • Risk scoring on suspicious behavior
  • Human-approval gate for sensitive cases
  • Wallet-native signed XDR flow
  • Auditable evidence trail for every decision

In short: Fortexa is the safety layer for agentic payments.


2) πŸš€ Jury Demo Flow (Fast Path)

If you only read one section, read this:

  1. Login with wallet on /login.
  2. Evaluate action in /console.
  3. Receive decision: BLOCK / REQUIRE_APPROVAL / WARN / APPROVE.
  4. For allowed flows, build unsigned XDR β†’ sign in wallet β†’ submit signed XDR.
  5. Verify outcome with Explorer link and inspect evidence in /activity and /ops.

3) 🧭 Current Product Model

Fortexa currently runs with a strict wallet-bound model:

  1. User logs in with wallet (/login).
  2. Session is created with role (operator / viewer).
  3. Session wallet is bound as execution source.
  4. Actions are evaluated by policy + security engine.
  5. Approved/warned decisions can proceed to signed-XDR payment flow.
  6. Decision/audit evidence is stored and visible in /activity and /ops.

4) πŸ” Auth and Access Control

4.1 Wallet-only Login

Fortexa uses a challenge-signature login flow:

  1. Client requests a one-time login challenge via POST /api/auth/challenge with the wallet public key (G...).
  2. The server returns a short-lived challenge message bound to that wallet.
  3. The wallet signs the challenge message (SEP-53 / Freighter signMessage).
  4. Client posts publicKey, challengeId, and signature to POST /api/auth/login.
  5. The server verifies the signature, enforces one-time challenge use + expiry, then issues fortexa_session.

Role is still resolved via allowlists:

  • FORTEXA_OPERATOR_WALLETS
  • FORTEXA_VIEWER_WALLETS

If both allowlists are empty, current behavior falls back to operator role for any valid wallet (recommended only for local/dev).

Session cookie: fortexa_session (HMAC-signed).

Challenge TTL: FORTEXA_AUTH_CHALLENGE_TTL_SECONDS (default 300).

4.2 Role Permissions

  • operator: full decision/policy/payment flow
  • viewer: read-only experience on sensitive execution paths

4.3 Login Hardening

  • Rate limiting
  • Brute-force lockout (FORTEXA_AUTH_MAX_ATTEMPTS, FORTEXA_AUTH_LOCK_MINUTES)

Note: MFA is removed from current implementation.


5) πŸ‘› Wallet and Signing Model (Current)

Fortexa currently does not perform server-side signing or private-key custody.

  • Session is wallet-bound at login.
  • Execution source wallet is derived from session identity.
  • Manual arbitrary wallet assignment in UI is removed.
  • /api/stellar/balance auto-syncs missing wallet mapping from session when possible.

6) βš™οΈ Decision and Payment Flow

6.1 Decisioning

  • Policy engine: src/lib/policy/engine.ts
  • Security analyzer: src/lib/security/analyzer.ts
  • Decision engine: src/lib/decision/engine.ts

Decision outcomes:

  • BLOCK
  • REQUIRE_APPROVAL
  • WARN
  • APPROVE

Human Approve & Re-run applies only when prior result is REQUIRE_APPROVAL.

6.1a Policy Simulation (Pre-Save Safety Check)

Before committing a policy change, operators can dry-run the unsaved draft from the Policy editor (Run simulation). The draft is evaluated against the seeded demo scenarios β€” and, optionally, a small recent-audit sample β€” and the result shows each action's current β†’ proposed decision so risky edits surface before they go live.

Simulation is strictly read-only: it never saves the policy and never consumes usage. Saving still happens only through POST /api/policy. See src/lib/decision/simulate.ts and POST /api/policy/simulate.

6.2 Signed XDR Payment Path

  1. Evaluate action in /console with a payment quote (paymentQuoteInput: destination, optional memo, network). On APPROVE/WARN, Fortexa stores an immutable paymentQuote on the audit entry.
  2. Build unsigned tx: POST /api/stellar/build-payment with auditEntryId plus the same destination, amount, asset, memo, and network. The server verifies every field against the authorized quote before constructing XDR.
  3. Submit Signed XDR orchestrates signing/submission path:
    • if signed input is already present β†’ submit directly
    • if unsigned input is present β†’ wallet signing is triggered first, then submit
  4. Submit signed tx: POST /api/stellar/submit-signed.
  5. Explorer URL is returned and shown as clickable link.

Quote-to-XDR trust boundary

The policy decision authorizes a fixed payment quote (destination, amount, asset, memo, network). POST /api/stellar/build-payment is the enforcement gate: it loads the audit entry by auditEntryId, confirms the decision is APPROVE/WARN, and rejects any request whose fields diverge from the stored quote.

Condition HTTP Response
Missing/invalid body (auditEntryId, schema) 400 Invalid payment build request. + zod details
Unknown audit entry or non-executable decision 403 No authorized payment decision found… / Decision 'BLOCK' does not authorize…
Tampered destination, amount, asset, or memo 403 { error, field } naming the mismatched field
Valid approved request 200 { ok: true, xdr, networkPassphrase, … }

Client-side UI must pass the same paymentQuoteInput at decision time and reuse the returned auditEntry.id when building XDR. Mutating any authorized field after approval cannot produce a valid unsigned transaction.

Idempotent retries: POST /api/stellar/submit-signed accepts an optional idempotency key, supplied either as an Idempotency-Key request header or an idempotencyKey body field (the header wins if both are present). Results are stored per authenticated user + key + signed-XDR hash. Replaying the same key with the same signed XDR returns the original result (200, with header Idempotency-Replayed: true) without resubmitting to Horizon. Reusing the same key with a different signed XDR returns 409 Conflict. Omitting the key preserves the original submit-on-every-request behavior. Keys must be 8–255 characters.

Additional behavior:

  • XDR build timeout configured to 180 seconds.
  • Submit errors include Horizon result codes when available.

7) πŸ“œ Audit and Evidence

  • Decisions are appended to audit store at evaluation time.
  • /activity reads entries by authenticated session user id.
  • Export endpoint supports mine and all scopes in JSON/CSV.

Hash chain integrity

Every new audit entry is linked into a tamper-evident SHA-256 hash chain:

Field Description
previousHash entryHash of the immediately preceding entry, or 0000…0000 (64 zeroes) for the first hashed entry.
entryHash SHA-256 of the entry's canonical fields (id, timestamp, action, decision, explanation, triggeredPolicies, riskFindings, stellarTxHash, previousHash). Object keys are sorted before hashing so DB-stored and file-stored entries produce identical digests.

Both fields are included in JSON exports. CSV exports add entryHash and previousHash columns.

Verification helper: verifyHashChain(entries) in src/lib/audit/hash-chain.ts β€” returns { valid: true } for an untouched log and { valid: false, reason } when it detects a modified, deleted, or reordered entry.

Entries written before this feature was introduced carry no hash fields and are treated as legacy entries; they do not break verification of newer hashed entries.


8) πŸ› οΈ Local Setup

Requirements

  • Node.js 20+
  • npm 10+

Start

npm install
cp .env.example .env.local
npm run dev

Open: http://localhost:3000


9) 🌍 Environment Variables

Reference (.env.example):

STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org

DATABASE_URL=
DATABASE_SSL=false

FORTEXA_STORE_DIR=

FORTEXA_SHARED_STATE_PATH=
REDIS_URL=

GROQ_API_KEY=
GROQ_MODEL=llama-3.3-70b-versatile

FORTEXA_AUTH_SECRET=
FORTEXA_OPERATOR_WALLETS=
FORTEXA_VIEWER_WALLETS=
FORTEXA_AUTH_MAX_ATTEMPTS=5
FORTEXA_AUTH_LOCK_MINUTES=10

NEXT_PUBLIC_STELLAR_DESTINATION=

# Optional external blocklist URL for dynamic threat-intel
# Accepts JSON array of domains or plain-text (one domain per line, # comments ignored)
# Cached in-memory for 5 minutes; feed failures fall back silently
FORTEXA_BLOCKLIST_URL=

10) ▢️ Scripts

npm run dev
npm run build
npm run start
npm run lint
npm run test
npm run test:watch
npm run demo:scenarios
npm run db:migrate

11) πŸ”Œ API Surface (Reference)

Auth

  • POST /api/auth/challenge
  • POST /api/auth/login
  • POST /api/auth/logout
  • GET /api/auth/session
  • POST /api/auth/refresh

Policy

  • GET /api/policy
  • POST /api/policy (operator)
  • POST /api/policy/simulate (operator) β€” read-only pre-save simulation
  • GET /api/policy/history (operator)
  • POST /api/policy/rollback (operator)

Decision / Planning

  • POST /api/decision (operator)
  • POST /api/agent/plan (operator, Groq-backed)

Audit / Observability

  • GET /api/audit
  • GET /api/audit/export?format=json|csv&scope=mine|all&from=<ISO8601>&to=<ISO8601>&decision=APPROVE|WARN|REQUIRE_APPROVAL|BLOCK&domain=<string>&actionId=<string>
    • Filters: from / to (ISO 8601 date), decision, domain, actionId β€” all optional
    • Scope: mine (own entries) or all (operator only)
    • Examples:
      • GET /api/audit/export?format=csv&scope=mine&from=2025-06-01T00:00:00Z&to=2025-06-30T23:59:59Z
      • GET /api/audit/export?format=json&scope=all&decision=BLOCK&domain=malicious.example.com
      • GET /api/audit/export?format=json&scope=mine&actionId=evt_abc123
  • GET /api/health
  • GET /api/metrics (?format=prometheus)

Stellar

  • GET /api/stellar/balance
  • POST /api/stellar/setup (session-wallet bootstrap/sync helper; not manual wallet linking)
  • POST /api/stellar/build-payment
  • POST /api/stellar/submit-signed (supports Idempotency-Key header/body for safe UI retries)
  • POST /api/stellar/pay (legacy disabled)
  • POST /api/stellar/fund (removed behavior, returns 410)

12) πŸ—ΊοΈ Pages

  • / β†’ Overview dashboard
  • /login β†’ Wallet-only authentication (Connect Wallet)
  • /wallet β†’ Session wallet status and balance
  • /console β†’ Decisioning + payment execution console
  • /policies β†’ Policy editor, history, rollback
  • /scenarios β†’ Scenario gallery
  • /activity β†’ Audit trail timeline
  • /ops β†’ Operations/telemetry dashboard

13) πŸ“ˆ Ops / Observability (Appendix)

  • Health endpoint: GET /api/health β€” returns blocklist object with configured, lastRefreshAt, domainCount, lastError
  • Metrics endpoint: GET /api/metrics + Prometheus format
  • /ops dashboard shows:
    • service health
    • total requests
    • error rate
    • signed tx count
    • blocklist feed health (configured, domain count, last refresh, errors)
    • top routes + rolling trend

Ops dashboard initial load is optimized so core telemetry renders first; slow TX-count fetch no longer blocks first paint.

See docs/observability.md for the Prometheus scrape config, sample PromQL (request rate, error rate, p95 latency), and an example alert rule.


14) πŸ’Ύ Persistence (Appendix)

DB-first with File Fallback

Stores include:

  • audit-store
  • policy-store
  • user-wallet-store
  • submit-idempotency-store

If DATABASE_URL is available and healthy, Postgres is used. Otherwise Fortexa falls back to local JSON files:

  • local/dev default: .fortexa/*.json
  • Vercel default: /tmp/fortexa/*.json

Optional overrides:

  • FORTEXA_STORE_DIR to set file-store directory explicitly
  • FORTEXA_SHARED_STATE_PATH for shared lockout/rate-limit state file path
    • use an absolute path on Vercel (example: /tmp/fortexa/shared-security-state.json)
  • REDIS_URL for multi-instance deployments (e.g. Vercel)
    • uses a Redis-backed adapter with automatic, transparent fallback to the file store if Redis is unreachable or unconfigured.

Versioned Migrations

  • Migrations: src/lib/storage/migrations.ts
  • Runner: src/lib/storage/db.ts
  • Tracking table: fortexa_schema_migrations
  • Manual run: npm run db:migrate

15) 🧱 Stack (Appendix)

  • Framework: Next.js App Router (next@16)
  • Language: TypeScript
  • UI: Tailwind CSS + custom UI primitives
  • Validation: zod
  • Charts: recharts
  • Stellar: @stellar/stellar-sdk, optional @stellar/freighter-api
  • Database: pg (optional Postgres, file fallback enabled)
  • Tests: Vitest

16) πŸ§ͺ Known Limitations (Current)

  1. Shared security state supports Redis distributed locking, but defaults to file-based for local development.
  2. Risk scoring remains heuristic-heavy (no external threat-intel integration).
  3. Stellar workflow is testnet-oriented.
  4. Server-side signing remains intentionally disabled.
  5. Full end-to-end automated coverage for the complete decision-to-payment lifecycle is still limited.

Fortexa is intentionally optimized for hackathon clarity and wallet-native control, not full production deployment.


17) ❓ Troubleshooting Payment Failures

Common Stellar Horizon failures during the signed payment flow:

  • tx_bad_seq: The transaction sequence number is incorrect. Wait for pending transactions to clear or refresh your wallet state.
  • tx_insufficient_fee: The provided fee is below the current network minimum. Increase the base fee.
  • op_no_destination: The destination account does not exist on the network. Verify the destination address.
  • op_underfunded: Your source wallet lacks the XLM necessary to complete the payment and satisfy the network base reserve.

18) πŸ›£οΈ Practical Next Steps

  • Add stronger risk intelligence + anomaly detection.
  • Expand end-to-end payment verification and automated lifecycle tests.

19) πŸ“„ License

MIT (see package.json).

About

Policy-Controlled Payment Firewall for Autonomous Agent Actions on Stellar

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages