aixyz 0.33.0 → 0.35.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/accepts.ts +38 -23
- package/app/index.ts +44 -8
- package/app/payment/payment.ts +149 -12
- package/app/plugin.ts +26 -4
- package/app/plugins/a2a.ts +3 -2
- package/app/plugins/index-page/index.ts +3 -2
- package/app/plugins/mcp.ts +85 -18
- package/app/plugins/session/index.ts +193 -0
- package/app/plugins/session/memory.ts +206 -0
- package/app/types.ts +6 -2
- package/docs/api-reference/accepts.mdx +71 -5
- package/docs/api-reference/agent.mdx +1 -1
- package/docs/api-reference/aixyz-server.mdx +3 -0
- package/docs/api-reference/session-plugin.mdx +192 -0
- package/docs/api-reference/tools.mdx +4 -4
- package/docs/getting-started/agent-and-tools.mdx +11 -0
- package/docs/getting-started/payments.mdx +47 -0
- package/docs/getting-started/project-structure.mdx +8 -1
- package/docs/protocols/mcp.mdx +17 -0
- package/docs/templates/advanced/x402-sessions.mdx +99 -0
- package/docs/templates/overview.mdx +3 -0
- package/package.json +5 -4
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { BasePlugin, type RegisterContext, type InitializeContext } from "../../plugin";
|
|
3
|
+
import type { PaymentGateway } from "../../payment/payment";
|
|
4
|
+
import { InMemorySessionStore } from "./memory";
|
|
5
|
+
|
|
6
|
+
export { InMemorySessionStore, type InMemorySessionStoreOptions } from "./memory";
|
|
7
|
+
|
|
8
|
+
// ── Storage Interface ────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/** Options for {@link SessionStore.set} and {@link SessionStore.setMany}. */
|
|
11
|
+
export interface SetOptions {
|
|
12
|
+
/** Per-key TTL in milliseconds. Overrides the store-level default when supported. */
|
|
13
|
+
ttlMs?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Options for {@link SessionStore.list}. */
|
|
17
|
+
export interface ListOptions {
|
|
18
|
+
/** Only return keys starting with this prefix. */
|
|
19
|
+
prefix?: string;
|
|
20
|
+
/** Opaque cursor from a previous `list()` call for pagination. */
|
|
21
|
+
cursor?: string;
|
|
22
|
+
/** Maximum number of entries to return. The store decides its own default when omitted. */
|
|
23
|
+
limit?: number;
|
|
24
|
+
/** If true, values are omitted (all values will be empty strings). */
|
|
25
|
+
keysOnly?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Return type for {@link SessionStore.list}. */
|
|
29
|
+
export interface ListResult {
|
|
30
|
+
entries: Record<string, string>;
|
|
31
|
+
/** If present, more results are available — pass to the next `list()` call. */
|
|
32
|
+
cursor?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pluggable session storage backend.
|
|
37
|
+
* All methods are async to support external stores (Redis, DB, KV, etc.).
|
|
38
|
+
* Operations are scoped by payer address — the x402 signer for the request.
|
|
39
|
+
*/
|
|
40
|
+
export interface SessionStore {
|
|
41
|
+
get(payer: string, key: string): Promise<string | undefined>;
|
|
42
|
+
set(payer: string, key: string, value: string, options?: SetOptions): Promise<void>;
|
|
43
|
+
delete(payer: string, key: string): Promise<boolean>;
|
|
44
|
+
list(payer: string, options?: ListOptions): Promise<ListResult>;
|
|
45
|
+
|
|
46
|
+
/** Retrieve multiple keys in a single call. */
|
|
47
|
+
getMany?(payer: string, keys: string[]): Promise<Record<string, string | undefined>>;
|
|
48
|
+
/** Set multiple keys in a single call. */
|
|
49
|
+
setMany?(payer: string, entries: Record<string, string>, options?: SetOptions): Promise<void>;
|
|
50
|
+
/** Delete multiple keys in a single call. Returns the number of keys actually removed. */
|
|
51
|
+
deleteMany?(payer: string, keys: string[]): Promise<number>;
|
|
52
|
+
|
|
53
|
+
/** Release resources held by the store (connections, timers, etc.). */
|
|
54
|
+
close?(): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Session Handle ───────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Payer-scoped session handle. All operations target the current
|
|
61
|
+
* x402 payer without requiring the caller to pass an address.
|
|
62
|
+
*/
|
|
63
|
+
export interface Session {
|
|
64
|
+
readonly payer: string;
|
|
65
|
+
get(key: string): Promise<string | undefined>;
|
|
66
|
+
set(key: string, value: string, options?: SetOptions): Promise<void>;
|
|
67
|
+
delete(key: string): Promise<boolean>;
|
|
68
|
+
list(options?: ListOptions): Promise<ListResult>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createSession(payer: string, store: SessionStore): Session {
|
|
72
|
+
// Normalize the payer address to lowercase so sessions are keyed consistently
|
|
73
|
+
// regardless of whether the x402 verifier returns a checksummed (EIP-55) or
|
|
74
|
+
// lowercase address. The original (possibly checksummed) address is still
|
|
75
|
+
// exposed as `session.payer` for display purposes.
|
|
76
|
+
const storedPayer = payer.toLowerCase();
|
|
77
|
+
return {
|
|
78
|
+
payer,
|
|
79
|
+
get: (key) => store.get(storedPayer, key),
|
|
80
|
+
set: (key, value, options) => store.set(storedPayer, key, value, options),
|
|
81
|
+
delete: (key) => store.delete(storedPayer, key),
|
|
82
|
+
list: (options) => store.list(storedPayer, options),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── AsyncLocalStorage API ────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const sessionStorage = new AsyncLocalStorage<Session | undefined>();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the current payer-scoped session.
|
|
92
|
+
* Returns `undefined` if no x402 payment was made for this request.
|
|
93
|
+
*/
|
|
94
|
+
export function getSession(): Session | undefined {
|
|
95
|
+
return sessionStorage.getStore();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the current x402 payer address.
|
|
100
|
+
* Shorthand for `getSession()?.payer`.
|
|
101
|
+
*/
|
|
102
|
+
export function getPayer(): string | undefined {
|
|
103
|
+
return sessionStorage.getStore()?.payer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Plugin ───────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export interface SessionPluginOptions {
|
|
109
|
+
/** Custom session store. Defaults to {@link InMemorySessionStore}. */
|
|
110
|
+
store?: SessionStore;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Session plugin for aixyz. Provides payer-scoped key-value storage
|
|
115
|
+
* gated by x402 payment identity.
|
|
116
|
+
*
|
|
117
|
+
* Register **before** other plugins so the session middleware runs first:
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* await app.withPlugin(new SessionPlugin());
|
|
121
|
+
* await app.withPlugin(new A2APlugin([...]));
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* Tools access the session via {@link getSession}:
|
|
125
|
+
*
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { getSession } from "aixyz/app/plugins/session";
|
|
128
|
+
*
|
|
129
|
+
* const session = getSession();
|
|
130
|
+
* await session?.set("key", "value");
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export class SessionPlugin extends BasePlugin {
|
|
134
|
+
readonly name = "session";
|
|
135
|
+
readonly store: SessionStore;
|
|
136
|
+
private payment?: PaymentGateway;
|
|
137
|
+
|
|
138
|
+
constructor(options?: SessionPluginOptions) {
|
|
139
|
+
super();
|
|
140
|
+
this.store = options?.store ?? new InMemorySessionStore();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
register(ctx: RegisterContext): void {
|
|
144
|
+
ctx.use(async (request, next) => {
|
|
145
|
+
const payer = this.payment?.getPayer(request);
|
|
146
|
+
if (payer) {
|
|
147
|
+
const session = createSession(payer, this.store);
|
|
148
|
+
return sessionStorage.run(session, next);
|
|
149
|
+
}
|
|
150
|
+
// Explicitly clear any inherited ALS context so that an unpaid route
|
|
151
|
+
// nested inside a paid handler's app.fetch() doesn't leak the outer session.
|
|
152
|
+
return sessionStorage.run(undefined, next);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
initialize(ctx: InitializeContext): void {
|
|
157
|
+
this.payment = ctx.payment;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run a function within a session context for the given payer.
|
|
162
|
+
* Used by other plugins (e.g., MCPPlugin) to set session context
|
|
163
|
+
* for tool execution when payment is handled at the protocol level.
|
|
164
|
+
*
|
|
165
|
+
* **Security note:** This method bypasses HTTP-level payment verification.
|
|
166
|
+
* Only call it with a payer address that has already been authenticated by a
|
|
167
|
+
* trusted payment mechanism (e.g., the `@x402/mcp` payment wrapper).
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
runWithPayer<T>(payer: string, fn: () => T): T {
|
|
171
|
+
const session = createSession(payer, this.store);
|
|
172
|
+
return sessionStorage.run(session, fn);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Identity helper that provides full type inference for a {@link SessionStore}.
|
|
178
|
+
* Use in `app/session.ts` to get type-safe autocompletion:
|
|
179
|
+
*
|
|
180
|
+
* ```ts
|
|
181
|
+
* import { defineSessionStore } from "aixyz/app/plugins/session";
|
|
182
|
+
*
|
|
183
|
+
* export default defineSessionStore({
|
|
184
|
+
* async get(payer, key) { ... },
|
|
185
|
+
* async set(payer, key, value) { ... },
|
|
186
|
+
* async delete(payer, key) { ... },
|
|
187
|
+
* async list(payer) { ... },
|
|
188
|
+
* });
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export function defineSessionStore(store: SessionStore): SessionStore {
|
|
192
|
+
return store;
|
|
193
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { SessionStore, SetOptions, ListOptions, ListResult } from "./index";
|
|
2
|
+
|
|
3
|
+
// ── Types ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface InMemorySessionStoreOptions {
|
|
6
|
+
/** Maximum number of entries across all payers. Default: 10 000. */
|
|
7
|
+
maxEntries?: number;
|
|
8
|
+
/** Time-to-live in milliseconds (sliding window). 0 = no expiry. Default: 3 600 000 (1 h). */
|
|
9
|
+
ttlMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Entry {
|
|
13
|
+
value: string;
|
|
14
|
+
payer: string;
|
|
15
|
+
key: string;
|
|
16
|
+
expiresAt: number; // 0 = never expires
|
|
17
|
+
ttlMs: number; // effective TTL used when refreshing on get(); 0 = no expiry
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Implementation ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Production-ready in-memory {@link SessionStore} with LRU eviction and
|
|
24
|
+
* optional TTL.
|
|
25
|
+
*
|
|
26
|
+
* - **LRU eviction** — backed by `Map` insertion-order. When `maxEntries`
|
|
27
|
+
* is reached, the least-recently-used entry is evicted.
|
|
28
|
+
* - **Sliding-window TTL** — `get()` refreshes the expiry timer.
|
|
29
|
+
* Expired entries are lazily removed on read; no background timers.
|
|
30
|
+
* - **OOM-safe** — `maxEntries` is a hard cap. Memory usage is bounded
|
|
31
|
+
* regardless of TTL or access patterns.
|
|
32
|
+
*/
|
|
33
|
+
export class InMemorySessionStore implements SessionStore {
|
|
34
|
+
private readonly maxEntries: number;
|
|
35
|
+
private readonly ttlMs: number;
|
|
36
|
+
|
|
37
|
+
/** Flat LRU map. Key = `${payer}:${key}`. Insertion order = access order. */
|
|
38
|
+
private readonly entries = new Map<string, Entry>();
|
|
39
|
+
/** Secondary index: payer → set of composite keys (for efficient `list`). */
|
|
40
|
+
private readonly payerIndex = new Map<string, Set<string>>();
|
|
41
|
+
|
|
42
|
+
constructor(options?: InMemorySessionStoreOptions) {
|
|
43
|
+
this.maxEntries = options?.maxEntries ?? 10_000;
|
|
44
|
+
this.ttlMs = options?.ttlMs ?? 3_600_000;
|
|
45
|
+
if (this.maxEntries < 1) throw new Error("maxEntries must be >= 1");
|
|
46
|
+
if (this.ttlMs < 0) throw new Error("ttlMs must be >= 0");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async get(payer: string, key: string): Promise<string | undefined> {
|
|
50
|
+
const ck = compositeKey(payer, key);
|
|
51
|
+
const entry = this.entries.get(ck);
|
|
52
|
+
if (!entry) return undefined;
|
|
53
|
+
|
|
54
|
+
if (this.isExpired(entry)) {
|
|
55
|
+
this.removeEntry(ck, entry);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// LRU touch: move to end + refresh TTL using the entry's own TTL
|
|
60
|
+
this.entries.delete(ck);
|
|
61
|
+
entry.expiresAt = this.expiryFor(entry.ttlMs);
|
|
62
|
+
this.entries.set(ck, entry);
|
|
63
|
+
|
|
64
|
+
return entry.value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async set(payer: string, key: string, value: string, options?: SetOptions): Promise<void> {
|
|
68
|
+
const ck = compositeKey(payer, key);
|
|
69
|
+
|
|
70
|
+
// If overwriting, remove first so re-insert lands at the end.
|
|
71
|
+
const existing = this.entries.get(ck);
|
|
72
|
+
if (existing) {
|
|
73
|
+
this.entries.delete(ck);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Evict LRU entry if at capacity.
|
|
77
|
+
if (this.entries.size >= this.maxEntries) {
|
|
78
|
+
const oldest = this.entries.keys().next();
|
|
79
|
+
if (!oldest.done) {
|
|
80
|
+
const oldestEntry = this.entries.get(oldest.value)!;
|
|
81
|
+
this.removeEntry(oldest.value, oldestEntry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const effectiveTtl = options?.ttlMs ?? this.ttlMs;
|
|
86
|
+
const entry: Entry = { value, payer, key, expiresAt: this.expiryFor(effectiveTtl), ttlMs: effectiveTtl };
|
|
87
|
+
this.entries.set(ck, entry);
|
|
88
|
+
|
|
89
|
+
// Update payer index.
|
|
90
|
+
let idx = this.payerIndex.get(payer);
|
|
91
|
+
if (!idx) {
|
|
92
|
+
idx = new Set();
|
|
93
|
+
this.payerIndex.set(payer, idx);
|
|
94
|
+
}
|
|
95
|
+
idx.add(ck);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async delete(payer: string, key: string): Promise<boolean> {
|
|
99
|
+
const ck = compositeKey(payer, key);
|
|
100
|
+
const entry = this.entries.get(ck);
|
|
101
|
+
if (!entry) return false;
|
|
102
|
+
this.removeEntry(ck, entry);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async list(payer: string, options?: ListOptions): Promise<ListResult> {
|
|
107
|
+
const idx = this.payerIndex.get(payer);
|
|
108
|
+
if (!idx) return { entries: {} };
|
|
109
|
+
|
|
110
|
+
const prefix = options?.prefix;
|
|
111
|
+
const limit = options?.limit && options.limit > 0 ? options.limit : Infinity;
|
|
112
|
+
const keysOnly = options?.keysOnly ?? false;
|
|
113
|
+
const cursor = options?.cursor;
|
|
114
|
+
|
|
115
|
+
const result: Record<string, string> = {};
|
|
116
|
+
let count = 0;
|
|
117
|
+
let pastCursor = !cursor;
|
|
118
|
+
let lastCk: string | undefined;
|
|
119
|
+
let hasMore = false;
|
|
120
|
+
|
|
121
|
+
for (const ck of idx) {
|
|
122
|
+
const entry = this.entries.get(ck);
|
|
123
|
+
if (!entry) {
|
|
124
|
+
idx.delete(ck);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (this.isExpired(entry)) {
|
|
128
|
+
this.removeEntry(ck, entry);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Skip until we pass the cursor position.
|
|
133
|
+
if (!pastCursor) {
|
|
134
|
+
if (ck === cursor) pastCursor = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Apply prefix filter.
|
|
139
|
+
if (prefix && !entry.key.startsWith(prefix)) continue;
|
|
140
|
+
|
|
141
|
+
if (count >= limit) {
|
|
142
|
+
hasMore = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
result[entry.key] = keysOnly ? "" : entry.value;
|
|
147
|
+
lastCk = ck;
|
|
148
|
+
count++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (idx.size === 0) this.payerIndex.delete(payer);
|
|
152
|
+
return { entries: result, cursor: hasMore ? lastCk : undefined };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getMany(payer: string, keys: string[]): Promise<Record<string, string | undefined>> {
|
|
156
|
+
const result: Record<string, string | undefined> = {};
|
|
157
|
+
for (const key of keys) {
|
|
158
|
+
result[key] = await this.get(payer, key);
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async setMany(payer: string, entries: Record<string, string>, options?: SetOptions): Promise<void> {
|
|
164
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
165
|
+
await this.set(payer, key, value, options);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteMany(payer: string, keys: string[]): Promise<number> {
|
|
170
|
+
let count = 0;
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
if (await this.delete(payer, key)) count++;
|
|
173
|
+
}
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async close(): Promise<void> {
|
|
178
|
+
this.entries.clear();
|
|
179
|
+
this.payerIndex.clear();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
private removeEntry(ck: string, entry: Entry): void {
|
|
185
|
+
this.entries.delete(ck);
|
|
186
|
+
const idx = this.payerIndex.get(entry.payer);
|
|
187
|
+
if (idx) {
|
|
188
|
+
idx.delete(ck);
|
|
189
|
+
if (idx.size === 0) this.payerIndex.delete(entry.payer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private isExpired(entry: Entry): boolean {
|
|
194
|
+
return entry.expiresAt > 0 && Date.now() > entry.expiresAt;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private expiryFor(ttlMs: number): number {
|
|
198
|
+
return ttlMs > 0 ? Date.now() + ttlMs : 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
function compositeKey(payer: string, key: string): string {
|
|
205
|
+
return `${payer}:${key}`;
|
|
206
|
+
}
|
package/app/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AcceptsX402 } from "../accepts";
|
|
1
|
+
import type { Accepts, AcceptsX402, AcceptsX402Multi } from "../accepts";
|
|
2
2
|
|
|
3
3
|
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
|
4
4
|
|
|
@@ -6,9 +6,13 @@ export type RouteHandler = (request: Request) => Response | Promise<Response>;
|
|
|
6
6
|
|
|
7
7
|
export type Middleware = (request: Request, next: () => Promise<Response>) => Response | Promise<Response>;
|
|
8
8
|
|
|
9
|
+
export interface RouteOptions {
|
|
10
|
+
payment?: Accepts;
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export interface RouteEntry {
|
|
10
14
|
method: HttpMethod;
|
|
11
15
|
path: string;
|
|
12
16
|
handler: RouteHandler;
|
|
13
|
-
payment?: AcceptsX402;
|
|
17
|
+
payment?: AcceptsX402 | AcceptsX402Multi;
|
|
14
18
|
}
|
|
@@ -6,18 +6,18 @@ description: "Payment configuration types and facilitator client for x402 gating
|
|
|
6
6
|
Types and utilities for configuring x402 payment gating on agent and tool endpoints.
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
|
-
import type { Accepts, AcceptsX402, AcceptsFree } from "aixyz/accepts";
|
|
10
|
-
import { HTTPFacilitatorClient, facilitator } from "aixyz/accepts";
|
|
9
|
+
import type { Accepts, AcceptsX402, AcceptsX402Entry, AcceptsX402Multi, AcceptsFree } from "aixyz/accepts";
|
|
10
|
+
import { HTTPFacilitatorClient, facilitator, normalizeAcceptsX402, isAcceptsPaid } from "aixyz/accepts";
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Types
|
|
14
14
|
|
|
15
15
|
### `Accepts`
|
|
16
16
|
|
|
17
|
-
Union type for payment configuration:
|
|
17
|
+
Union type for payment configuration. Supports a single payment option, free access, or an array of payment options for multi-network support:
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
type Accepts = AcceptsX402 | AcceptsFree;
|
|
20
|
+
type Accepts = AcceptsX402 | AcceptsFree | AcceptsX402Multi;
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### `AcceptsX402`
|
|
@@ -40,6 +40,34 @@ type AcceptsX402 = {
|
|
|
40
40
|
| `network` | `string` | No | CAIP-2 chain ID, overrides `x402.network` from config |
|
|
41
41
|
| `payTo` | `string` | No | EVM address to receive payment, overrides `x402.payTo` config |
|
|
42
42
|
|
|
43
|
+
### `AcceptsX402Entry`
|
|
44
|
+
|
|
45
|
+
A single payment option within a multi-accepts array. Same as `AcceptsX402` but `network` is **required** — the server needs an explicit network to register each payment scheme:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
type AcceptsX402Entry = {
|
|
49
|
+
scheme: "exact";
|
|
50
|
+
price: string;
|
|
51
|
+
network: string; // required
|
|
52
|
+
payTo?: string;
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Field | Type | Required | Description |
|
|
57
|
+
| --------- | -------- | -------- | ------------------------------------------------------------- |
|
|
58
|
+
| `scheme` | `string` | Yes | Must be `"exact"` |
|
|
59
|
+
| `price` | `string` | Yes | USD price string (e.g. `"$0.005"`) |
|
|
60
|
+
| `network` | `string` | Yes | CAIP-2 chain ID — required for multi-accepts |
|
|
61
|
+
| `payTo` | `string` | No | EVM address to receive payment, overrides `x402.payTo` config |
|
|
62
|
+
|
|
63
|
+
### `AcceptsX402Multi`
|
|
64
|
+
|
|
65
|
+
An array of payment entries, enabling multi-network support. Must contain at least one entry:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
type AcceptsX402Multi = AcceptsX402Entry[];
|
|
69
|
+
```
|
|
70
|
+
|
|
43
71
|
### `AcceptsFree`
|
|
44
72
|
|
|
45
73
|
No payment required:
|
|
@@ -52,6 +80,22 @@ type AcceptsFree = {
|
|
|
52
80
|
|
|
53
81
|
## Exports
|
|
54
82
|
|
|
83
|
+
### `normalizeAcceptsX402`
|
|
84
|
+
|
|
85
|
+
Converts a single or multi accepts value into a uniform array:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
function normalizeAcceptsX402(accepts: AcceptsX402 | AcceptsX402Multi): AcceptsX402[];
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `isAcceptsPaid`
|
|
92
|
+
|
|
93
|
+
Type guard that returns `true` if the accepts config requires payment (i.e., is not `{ scheme: "free" }`):
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
function isAcceptsPaid(accepts: Accepts): accepts is AcceptsX402 | AcceptsX402Multi;
|
|
97
|
+
```
|
|
98
|
+
|
|
55
99
|
### `HTTPFacilitatorClient`
|
|
56
100
|
|
|
57
101
|
Client for communicating with an x402 facilitator service. Re-exported from `@x402/core/server`.
|
|
@@ -74,12 +118,19 @@ import { facilitator } from "aixyz/accepts";
|
|
|
74
118
|
|
|
75
119
|
### `AcceptsScheme`
|
|
76
120
|
|
|
77
|
-
Zod schema for validating `Accepts` objects at runtime:
|
|
121
|
+
Zod schema for validating `Accepts` objects at runtime. Accepts a single object or an array of payment entries:
|
|
78
122
|
|
|
79
123
|
```typescript
|
|
80
124
|
import { AcceptsScheme } from "aixyz/accepts";
|
|
81
125
|
|
|
126
|
+
// Single accepts
|
|
82
127
|
AcceptsScheme.parse({ scheme: "exact", price: "$0.005" });
|
|
128
|
+
|
|
129
|
+
// Multiple accepts
|
|
130
|
+
AcceptsScheme.parse([
|
|
131
|
+
{ scheme: "exact", price: "$0.005", network: "eip155:8453" },
|
|
132
|
+
{ scheme: "exact", price: "$0.005", network: "eip155:84532" },
|
|
133
|
+
]);
|
|
83
134
|
```
|
|
84
135
|
|
|
85
136
|
## Usage
|
|
@@ -103,6 +154,21 @@ export const accepts: Accepts = {
|
|
|
103
154
|
};
|
|
104
155
|
```
|
|
105
156
|
|
|
157
|
+
### Multiple payment options
|
|
158
|
+
|
|
159
|
+
Accept payment across multiple networks by passing an array. Each entry requires an explicit `network`:
|
|
160
|
+
|
|
161
|
+
```typescript title="app/agent.ts"
|
|
162
|
+
import type { Accepts } from "aixyz/accepts";
|
|
163
|
+
|
|
164
|
+
export const accepts: Accepts = [
|
|
165
|
+
{ scheme: "exact", price: "$0.005", network: "eip155:8453" },
|
|
166
|
+
{ scheme: "exact", price: "$0.005", network: "eip155:84532" },
|
|
167
|
+
];
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
All referenced networks are automatically registered with the payment gateway during initialization — no manual setup required.
|
|
171
|
+
|
|
106
172
|
### Custom facilitator
|
|
107
173
|
|
|
108
174
|
Create `app/accepts.ts` to override the default facilitator:
|
|
@@ -38,7 +38,7 @@ export default new ToolLoopAgent({
|
|
|
38
38
|
| `accepts` | `Accepts` | No | Payment config — gates the A2A `/agent` route |
|
|
39
39
|
| `capabilities` | `Capabilities` | No | A2A capabilities — controls streaming and push notification support |
|
|
40
40
|
|
|
41
|
-
When `accepts` is exported, the `/agent` endpoint requires x402 payment. Without it, the agent is not registered on the A2A endpoint.
|
|
41
|
+
When `accepts` is exported, the `/agent` endpoint requires x402 payment. Without it, the agent is not registered on the A2A endpoint. `accepts` can also be an array of payment entries for [multi-network support](/getting-started/payments#multiple-payment-options).
|
|
42
42
|
|
|
43
43
|
## Capabilities
|
|
44
44
|
|
|
@@ -91,12 +91,15 @@ await server.initialize();
|
|
|
91
91
|
|
|
92
92
|
```typescript title="app/server.ts"
|
|
93
93
|
import { AixyzApp } from "aixyz/app";
|
|
94
|
+
import { SessionPlugin } from "aixyz/app/plugins/session";
|
|
94
95
|
import { IndexPagePlugin } from "aixyz/app/plugins/index-page";
|
|
95
96
|
import { A2APlugin } from "aixyz/app/plugins/a2a";
|
|
96
97
|
import * as agent from "./agent";
|
|
97
98
|
|
|
98
99
|
const server = new AixyzApp();
|
|
99
100
|
|
|
101
|
+
// SessionPlugin must be registered first so its middleware runs before other plugins
|
|
102
|
+
await server.withPlugin(new SessionPlugin());
|
|
100
103
|
await server.withPlugin(new IndexPagePlugin());
|
|
101
104
|
await server.withPlugin(new A2APlugin([{ exports: agent }]));
|
|
102
105
|
|