@agentis-hq/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@agentis-hq/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "agentis": "./dist/index.js"
7
+ },
8
+ "description": "Agentis CLI",
9
+ "exports": {
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "templates",
15
+ "package.json"
16
+ ],
17
+ "scripts": {
18
+ "dev": "bun run src/index.ts",
19
+ "build": "tsup",
20
+ "check": "bun --check src/index.ts"
21
+ },
22
+ "dependencies": {
23
+ "@agentis-hq/core": "^0.1.0",
24
+ "@agentis-hq/sdk": "^0.1.0",
25
+ "@napi-rs/keyring": "^1.2.0",
26
+ "@noble/ciphers": "^2.2.0",
27
+ "@noble/curves": "^2.2.0",
28
+ "@noble/hashes": "^2.2.0",
29
+ "@scure/bip32": "^2.2.0",
30
+ "@scure/bip39": "^2.2.0",
31
+ "@solana-program/system": "^0.12.0",
32
+ "@solana/kit": "^6.8.0",
33
+ "uuid": "^14.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/bun": "latest",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.0.0"
39
+ }
40
+ }
@@ -0,0 +1,20 @@
1
+ AGENTIS_API_URL={{AGENTIS_API_URL}}
2
+ AGENTIS_FACILITATOR_ID={{FACILITATOR_ID}}
3
+ AGENTIS_HEARTBEAT_SECRET={{HEARTBEAT_SECRET}}
4
+ AGENTIS_PUBLIC_URL=
5
+
6
+ NETWORK={{NETWORK}}
7
+ ACCEPTED_MINT={{ACCEPTED_MINT}}
8
+ FACILITATOR_FEE_BPS={{FEE_BPS}}
9
+ FACILITATOR_PORT=3000
10
+ DATABASE_PATH=data/facilitator.db
11
+
12
+ KORA_RPC_URL=http://localhost:8080/
13
+ KORA_API_KEY={{KORA_API_KEY}}
14
+ KORA_PORT=8080
15
+ SOLANA_RPC_URL=https://api.devnet.solana.com
16
+ KORA_PRIVATE_KEY=
17
+
18
+ ADMIN_TOKEN=change-me
19
+ DEFAULT_SELLER_PAY_TO=
20
+ DEFAULT_SELLER_BALANCE_USD=0
@@ -0,0 +1,46 @@
1
+ # {{NAME}}
2
+
3
+ Kora-backed x402 facilitator scaffolded by Agentis.
4
+
5
+ ## What This Does
6
+
7
+ - Exposes x402 `/verify`, `/settle`, and `/supported`.
8
+ - Uses Kora to sponsor/sign/submit Solana transactions.
9
+ - Keeps a local SQLite seller ledger.
10
+ - Charges facilitator fees from prepaid seller balances.
11
+ - Sends heartbeat metrics to Agentis for operator tracking.
12
+
13
+ ## Run
14
+
15
+ ```bash
16
+ bun install
17
+ cp .env.example .env
18
+ ```
19
+
20
+ Fill `KORA_PRIVATE_KEY`, then fund that signer with devnet SOL. Start Kora and the facilitator in separate terminals:
21
+
22
+ ```bash
23
+ bun run kora
24
+ bun run dev
25
+ ```
26
+
27
+ ## Seller Ledger
28
+
29
+ The seller should advertise a gross x402 price that already accounts for facilitator fees. This facilitator settles the gross payment to the seller, then deducts its configured fee from the seller's prepaid local ledger.
30
+
31
+ Add or top up a seller:
32
+
33
+ ```bash
34
+ curl -X POST http://localhost:3000/admin/sellers \
35
+ -H "content-type: application/json" \
36
+ -H "x-admin-token: $ADMIN_TOKEN" \
37
+ -d '{"payTo":"SELLER_WALLET","topUpUsd":10}'
38
+ ```
39
+
40
+ ## Publish
41
+
42
+ After deploying publicly:
43
+
44
+ ```bash
45
+ agentis facilitator publish {{FACILITATOR_ID}} --url https://your-facilitator.example --listed
46
+ ```
@@ -0,0 +1,103 @@
1
+ [kora]
2
+ rate_limit = 100
3
+
4
+ [kora.auth]
5
+ api_key = "{{KORA_API_KEY}}"
6
+
7
+ [kora.cache]
8
+ enabled = false
9
+ default_ttl = 300
10
+ account_ttl = 60
11
+
12
+ [kora.enabled_methods]
13
+ liveness = true
14
+ estimate_transaction_fee = false
15
+ get_supported_tokens = true
16
+ sign_transaction = true
17
+ sign_and_send_transaction = true
18
+ transfer_transaction = false
19
+ get_blockhash = true
20
+ get_config = true
21
+ get_payer_signer = true
22
+ get_version = true
23
+
24
+ [validation]
25
+ max_allowed_lamports = 1000000
26
+ max_signatures = 10
27
+ price_source = "Mock"
28
+ allow_durable_transactions = false
29
+ allowed_programs = [
30
+ "11111111111111111111111111111111",
31
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
32
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
33
+ "ComputeBudget111111111111111111111111111111",
34
+ "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
35
+ ]
36
+ allowed_tokens = [
37
+ "{{ACCEPTED_MINT}}"
38
+ ]
39
+ allowed_spl_paid_tokens = [
40
+ "{{ACCEPTED_MINT}}"
41
+ ]
42
+ disallowed_accounts = []
43
+
44
+ [validation.fee_payer_policy]
45
+
46
+ [validation.fee_payer_policy.system]
47
+ allow_transfer = false
48
+ allow_assign = false
49
+ allow_create_account = false
50
+ allow_allocate = false
51
+
52
+ [validation.fee_payer_policy.system.nonce]
53
+ allow_initialize = false
54
+ allow_advance = false
55
+ allow_authorize = false
56
+ allow_withdraw = false
57
+
58
+ [validation.fee_payer_policy.spl_token]
59
+ allow_transfer = false
60
+ allow_burn = false
61
+ allow_close_account = false
62
+ allow_approve = false
63
+ allow_revoke = false
64
+ allow_set_authority = false
65
+ allow_mint_to = false
66
+ allow_initialize_mint = false
67
+ allow_initialize_account = false
68
+ allow_initialize_multisig = false
69
+ allow_freeze_account = false
70
+ allow_thaw_account = false
71
+
72
+ [validation.fee_payer_policy.token_2022]
73
+ allow_transfer = false
74
+ allow_burn = false
75
+ allow_close_account = false
76
+ allow_approve = false
77
+ allow_revoke = false
78
+ allow_set_authority = false
79
+ allow_mint_to = false
80
+ allow_initialize_mint = false
81
+ allow_initialize_account = false
82
+ allow_initialize_multisig = false
83
+ allow_freeze_account = false
84
+ allow_thaw_account = false
85
+
86
+ [validation.fee_payer_policy.alt]
87
+ allow_create = false
88
+ allow_extend = false
89
+ allow_freeze = false
90
+ allow_deactivate = false
91
+ allow_close = false
92
+
93
+ [validation.price]
94
+ type = "free"
95
+
96
+ [kora.usage_limit]
97
+ enabled = false
98
+ cache_url = "redis://localhost:6379"
99
+ max_transactions = 1000
100
+ fallback_if_unavailable = false
101
+
102
+ [kora.bundle]
103
+ enabled = false
@@ -0,0 +1,8 @@
1
+ [signer_pool]
2
+ strategy = "round_robin"
3
+
4
+ [[signers]]
5
+ name = "main_signer"
6
+ type = "memory"
7
+ private_key_env = "KORA_PRIVATE_KEY"
8
+ weight = 1
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx src/facilitator.ts",
8
+ "start": "tsx src/facilitator.ts",
9
+ "typecheck": "tsc --noEmit",
10
+ "kora": "RPC_URL=${SOLANA_RPC_URL:-https://api.devnet.solana.com} kora --config kora/kora.toml rpc start --signers-config kora/signers.toml --port ${KORA_PORT:-8080}"
11
+ },
12
+ "dependencies": {
13
+ "@solana/kora": "^0.2.1",
14
+ "@x402/core": "^2.10.0",
15
+ "@x402/svm": "^2.10.0",
16
+ "dotenv": "^17.2.3",
17
+ "express": "^5.2.1"
18
+ },
19
+ "devDependencies": {
20
+ "@types/express": "^5.0.0",
21
+ "@types/node": "^24.5.2",
22
+ "tsx": "^4.21.0",
23
+ "typescript": "^5.9.0"
24
+ }
25
+ }
@@ -0,0 +1,163 @@
1
+ import { config } from 'dotenv'
2
+ import express from 'express'
3
+ import { KoraClient } from '@solana/kora'
4
+ import { SOLANA_DEVNET_CAIP2 } from '@x402/svm'
5
+ import {
6
+ getMetrics,
7
+ listSellers,
8
+ recordSettlement,
9
+ requireSellerCanPayFee,
10
+ upsertSeller,
11
+ microsToDollars,
12
+ } from './ledger'
13
+ import { startHeartbeat } from './heartbeat'
14
+
15
+ config()
16
+
17
+ const app = express()
18
+ app.use(express.json({ limit: '2mb' }))
19
+
20
+ const PORT = Number(process.env.FACILITATOR_PORT ?? 3000)
21
+ const NETWORK = process.env.NETWORK ?? SOLANA_DEVNET_CAIP2
22
+ const KORA_RPC_URL = process.env.KORA_RPC_URL ?? 'http://localhost:8080/'
23
+ const KORA_API_KEY = process.env.KORA_API_KEY ?? ''
24
+ const DEFAULT_FEE_BPS = Number(process.env.FACILITATOR_FEE_BPS ?? 500)
25
+
26
+ function kora() {
27
+ return new KoraClient({ rpcUrl: KORA_RPC_URL, apiKey: KORA_API_KEY })
28
+ }
29
+
30
+ function paymentAmountMicros(paymentRequirements: any): number {
31
+ const raw = paymentRequirements?.maxAmountRequired ?? paymentRequirements?.amount
32
+ const amount = typeof raw === 'bigint' ? Number(raw) : Number(String(raw ?? '0'))
33
+ if (!Number.isFinite(amount) || amount <= 0) throw new Error('Invalid payment amount')
34
+ return amount
35
+ }
36
+
37
+ function paymentPayTo(paymentRequirements: any): string {
38
+ const payTo = String(paymentRequirements?.payTo ?? '')
39
+ if (!payTo) throw new Error('Missing payment recipient')
40
+ return payTo
41
+ }
42
+
43
+ function paymentTransaction(paymentPayload: any): string {
44
+ const transaction = paymentPayload?.payload?.transaction
45
+ if (!transaction || typeof transaction !== 'string') throw new Error('Missing Solana transaction in payment payload')
46
+ return transaction
47
+ }
48
+
49
+ function assertNetwork(paymentRequirements: any) {
50
+ const network = String(paymentRequirements?.network ?? '')
51
+ if (!network.startsWith('solana:')) throw new Error('Invalid network')
52
+ }
53
+
54
+ function requireAdmin(req: express.Request, res: express.Response): boolean {
55
+ const token = process.env.ADMIN_TOKEN
56
+ if (!token || token === 'change-me') {
57
+ res.status(500).json({ error: 'ADMIN_TOKEN must be set before using admin endpoints' })
58
+ return false
59
+ }
60
+ if (req.header('x-admin-token') !== token) {
61
+ res.status(401).json({ error: 'Unauthorized' })
62
+ return false
63
+ }
64
+ return true
65
+ }
66
+
67
+ const defaultSeller = process.env.DEFAULT_SELLER_PAY_TO
68
+ if (defaultSeller) {
69
+ upsertSeller({
70
+ payTo: defaultSeller,
71
+ label: 'default',
72
+ balanceUsd: Number(process.env.DEFAULT_SELLER_BALANCE_USD ?? 0),
73
+ })
74
+ }
75
+
76
+ app.get('/health', (_req, res) => {
77
+ res.json({ ok: true, network: NETWORK, feeBps: DEFAULT_FEE_BPS, ...getMetrics() })
78
+ })
79
+
80
+ app.get('/supported', async (_req, res) => {
81
+ try {
82
+ const { signer_address } = await kora().getPayerSigner()
83
+ res.json({
84
+ kinds: [{
85
+ x402Version: 2,
86
+ scheme: 'exact',
87
+ network: NETWORK,
88
+ extra: { feePayer: signer_address },
89
+ }],
90
+ })
91
+ } catch (err) {
92
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) })
93
+ }
94
+ })
95
+
96
+ app.post('/verify', async (req, res) => {
97
+ try {
98
+ const { paymentPayload, paymentRequirements } = req.body
99
+ assertNetwork(paymentRequirements)
100
+ const payTo = paymentPayTo(paymentRequirements)
101
+ const amountMicros = paymentAmountMicros(paymentRequirements)
102
+ requireSellerCanPayFee(payTo, amountMicros, DEFAULT_FEE_BPS)
103
+ await kora().signTransaction({ transaction: paymentTransaction(paymentPayload) })
104
+ res.json({ isValid: true })
105
+ } catch (err) {
106
+ res.status(400).json({
107
+ isValid: false,
108
+ invalidReason: err instanceof Error ? err.message : 'Kora validation failed',
109
+ })
110
+ }
111
+ })
112
+
113
+ app.post('/settle', async (req, res) => {
114
+ try {
115
+ const { paymentPayload, paymentRequirements } = req.body
116
+ assertNetwork(paymentRequirements)
117
+ const payTo = paymentPayTo(paymentRequirements)
118
+ const amountMicros = paymentAmountMicros(paymentRequirements)
119
+ const { feeMicros } = requireSellerCanPayFee(payTo, amountMicros, DEFAULT_FEE_BPS)
120
+ const { signature } = await kora().signAndSendTransaction({
121
+ transaction: paymentTransaction(paymentPayload),
122
+ })
123
+ recordSettlement({ payTo, signature, amountMicros, feeMicros })
124
+ res.json({ transaction: signature, success: true, network: NETWORK })
125
+ } catch (err) {
126
+ res.status(400).json({
127
+ transaction: '',
128
+ success: false,
129
+ network: NETWORK,
130
+ errorReason: err instanceof Error ? err.message : 'Kora settlement failed',
131
+ })
132
+ }
133
+ })
134
+
135
+ app.get('/admin/sellers', (req, res) => {
136
+ if (!requireAdmin(req, res)) return
137
+ res.json(listSellers().map((seller: any) => ({
138
+ ...seller,
139
+ balanceUsd: microsToDollars(seller.balanceMicros),
140
+ })))
141
+ })
142
+
143
+ app.post('/admin/sellers', (req, res) => {
144
+ if (!requireAdmin(req, res)) return
145
+ const payTo = String(req.body?.payTo ?? '')
146
+ if (!payTo) return res.status(400).json({ error: 'payTo is required' })
147
+ const seller = upsertSeller({
148
+ payTo,
149
+ label: req.body?.label ? String(req.body.label) : undefined,
150
+ topUpUsd: req.body?.topUpUsd === undefined ? undefined : Number(req.body.topUpUsd),
151
+ balanceUsd: req.body?.balanceUsd === undefined ? undefined : Number(req.body.balanceUsd),
152
+ feeBps: req.body?.feeBps === undefined ? undefined : Number(req.body.feeBps),
153
+ active: req.body?.active === undefined ? undefined : Boolean(req.body.active),
154
+ })
155
+ res.json({ ...seller, balanceUsd: microsToDollars(seller.balanceMicros) })
156
+ })
157
+
158
+ startHeartbeat()
159
+
160
+ app.listen(PORT, () => {
161
+ console.log(`Agentis facilitator listening at http://localhost:${PORT}`)
162
+ console.log(`Kora RPC: ${KORA_RPC_URL}`)
163
+ })
@@ -0,0 +1,36 @@
1
+ import { getMetrics } from './ledger'
2
+
3
+ export function startHeartbeat() {
4
+ const apiUrl = process.env.AGENTIS_API_URL
5
+ const id = process.env.AGENTIS_FACILITATOR_ID
6
+ const secret = process.env.AGENTIS_HEARTBEAT_SECRET
7
+ if (!apiUrl || !id || !secret) {
8
+ console.warn('[heartbeat] disabled: missing Agentis heartbeat env')
9
+ return
10
+ }
11
+
12
+ const send = async () => {
13
+ try {
14
+ const metrics = getMetrics()
15
+ await fetch(`${apiUrl}/facilitators/${id}/heartbeat`, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'content-type': 'application/json',
19
+ 'x-agentis-heartbeat-secret': secret,
20
+ },
21
+ body: JSON.stringify({
22
+ publicUrl: process.env.AGENTIS_PUBLIC_URL || null,
23
+ version: 'agentis-facilitator/0.1.0',
24
+ supported: ['x402', process.env.NETWORK ?? 'solana-devnet'],
25
+ feeBps: Number(process.env.FACILITATOR_FEE_BPS ?? 500),
26
+ ...metrics,
27
+ }),
28
+ })
29
+ } catch (err) {
30
+ console.warn('[heartbeat] failed', err instanceof Error ? err.message : err)
31
+ }
32
+ }
33
+
34
+ void send()
35
+ setInterval(send, 30_000).unref()
36
+ }
@@ -0,0 +1,141 @@
1
+ import { DatabaseSync } from 'node:sqlite'
2
+ import { mkdirSync } from 'fs'
3
+ import { dirname } from 'path'
4
+
5
+ export type Seller = {
6
+ payTo: string
7
+ label: string | null
8
+ balanceMicros: number
9
+ feeBps: number | null
10
+ active: number
11
+ }
12
+
13
+ const dbPath = process.env.DATABASE_PATH ?? 'data/facilitator.db'
14
+ mkdirSync(dirname(dbPath), { recursive: true })
15
+
16
+ const db = new DatabaseSync(dbPath)
17
+ db.exec(`
18
+ PRAGMA journal_mode = WAL;
19
+ CREATE TABLE IF NOT EXISTS sellers (
20
+ pay_to TEXT PRIMARY KEY,
21
+ label TEXT,
22
+ balance_micros INTEGER NOT NULL DEFAULT 0,
23
+ fee_bps INTEGER,
24
+ active INTEGER NOT NULL DEFAULT 1,
25
+ created_at TEXT NOT NULL,
26
+ updated_at TEXT NOT NULL
27
+ );
28
+ CREATE TABLE IF NOT EXISTS settlements (
29
+ id TEXT PRIMARY KEY,
30
+ pay_to TEXT NOT NULL,
31
+ transaction_signature TEXT NOT NULL,
32
+ amount_micros INTEGER NOT NULL,
33
+ fee_micros INTEGER NOT NULL,
34
+ created_at TEXT NOT NULL
35
+ );
36
+ `)
37
+
38
+ export function dollarsToMicros(value: number): number {
39
+ return Math.round(value * 1_000_000)
40
+ }
41
+
42
+ export function microsToDollars(value: number): number {
43
+ return value / 1_000_000
44
+ }
45
+
46
+ export function getSeller(payTo: string): Seller | null {
47
+ const row = db.prepare(`
48
+ SELECT pay_to as payTo, label, balance_micros as balanceMicros, fee_bps as feeBps, active
49
+ FROM sellers
50
+ WHERE pay_to = ?
51
+ `).get(payTo) as Seller | null
52
+ return row
53
+ }
54
+
55
+ export function upsertSeller(input: { payTo: string; label?: string | null; topUpUsd?: number; balanceUsd?: number; feeBps?: number | null; active?: boolean }): Seller {
56
+ const current = getSeller(input.payTo)
57
+ const now = new Date().toISOString()
58
+ const balanceMicros = input.balanceUsd === undefined
59
+ ? (current?.balanceMicros ?? 0) + dollarsToMicros(input.topUpUsd ?? 0)
60
+ : dollarsToMicros(input.balanceUsd)
61
+
62
+ if (current) {
63
+ db.prepare(`
64
+ UPDATE sellers
65
+ SET label = ?, balance_micros = ?, fee_bps = ?, active = ?, updated_at = ?
66
+ WHERE pay_to = ?
67
+ `).run(
68
+ input.label ?? current.label,
69
+ balanceMicros,
70
+ input.feeBps ?? current.feeBps,
71
+ input.active === undefined ? current.active : Number(input.active),
72
+ now,
73
+ input.payTo,
74
+ )
75
+ } else {
76
+ db.prepare(`
77
+ INSERT INTO sellers (pay_to, label, balance_micros, fee_bps, active, created_at, updated_at)
78
+ VALUES (?, ?, ?, ?, ?, ?, ?)
79
+ `).run(
80
+ input.payTo,
81
+ input.label ?? null,
82
+ balanceMicros,
83
+ input.feeBps ?? null,
84
+ input.active === undefined ? 1 : Number(input.active),
85
+ now,
86
+ now,
87
+ )
88
+ }
89
+
90
+ return getSeller(input.payTo)!
91
+ }
92
+
93
+ export function requireSellerCanPayFee(payTo: string, amountMicros: number, defaultFeeBps: number): { seller: Seller; feeMicros: number } {
94
+ const seller = getSeller(payTo)
95
+ if (!seller || !seller.active) throw new Error('Seller is not enabled on this facilitator')
96
+ const feeBps = seller.feeBps ?? defaultFeeBps
97
+ const feeMicros = Math.ceil(amountMicros * feeBps / 10_000)
98
+ if (seller.balanceMicros < feeMicros) throw new Error('Seller prepaid facilitator balance is too low')
99
+ return { seller, feeMicros }
100
+ }
101
+
102
+ export function recordSettlement(input: { payTo: string; signature: string; amountMicros: number; feeMicros: number }) {
103
+ const now = new Date().toISOString()
104
+ db.exec('BEGIN IMMEDIATE')
105
+ try {
106
+ db.prepare(`
107
+ UPDATE sellers
108
+ SET balance_micros = balance_micros - ?, updated_at = ?
109
+ WHERE pay_to = ?
110
+ `).run(input.feeMicros, now, input.payTo)
111
+ db.prepare(`
112
+ INSERT INTO settlements (id, pay_to, transaction_signature, amount_micros, fee_micros, created_at)
113
+ VALUES (?, ?, ?, ?, ?, ?)
114
+ `).run(crypto.randomUUID(), input.payTo, input.signature, input.amountMicros, input.feeMicros, now)
115
+ db.exec('COMMIT')
116
+ } catch (err) {
117
+ db.exec('ROLLBACK')
118
+ throw err
119
+ }
120
+ }
121
+
122
+ export function listSellers() {
123
+ return db.prepare(`
124
+ SELECT pay_to as payTo, label, balance_micros as balanceMicros, fee_bps as feeBps, active
125
+ FROM sellers
126
+ ORDER BY updated_at DESC
127
+ `).all()
128
+ }
129
+
130
+ export function getMetrics() {
131
+ const settled = db.prepare(`
132
+ SELECT COUNT(*) as count, COALESCE(SUM(amount_micros), 0) as volumeMicros
133
+ FROM settlements
134
+ `).get() as { count: number; volumeMicros: number }
135
+ const sellers = db.prepare(`SELECT COUNT(*) as count FROM sellers WHERE active = 1`).get() as { count: number }
136
+ return {
137
+ settledCount: settled.count,
138
+ settledVolumeUsd: microsToDollars(settled.volumeMicros),
139
+ sellerCount: sellers.count,
140
+ }
141
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["node"]
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }