aixyz 0.34.0 → 0.36.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.
@@ -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
+ }
@@ -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
 
@@ -0,0 +1,192 @@
1
+ ---
2
+ title: "SessionPlugin"
3
+ description: "Payer-scoped key-value storage gated by x402 payment identity"
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ `SessionPlugin` provides per-payer key-value storage for aixyz agents. Each x402 signer gets an isolated session -- two different payers never see each other's data. Sessions are accessed via `getSession()` using `AsyncLocalStorage`, so tools don't need to thread payer identity through function parameters.
9
+
10
+ SessionPlugin is **always registered** by the build pipeline. To use a custom storage backend, create an `app/session.ts` file.
11
+
12
+ ```typescript
13
+ import { getSession, getPayer } from "aixyz/app/plugins/session";
14
+ ```
15
+
16
+ ## Setup
17
+
18
+ SessionPlugin is auto-registered when `aixyz build` or `aixyz dev` generates the server entrypoint. No manual setup is needed for the default in-memory store.
19
+
20
+ To use a custom store (Redis, database, etc.), create `app/session.ts`:
21
+
22
+ ```typescript title="app/session.ts"
23
+ import { defineSessionStore, InMemorySessionStore } from "aixyz/app/plugins/session";
24
+
25
+ export default defineSessionStore(new InMemorySessionStore());
26
+ ```
27
+
28
+ The build pipeline detects `app/session.ts` and passes it to `SessionPlugin({ store: sessionStore })`. If no `app/session.ts` exists, the default `InMemorySessionStore` is used.
29
+
30
+ ## API
31
+
32
+ ### `getSession()`
33
+
34
+ Returns the current payer-scoped `Session`, or `undefined` if no x402 payment was made for this request.
35
+
36
+ ```typescript
37
+ import { getSession } from "aixyz/app/plugins/session";
38
+
39
+ const session = getSession();
40
+ if (!session) {
41
+ return { error: "No authenticated signer" };
42
+ }
43
+
44
+ await session.set("key", "value");
45
+ const value = await session.get("key");
46
+ ```
47
+
48
+ ### `getPayer()`
49
+
50
+ Shorthand for `getSession()?.payer`. Returns the x402 signer address or `undefined`.
51
+
52
+ ```typescript
53
+ import { getPayer } from "aixyz/app/plugins/session";
54
+
55
+ const payer = getPayer(); // "0x1234..."
56
+ ```
57
+
58
+ ## Session Interface
59
+
60
+ All operations are scoped to the current x402 payer automatically.
61
+
62
+ | Method | Signature | Description |
63
+ | -------- | --------------------------------------------------------------------- | --------------------------------------------- |
64
+ | `payer` | `readonly string` | The x402 signer address |
65
+ | `get` | `(key: string) => Promise<string \| undefined>` | Get a value by key |
66
+ | `set` | `(key: string, value: string, options?: SetOptions) => Promise<void>` | Store a key-value pair |
67
+ | `delete` | `(key: string) => Promise<boolean>` | Delete a key, returns whether it existed |
68
+ | `list` | `(options?: ListOptions) => Promise<ListResult>` | List key-value pairs with optional pagination |
69
+
70
+ ### `SetOptions`
71
+
72
+ | Field | Type | Description |
73
+ | ------- | -------- | -------------------------------------------------------------------- |
74
+ | `ttlMs` | `number` | Per-key TTL in milliseconds. Overrides store default when supported. |
75
+
76
+ ### `ListOptions`
77
+
78
+ | Field | Type | Description |
79
+ | ---------- | --------- | ------------------------------------------------- |
80
+ | `prefix` | `string` | Only return keys starting with this prefix |
81
+ | `cursor` | `string` | Opaque cursor from a previous `list()` call |
82
+ | `limit` | `number` | Maximum number of entries to return |
83
+ | `keysOnly` | `boolean` | If true, values are omitted (all values are `""`) |
84
+
85
+ ### `ListResult`
86
+
87
+ | Field | Type | Description |
88
+ | --------- | ------------------------ | ------------------------------------------------------------------- |
89
+ | `entries` | `Record<string, string>` | The key-value pairs |
90
+ | `cursor` | `string \| undefined` | If present, more results are available. Pass to next `list()` call. |
91
+
92
+ ## InMemorySessionStore
93
+
94
+ The default store. Uses LRU eviction and optional sliding-window TTL.
95
+
96
+ - **LRU eviction** -- when `maxEntries` is reached, the least-recently-used entry is evicted
97
+ - **Sliding-window TTL** -- `get()` refreshes the expiry timer. Expired entries are lazily removed on read.
98
+ - **OOM-safe** -- `maxEntries` is a hard cap across all payers
99
+
100
+ ```typescript
101
+ import { InMemorySessionStore } from "aixyz/app/plugins/session";
102
+
103
+ const store = new InMemorySessionStore({
104
+ maxEntries: 10_000, // default
105
+ ttlMs: 3_600_000, // 1 hour default, 0 to disable
106
+ });
107
+ ```
108
+
109
+ | Option | Type | Default | Description |
110
+ | ------------ | -------- | --------- | ------------------------------------------------ |
111
+ | `maxEntries` | `number` | `10000` | Maximum entries across all payers. Must be >= 1. |
112
+ | `ttlMs` | `number` | `3600000` | Sliding-window TTL in ms. 0 disables expiry. |
113
+
114
+ ## Custom Storage Backend
115
+
116
+ Implement the `SessionStore` interface for Redis, a database, or any KV store:
117
+
118
+ ```typescript title="app/session.ts"
119
+ import { defineSessionStore } from "aixyz/app/plugins/session";
120
+ import type { SessionStore, ListOptions, SetOptions } from "aixyz/app/plugins/session";
121
+
122
+ class RedisSessionStore implements SessionStore {
123
+ async get(payer: string, key: string) {
124
+ return (await redis.get(`${payer}:${key}`)) ?? undefined;
125
+ }
126
+ async set(payer: string, key: string, value: string, options?: SetOptions) {
127
+ if (options?.ttlMs) {
128
+ await redis.set(`${payer}:${key}`, value, "PX", options.ttlMs);
129
+ } else {
130
+ await redis.set(`${payer}:${key}`, value);
131
+ }
132
+ }
133
+ async delete(payer: string, key: string) {
134
+ return (await redis.del(`${payer}:${key}`)) > 0;
135
+ }
136
+ async list(payer: string, options?: ListOptions) {
137
+ // scan for keys matching payer prefix, apply options.prefix/limit/cursor
138
+ return {
139
+ entries: {
140
+ /* ... */
141
+ },
142
+ };
143
+ }
144
+ }
145
+
146
+ export default defineSessionStore(new RedisSessionStore());
147
+ ```
148
+
149
+ ### `SessionStore` Interface
150
+
151
+ **Required methods:**
152
+
153
+ | Method | Signature | Description |
154
+ | -------- | ------------------------------------------------------------------------------------ | ----------------------- |
155
+ | `get` | `(payer: string, key: string) => Promise<string \| undefined>` | Get a value |
156
+ | `set` | `(payer: string, key: string, value: string, options?: SetOptions) => Promise<void>` | Store a value |
157
+ | `delete` | `(payer: string, key: string) => Promise<boolean>` | Delete a value |
158
+ | `list` | `(payer: string, options?: ListOptions) => Promise<ListResult>` | List values for a payer |
159
+
160
+ **Optional methods:**
161
+
162
+ | Method | Signature | Description |
163
+ | ------------ | ----------------------------------------------------------------------------------------- | --------------------------- |
164
+ | `getMany` | `(payer: string, keys: string[]) => Promise<Record<string, string \| undefined>>` | Batch get |
165
+ | `setMany` | `(payer: string, entries: Record<string, string>, options?: SetOptions) => Promise<void>` | Batch set |
166
+ | `deleteMany` | `(payer: string, keys: string[]) => Promise<number>` | Batch delete, returns count |
167
+ | `close` | `() => Promise<void>` | Release connections/timers |
168
+
169
+ ## MCP Integration
170
+
171
+ Sessions work automatically inside MCP tool handlers. The MCP plugin detects the session plugin and wraps tool execution with the payer context from x402 payment verification.
172
+
173
+ No additional configuration is needed.
174
+
175
+ ## Constructor Options
176
+
177
+ ```typescript
178
+ new SessionPlugin(options?: SessionPluginOptions)
179
+ ```
180
+
181
+ | Option | Type | Default | Description |
182
+ | ------- | -------------- | ---------------------- | ---------------------- |
183
+ | `store` | `SessionStore` | `InMemorySessionStore` | Custom storage backend |
184
+
185
+ <CardGroup cols={2}>
186
+ <Card title="x402 Payments" icon="credit-card" href="/protocols/x402">
187
+ Payment protocol that provides payer identity for sessions.
188
+ </Card>
189
+ <Card title="x402 Sessions Template" icon="database" href="/templates/advanced/x402-sessions">
190
+ Working example with session-backed content storage.
191
+ </Card>
192
+ </CardGroup>
@@ -137,3 +137,19 @@ export const facilitator = new HTTPFacilitatorClient({
137
137
  ```
138
138
 
139
139
  See the [BYO Facilitator template](/templates/advanced/with-custom-facilitator) for a working example.
140
+
141
+ ## Payer Identity and Sessions
142
+
143
+ Every verified x402 payment identifies the payer by their wallet address. `SessionPlugin` (auto-registered by the build pipeline) gives each payer isolated key-value storage accessible via `getSession()`:
144
+
145
+ ```typescript
146
+ import { getSession } from "aixyz/app/plugins/session";
147
+
148
+ const session = getSession();
149
+ if (session) {
150
+ await session.set("preference", "dark-mode");
151
+ const payer = session.payer; // "0x1234..."
152
+ }
153
+ ```
154
+
155
+ This works in both A2A agent handlers and MCP tool handlers. See [SessionPlugin](/api-reference/session-plugin) for the full API and custom store configuration.
@@ -35,6 +35,11 @@ An aixyz agent is defined by a small set of files in a standard layout. Run `aix
35
35
  description: "Custom x402 facilitator",
36
36
  badge: { text: "optional override", color: "purple" },
37
37
  },
38
+ {
39
+ name: "session.ts",
40
+ description: "Custom session store",
41
+ badge: { text: "optional override", color: "purple" },
42
+ },
38
43
  {
39
44
  name: "erc-8004.ts",
40
45
  description: "ERC-8004 identity registration",
@@ -126,6 +131,7 @@ An aixyz agent is defined by a small set of files in a standard layout. Run `aix
126
131
  | `app/tools/*.ts` | No | Tool implementations, auto-discovered by the build |
127
132
  | `app/server.ts` | No | [Custom server](/api-reference/aixyz-server) — overrides auto-generation entirely |
128
133
  | `app/accepts.ts` | No | [Custom x402 facilitator](/api-reference/accepts) for payment verification |
134
+ | `app/session.ts` | No | [Custom session store](/api-reference/session-plugin) override for SessionPlugin |
129
135
  | `app/erc-8004.ts` | No | ERC-8004 identity registration and trust config |
130
136
  | `app/agent.test.ts` | No | Agent tests using `bun:test` |
131
137
  | `app/icon.svg` | No | Agent icon served as a static asset |
@@ -141,7 +147,8 @@ The build pipeline scans the `app/` directory to auto-generate a server:
141
147
  2. Imports `app/agent.ts` for the main agent definition (if present)
142
148
  3. Discovers all `.ts` files in `app/agents/` for sub-agents (each gets its own A2A endpoint)
143
149
  4. Discovers all `.ts` files in `app/tools/` (excluding `_` prefixed files)
144
- 5. Wires up A2A (when agents exist), MCP (when tools exist), and x402 endpoints automatically
150
+ 5. Registers `SessionPlugin` (with custom store from `app/session.ts` if present)
151
+ 6. Wires up A2A (when agents exist), MCP (when tools exist), and x402 endpoints automatically
145
152
 
146
153
  The result is a server exposing:
147
154
 
@@ -58,6 +58,20 @@ await server.withPlugin(
58
58
  );
59
59
  ```
60
60
 
61
+ ## Session Integration
62
+
63
+ Paid MCP tools automatically get session context. When a tool is invoked with x402 payment, `getSession()` returns the payer-scoped session inside the tool handler — no additional configuration needed.
64
+
65
+ ```typescript
66
+ import { getSession } from "aixyz/app/plugins/session";
67
+
68
+ // Inside an MCP tool handler:
69
+ const session = getSession();
70
+ await session?.set("key", "value"); // stored per-payer
71
+ ```
72
+
73
+ See [SessionPlugin](/api-reference/session-plugin) for the full API.
74
+
61
75
  ## Payment-Gated Tools
62
76
 
63
77
  Tools with `accepts.scheme === "exact"` require [x402 payment](/protocols/x402) via `@x402/mcp`. The payment wrapper is applied automatically when you provide an `accepts` configuration during registration.
@@ -81,6 +95,9 @@ await server.withPlugin(
81
95
  <Card title="Tools" icon="folder-tree" href="/api-reference/tools">
82
96
  How tools are defined in the app/ directory.
83
97
  </Card>
98
+ <Card title="SessionPlugin" icon="database" href="/api-reference/session-plugin">
99
+ Payer-scoped storage for MCP tools.
100
+ </Card>
84
101
  <Card title="A2A Protocol" icon="diagram-project" href="/protocols/a2a">
85
102
  Agent discovery and communication via A2A.
86
103
  </Card>
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: "x402 Sessions"
3
+ description: "Template demonstrating payer-scoped session storage with x402 payment identity"
4
+ ---
5
+
6
+ <Info>**Source:** [`examples/x402-sessions`](https://github.com/AgentlyHQ/aixyz/tree/main/examples/x402-sessions)</Info>
7
+
8
+ ## Overview
9
+
10
+ This template demonstrates `SessionPlugin` -- payer-scoped key-value storage gated by x402 payment identity. Each x402 signer gets isolated storage that persists across requests. Two different payers never see each other's data.
11
+
12
+ ## Project Structure
13
+
14
+ ```
15
+ x402-sessions/
16
+ ├── aixyz.config.ts # Agent metadata and skills
17
+ ├── app/
18
+ │ ├── session.ts # Custom session store (optional)
19
+ │ ├── agent.ts # Agent definition
20
+ │ ├── accepts.ts # Custom x402 facilitator
21
+ │ └── tools/
22
+ │ ├── put-content.ts # Store content in session
23
+ │ └── get-content.ts # Retrieve content from session
24
+ ├── package.json
25
+ └── vercel.json
26
+ ```
27
+
28
+ ## Session Store
29
+
30
+ SessionPlugin is auto-registered by the build pipeline. To customize the store, create `app/session.ts`:
31
+
32
+ ```typescript title="app/session.ts"
33
+ import { defineSessionStore, InMemorySessionStore } from "aixyz/app/plugins/session";
34
+
35
+ // Use the built-in in-memory store. Replace with Redis, DB, etc. for production.
36
+ export default defineSessionStore(new InMemorySessionStore());
37
+ ```
38
+
39
+ If `app/session.ts` is not present, the default `InMemorySessionStore` is used automatically.
40
+
41
+ ## Using Sessions in Tools
42
+
43
+ Tools access the session via `getSession()` -- no need to pass payer identity manually:
44
+
45
+ ```typescript title="app/tools/put-content.ts"
46
+ import { tool } from "ai";
47
+ import { z } from "zod";
48
+ import { getSession } from "aixyz/app/plugins/session";
49
+ import type { Accepts } from "aixyz/accepts";
50
+
51
+ export default tool({
52
+ description: "Store a key-value pair in the current user's session",
53
+ inputSchema: z.object({
54
+ key: z.string(),
55
+ value: z.string().nullable(),
56
+ }),
57
+ execute: async ({ key, value }) => {
58
+ const session = getSession();
59
+ if (!session) {
60
+ return { success: false, error: "No authenticated signer in context" };
61
+ }
62
+
63
+ if (value === null) {
64
+ await session.delete(key);
65
+ return { success: true, key, deleted: true };
66
+ }
67
+
68
+ await session.set(key, value);
69
+ return { success: true, key };
70
+ },
71
+ });
72
+
73
+ export const accepts: Accepts = { scheme: "exact", price: "$0.01" };
74
+ ```
75
+
76
+ ## Skills
77
+
78
+ | Skill | Description | Price |
79
+ | ------------- | ----------------------------------------------- | ------ |
80
+ | `put-content` | Store key-value content in payer-scoped session | $0.01 |
81
+ | `get-content` | Retrieve stored content from session | $0.001 |
82
+
83
+ ## Running
84
+
85
+ ```bash
86
+ cd examples/x402-sessions
87
+ # create a .env file
88
+ bun install
89
+ bun run dev
90
+ ```
91
+
92
+ <CardGroup cols={2}>
93
+ <Card title="SessionPlugin API" icon="plug" href="/api-reference/session-plugin">
94
+ Full API reference for SessionPlugin, Session, and SessionStore.
95
+ </Card>
96
+ <Card title="x402 Payments" icon="credit-card" href="/protocols/x402">
97
+ Payment protocol that provides payer identity for sessions.
98
+ </Card>
99
+ </CardGroup>
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: "Vercel Blob"
3
+ description: "MCP-only template that stores text on Vercel Blob"
4
+ ---
5
+
6
+ <Info>
7
+ **Source:** [`examples/with-vercel-blob`](https://github.com/AgentlyHQ/aixyz/tree/main/examples/with-vercel-blob)
8
+ </Info>
9
+
10
+ ## Overview
11
+
12
+ This template exposes two MCP tools, `put-text` and `get-text`, that store and retrieve text using Vercel Blob. Each call is priced at `$0.001` via x402. There is **no** `app/agent.ts` — the server only serves `/mcp`.
13
+
14
+ ## Project Structure
15
+
16
+ ```
17
+ with-vercel-blob/
18
+ ├── aixyz.config.ts # Agent metadata + x402 config
19
+ ├── app/
20
+ │ ├── icon.svg # Template icon
21
+ │ └── tools/
22
+ │ ├── put-text.ts # Write txt blob
23
+ │ └── get-text.ts # Read txt blob
24
+ ├── package.json
25
+ ├── tsconfig.json
26
+ └── vercel.json
27
+ ```
28
+
29
+ ## Tools
30
+
31
+ ### `put-text`
32
+
33
+ - Input: `text` (string, 1–50k chars)
34
+ - Returns: `{ id }` — a UUID identifying the stored text
35
+
36
+ ### `get-text`
37
+
38
+ - Input: `id` (UUID returned by `put-text`)
39
+ - Returns: `{ id, text }`
40
+
41
+ ## Environment
42
+
43
+ Set a Vercel Blob read/write token before running locally (required by both tools):
44
+
45
+ ```bash
46
+ echo 'BLOB_READ_WRITE_TOKEN=...' >> .env.local
47
+ ```
48
+
49
+ ## Payment
50
+
51
+ `put-text` and `get-text` each charge `$0.001` per call using the x402 settings in `aixyz.config.ts` (Base mainnet in production, Base Sepolia in development).
52
+
53
+ ## Running locally
54
+
55
+ ```bash
56
+ cd examples/with-vercel-blob
57
+ bun install
58
+ bun run dev
59
+ ```
60
+
61
+ Connect any MCP client (Claude Desktop, VS Code MCP extension, etc.) to `http://localhost:3000/mcp` and call `put-text` / `get-text` with the schemas above.
@@ -29,6 +29,9 @@ Standard agents using `app/agent.ts` + `app/tools/`. No custom server — the se
29
29
  <Card title="Chainlink" icon="link" href="/templates/basic/chainlink">
30
30
  Chainlink price feeds on Ethereum mainnet.
31
31
  </Card>
32
+ <Card title="Vercel Blob" icon="file-lines" href="/templates/basic/with-vercel-blob">
33
+ MCP-only txt storage with put-text/get-text using @vercel/blob.
34
+ </Card>
32
35
  </CardGroup>
33
36
 
34
37
  ## Advanced
@@ -54,4 +57,7 @@ Custom server wiring, bring-your-own payment facilitator, and testing patterns.
54
57
  <Card title="Express Integration" icon="plug" href="/templates/advanced/with-express">
55
58
  Mount AixyzApp as Express middleware alongside your own routes.
56
59
  </Card>
60
+ <Card title="x402 Sessions" icon="database" href="/templates/advanced/x402-sessions">
61
+ Payer-scoped session storage with x402 payment identity.
62
+ </Card>
57
63
  </CardGroup>
@@ -0,0 +1,9 @@
1
+ <!-- BEGIN:aixyz-agent-rules -->
2
+
3
+ # aixyz: ALWAYS read docs before coding
4
+
5
+ Before any aixyz work, find and read the relevant doc in `node_modules/aixyz/docs/`.
6
+ And you can read `node_modules/aixyz/examples/` for examples that you can reference.
7
+ Your training data is outdated — the docs are the source of truth.
8
+
9
+ <!-- END:aixyz-agent-rules -->