plugin-sagg-x402 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SAGG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # plugin-sagg-x402
2
+
3
+ An [ElizaOS](https://elizaos.ai/) plugin that pays for SAGG chat-completion requests
4
+ **per-request, via the [x402 protocol](https://x402.org/)** (a signed USDC micropayment)
5
+ instead of an API key. Registers as a `TEXT_LARGE`/`TEXT_SMALL` model handler, so an
6
+ ElizaOS agent can use SAGG as its LLM the same way it would use `@elizaos/plugin-openai`
7
+ or `@elizaos/plugin-anthropic` - except every call is paid for by the agent's own
8
+ funded wallet, with no human provisioning an API key first.
9
+
10
+ **Status: testnet only (Base Sepolia). Not published to npm or the ElizaOS plugin
11
+ registry - prepared for review, not distributed.**
12
+
13
+ ## Why this exists
14
+
15
+ ElizaOS agents are unusually well-suited for this: they already carry EVM wallets for
16
+ DeFi/on-chain actions (`@elizaos/plugin-evm` and others), so paying for an LLM call the
17
+ same way is a natural fit rather than a bolted-on feature. ElizaOS has no native x402 /
18
+ decentralized-inference plugin today - the same kind of gap
19
+ [langchain-ai/langchain#36306](https://github.com/langchain-ai/langchain/issues/36306)
20
+ documents for LangChain. This plugin is the ElizaOS counterpart to the sibling
21
+ `langchain-x402-sagg`, `crewai-x402-sagg`, and `sagg-mcp-server` adapters already built
22
+ for the same SAGG x402 endpoint - same protocol, same gateway, this ecosystem's own
23
+ idiom (a `Plugin` object with a `models` map) instead of a `Tool`/MCP server.
24
+
25
+ ## Why pure TypeScript (viem), not a bridge to the existing Go module
26
+
27
+ SAGG's own x402 client/server code (`x402module/`, now its own repo) is written in Go.
28
+ As with the earlier Python adapters, two approaches were considered: wrapping the Go
29
+ module via a subprocess/bridge, or reimplementing the EIP-712/EIP-3009 signing logic
30
+ natively. EIP-712 and EIP-3009 are open cryptographic standards, not Go-specific -
31
+ [viem](https://viem.sh) is the standard, actively maintained TypeScript implementation,
32
+ and it's already what `@elizaos/plugin-evm` itself is built on. Shipping or requiring a
33
+ platform-specific Go binary alongside an npm package would be real distribution
34
+ friction for no real benefit here - the same reasoning the LangChain adapter's own
35
+ README gives for choosing `eth_account` over a Go bridge in Python. Independently
36
+ verified, not just "no exception thrown": `src/__tests__/signer.test.ts` recovers the
37
+ signer's address from a produced signature via a **separate** call path
38
+ (`viem`'s `recoverTypedDataAddress`, not the package's own signing code), and includes
39
+ a double control that a corrupted signature genuinely fails that recovery.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install
45
+ npm run build
46
+ ```
47
+
48
+ Not published - install from this local checkout, and reference it by local path from
49
+ an ElizaOS project's `character.ts`:
50
+
51
+ ```ts
52
+ plugins: [
53
+ '@elizaos/plugin-sql',
54
+ '../plugin-sagg-x402',
55
+ ]
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ All read via `runtime.getSetting()` (character `settings` or environment variables):
61
+
62
+ | Setting | Default | Meaning |
63
+ |---|---|---|
64
+ | `SAGG_X402_URL` | required | Base URL of the SAGG gateway (no trailing `/v1/...`). |
65
+ | `SAGG_X402_PRIVATE_KEY` | none | Dedicated hex EVM private key for x402 payments. **Recommended over `EVM_PRIVATE_KEY`** - see Security below. |
66
+ | `EVM_PRIVATE_KEY` | none | Fallback: the same key `@elizaos/plugin-evm` already reads. Used only if `SAGG_X402_PRIVATE_KEY` is unset. |
67
+ | `SAGG_X402_MODEL` | `deepseek-ai/DeepSeek-V4-Flash-0731` | Model id to request. |
68
+ | `SAGG_X402_MAX_AMOUNT_ATOMIC` | `1000000` (1.00 USDC) | Hard per-call payment ceiling, in USDC's smallest unit (6 decimals). A 402 challenge asking for more is refused **before any signature is produced**. |
69
+ | `SAGG_X402_NETWORK` | `base-sepolia` | Only network this plugin has been tested against. `base` (mainnet) is accepted by the code but untested and out of scope - see Security. |
70
+
71
+ One of `SAGG_X402_PRIVATE_KEY` or `EVM_PRIVATE_KEY` is required; a missing/misconfigured
72
+ key surfaces as a clear error on the first `useModel` call routed here (not at plugin
73
+ load time - see `model.ts`'s own comment on why).
74
+
75
+ ## How it works
76
+
77
+ Registers the same handler function for both `ModelType.TEXT_LARGE` and
78
+ `ModelType.TEXT_SMALL` (SAGG's gateway already does provider tiering/failover
79
+ server-side - this plugin doesn't reimplement a large/small distinction of its own; see
80
+ "What this plugin does NOT do"). On each `useModel` call:
81
+
82
+ 1. POST the composed prompt/messages to `{SAGG_X402_URL}/v1/chat/completions`.
83
+ 2. If the response is `402 Payment Required`, parse the price from the challenge
84
+ (`accepts[0]`).
85
+ 3. Refuse if the price exceeds `SAGG_X402_MAX_AMOUNT_ATOMIC`.
86
+ 4. Sign a fresh EIP-3009 `TransferWithAuthorization` for the exact amount, to the exact
87
+ recipient, with a fresh random nonce (never reused - see `signer.ts`'s own comment on
88
+ why that's the actual replay-protection mechanism).
89
+ 5. Repost with an `X-PAYMENT` header carrying the signed payload.
90
+ 6. Return the model's response text.
91
+
92
+ ## A real finding from live-testing, not assumed from the spec
93
+
94
+ The generic x402 spec's `X-PAYMENT-RESPONSE` header (what `x402-go`'s own stock
95
+ middleware sets, and what the sibling Go/Python clients' `GetSettlement`/
96
+ `get_settlement` look for) is **never sent** by SAGG's actual `/v1/chat/completions`
97
+ x402 path - confirmed by inspecting real response headers from a live gateway
98
+ instance, not by reading the handler source alone. SAGG's `x402ChatCompletionsHandler`
99
+ is built on `DynamicServer` (needed for per-request dynamic pricing - x402-go's stock
100
+ middleware only supports one fixed price set at startup) and sets its own plain
101
+ headers instead: `X-SAGG-X402-Ceiling-Tx` (always present on a served response) and
102
+ `X-SAGG-X402-Refund-Tx` (present only when a refund was actually sent). This plugin's
103
+ `SaggX402Client.getSettlement()` reads the real headers; worth fixing in the sibling
104
+ Python adapters too, but out of this plugin's own scope.
105
+
106
+ Also corrected here: the sibling LangChain/CrewAI adapters default to the model id
107
+ `deepseek-v4-flash-0731`. The real, documented full-form id is
108
+ `deepseek-ai/DeepSeek-V4-Flash-0731` (PROJECT_STATE.md sec 3.164) - the short form is
109
+ exactly the class of bug that section found and fixed elsewhere (it breaks model-name
110
+ translation for the `eterial-deepseek`/`gonka24-deepseek` fallback tiers specifically).
111
+ This plugin defaults to the correct full form; worth fixing upstream in the sibling
112
+ adapters too.
113
+
114
+ ## Testing
115
+
116
+ ```bash
117
+ npm test # fast, no network - signing/recovery logic + a mocked-fetch payment flow
118
+ npm run test:live # opt-in, moves real (testnet) funds - needs a reachable SAGG endpoint
119
+ ```
120
+
121
+ ```bash
122
+ SAGG_X402_TEST_PRIVATE_KEY=0x... \
123
+ SAGG_X402_TEST_URL=http://localhost:8080 \
124
+ RUN_LIVE_X402_TEST=1 \
125
+ npm run test:live
126
+ ```
127
+
128
+ **This was run for real** against a locally-running SAGG gateway built from
129
+ `feat/x402-billing-integration` (Base Sepolia, real `x402.org/facilitator`, real
130
+ on-chain settlement) during this plugin's own development - 5 consecutive real payment
131
+ cycles across both the raw client and the actual registered `handleGenerateText` model
132
+ handler, each ceiling transaction independently confirmed via a raw
133
+ `eth_getTransactionReceipt` RPC call (not trusted from the gateway's own response
134
+ alone) - e.g. `0xcd8df9355f0a86758c1b228607856700e8e47d39cbccab61063c2a0b46e59f27`,
135
+ `status: 0x1`, a real USDC `Transfer` event for exactly 326 atomic units
136
+ (0.000326 USDC) from the test wallet to SAGG's configured `payTo` address. Every real
137
+ completion came back as genuine model output (e.g. a literal `"pong"` for a
138
+ `"reply with exactly one word: pong"` prompt), not a canned/mocked value.
139
+
140
+ **One real limitation, disclosed rather than worked around**: this development session's
141
+ server-side test wallet held testnet USDC but no testnet ETH, so every refund attempt
142
+ (the ceiling always includes a safety margin above the real cost, so a small refund is
143
+ normal) failed with `insufficient funds for gas` and was logged - but the request was
144
+ still served successfully to the client either way, since SAGG's own handler treats a
145
+ refund failure as a bookkeeping gap, never a reason to fail an already-paid request (see
146
+ `cmd/gateway/x402billing.go`). The **payment-and-serve path this plugin actually
147
+ depends on was fully live-verified**; the refund leg specifically was not, for lack of
148
+ testnet ETH on the server wallet used this session - a future run with a
149
+ gas-funded server wallet would additionally confirm `X-SAGG-X402-Refund-Tx` end to end.
150
+ Separately: this run surfaced a real, pre-existing minor bug in SAGG's own gateway - a
151
+ refund failure for ANY reason (including plain insufficient gas, as hit here) is logged
152
+ as `"actual cost exceeded ceiling - refund refused per metering's own invariant"`,
153
+ which is only actually true for one specific failure cause, not this one. Flagged for
154
+ SAGG's own maintainers; not fixed here, out of this plugin's scope.
155
+
156
+ ## Security
157
+
158
+ - **Testnet only.** Tested exclusively against Base Sepolia. Do not put a
159
+ mainnet-funded private key in `SAGG_X402_PRIVATE_KEY`/`EVM_PRIVATE_KEY` - nothing in
160
+ this plugin's code prevents it, but no part of it has been reviewed or tested for
161
+ mainnet use.
162
+ - **Use a dedicated key, not the agent's main wallet, if you can.** `EVM_PRIVATE_KEY`
163
+ (the same key `plugin-evm` uses for DeFi actions) works as a fallback for
164
+ convenience, but a compromised or over-eager LLM-payment path then has access to
165
+ every other fund that wallet holds. `SAGG_X402_PRIVATE_KEY` lets you isolate this
166
+ plugin's spending to a wallet that holds only what you're willing to lose - the same
167
+ principle the sibling Python adapters' own README states.
168
+ - **The private key is held in plain process memory** for the agent's lifetime - the
169
+ same custody model SAGG's own Go server wallet uses today (see the main
170
+ `sagg-project` repo's Stage 3 security audit, `PROJECT_STATE.md` sec 3.147).
171
+ - `SAGG_X402_MAX_AMOUNT_ATOMIC` is a real, enforced ceiling checked before any
172
+ signature is produced, but it's a courtesy limit on THIS client's own behavior - it
173
+ does not protect against a malicious or compromised server issuing a valid-looking
174
+ but overpriced challenge below that ceiling. Set it to the smallest value your actual
175
+ use case needs.
176
+ - **Signing refuses on a network mismatch, before any signature is produced.** Added
177
+ after a real failure in the sibling `langchain-x402-sagg` package's first mainnet
178
+ attempt: a client configured for one network but pointed at a server actually
179
+ issuing a challenge for a different one would otherwise sign with the wrong chain id
180
+ - a well-formed but cryptographically wrong signature the facilitator correctly
181
+ rejects, with no clue in the error as to why. `postWithPayment` now checks the
182
+ challenge's own `network` field against `SAGG_X402_NETWORK` and throws
183
+ `SaggX402PaymentError` immediately on any mismatch instead.
184
+
185
+ ## What this plugin does NOT do
186
+
187
+ - Publish itself anywhere (no npm, no ElizaOS plugin registry) - prepared for review
188
+ only, per its own scope.
189
+ - Distinguish `TEXT_LARGE` from `TEXT_SMALL` - both call the same configured SAGG model;
190
+ SAGG's own gateway already handles provider tiering server-side.
191
+ - Support streaming responses, `TEXT_EMBEDDING`, or any ModelType beyond text
192
+ generation.
193
+ - Implement provider failover/retries beyond what SAGG's own gateway already does -
194
+ this is a minimal, first-version client.
195
+ - Touch mainnet, create a real wallet, or move real money in any way.
@@ -0,0 +1,211 @@
1
+ import { IAgentRuntime, Plugin } from '@elizaos/core';
2
+ import { Hex } from 'viem';
3
+
4
+ /**
5
+ * The actual SAGG-backed TEXT_LARGE/TEXT_SMALL model handler. SAGG's own
6
+ * gateway already does provider tiering/failover server-side (Standard
7
+ * vs Super Deal lines - see the main sagg-project repo) - this plugin
8
+ * deliberately does not reimplement a large/small model distinction of
9
+ * its own; both ModelType.TEXT_LARGE and ModelType.TEXT_SMALL are wired
10
+ * to the SAME handler and the same configured model id (see README's
11
+ * "What this plugin does NOT do" for this and other scope calls).
12
+ */
13
+
14
+ interface GenerateTextParamsLike {
15
+ prompt?: string;
16
+ system?: string;
17
+ messages?: Array<{
18
+ role: string;
19
+ content: string;
20
+ }>;
21
+ maxTokens?: number;
22
+ temperature?: number;
23
+ stopSequences?: string[];
24
+ }
25
+ /** The handler wired to ModelType.TEXT_LARGE/TEXT_SMALL in index.ts.
26
+ * Pays via x402 on every call - SAGG's x402 path is a genuinely separate
27
+ * accounting mechanism from its API-key/Stripe path (no quota, no
28
+ * subscription), so there is no "free" request to fall back to if a
29
+ * payment fails; a payment/HTTP error here is a real, user-visible
30
+ * failure, not something to swallow. */
31
+ declare function handleGenerateText(runtime: IAgentRuntime, params: GenerateTextParamsLike): Promise<string>;
32
+
33
+ /**
34
+ * HTTP client implementing the client side of the x402 "exact" scheme:
35
+ * probe -> 402 challenge -> sign -> pay -> response. Mirrors
36
+ * x402module/poc/pkg/x402protocol/dynamicclient.go's own two-request
37
+ * design (a fresh, unpaid probe carrying the real body, then a SEPARATE
38
+ * paid retry with its own fresh body) and the sibling Python adapters'
39
+ * client.py (langchain-x402-sagg, crewai-x402-sagg) field-for-field.
40
+ * Node's global `fetch` builds a brand-new request body from the object
41
+ * passed to it on each call, so - like the Python adapters using
42
+ * `requests` - there is no equivalent of the mark3labs/x402-go
43
+ * single-consumed-body bug this project found and worked around in Go
44
+ * (PROJECT_STATE.md sec 3.145); noted for comparison, not because a
45
+ * workaround was needed here.
46
+ */
47
+
48
+ declare class SaggX402PaymentError extends Error {
49
+ }
50
+ /** Shaped around what SAGG's own x402 handler actually reports, not the
51
+ * generic x402 spec's success/errorReason/transaction Settlement shape
52
+ * (see getSettlement's own doc comment for why those two differ). */
53
+ interface Settlement {
54
+ ceilingTx: string;
55
+ refundTx: string | null;
56
+ }
57
+ interface SaggX402ClientOptions {
58
+ privateKey: Hex;
59
+ maxAmountAtomic: bigint;
60
+ network?: string;
61
+ timeoutMs?: number;
62
+ }
63
+ /** Pays for x402-gated SAGG requests using an EVM private key, enforcing
64
+ * a hard per-call maximum (maxAmountAtomic) independent of whatever a
65
+ * server's own requirement asks for - the same "never auto-pay more
66
+ * than this" discipline as the Go module's WalletSigner and both Python
67
+ * adapters. */
68
+ declare class SaggX402Client {
69
+ private readonly privateKey;
70
+ private readonly maxAmountAtomic;
71
+ private readonly network;
72
+ private readonly chainId;
73
+ private readonly timeoutMs;
74
+ constructor(options: SaggX402ClientOptions);
75
+ /** POSTs jsonBody to url. If the first response is a real x402 402
76
+ * challenge, signs the first accepted requirement (refusing if it
77
+ * exceeds maxAmountAtomic) and reposts a FRESH request with the
78
+ * resulting X-PAYMENT header. Any other first response (2xx, or a
79
+ * non-402 error) is returned as-is, unpaid. */
80
+ postWithPayment(url: string, jsonBody: Record<string, unknown>): Promise<Response>;
81
+ /** Extracts settlement info from SAGG's OWN response headers.
82
+ *
83
+ * A real, live-testing-only finding, not something assumed from the
84
+ * spec or copied from the sibling adapters: the generic x402 spec's
85
+ * `X-PAYMENT-RESPONSE` header (base64 JSON, what x402-go's OWN stock
86
+ * middleware sets, and what x402protocol.GetSettlement (client.go)
87
+ * and the sibling LangChain/CrewAI adapters' get_settlement look for)
88
+ * is NEVER set by SAGG's actual `/v1/chat/completions` x402 path -
89
+ * confirmed by inspecting real response headers from a live local
90
+ * gateway instance, not by reading the handler source alone. SAGG's
91
+ * x402ChatCompletionsHandler (x402billing.go) is built directly on
92
+ * DynamicServer (needed for per-request dynamic pricing - x402-go's
93
+ * own stock middleware only supports one fixed price set at startup),
94
+ * and sets its OWN plain headers instead:
95
+ * `X-SAGG-X402-Ceiling-Tx` (always present on a served response) and
96
+ * `X-SAGG-X402-Refund-Tx` (present only when a refund was actually
97
+ * sent - absent, not empty, when the refund amount was zero OR when
98
+ * sending it failed, e.g. the server wallet lacking gas - see this
99
+ * plugin's README for what that means for a real refund's
100
+ * observability from the client side). No `success`/`errorReason`
101
+ * fields exist in this scheme at all - a served 200 response with a
102
+ * ceiling-tx header IS the success signal. */
103
+ static getSettlement(response: Response): Settlement | null;
104
+ private postJson;
105
+ }
106
+
107
+ /**
108
+ * EIP-3009 (transferWithAuthorization) signing for the x402 "exact"
109
+ * payment scheme, via viem's own EIP-712 typed-data support.
110
+ *
111
+ * Mirrors the EXACT typed-data shape SAGG's Go gateway builds
112
+ * (x402module/poc/pkg/x402protocol/signer_adapter.go, WalletSigner.Sign)
113
+ * and this project's own Python adapters (langchain-x402-sagg/signer.py,
114
+ * crewai-x402-sagg) - same field names, same types, same domain
115
+ * structure - so a signature produced here is verified identically by
116
+ * the same facilitator/on-chain contract, regardless of which language
117
+ * signed it. EIP-712/EIP-3009 are open cryptographic standards, not
118
+ * anything Go- or Python-specific; viem is the standard, widely-used
119
+ * TypeScript implementation (the same library @elizaos/plugin-evm
120
+ * itself is built on) - this mirrors the earlier adapters' own choice
121
+ * of the host ecosystem's standard web3 library over a cross-language
122
+ * bridge to the Go module.
123
+ *
124
+ * One real difference from the Go/Python versions, not a bug: viem's
125
+ * signTypedData derives the "EIP712Domain" type entry itself from the
126
+ * `domain` object - it must NOT be listed again in `types` (unlike
127
+ * go-ethereum's apitypes.TypedData and eth_account's
128
+ * encode_typed_data/sign_typed_data, which both require it spelled out
129
+ * explicitly). Verified against viem's own docs before writing this.
130
+ */
131
+
132
+ interface SignedAuthorization {
133
+ signature: Hex;
134
+ fromAddress: Hex;
135
+ toAddress: Hex;
136
+ value: string;
137
+ validAfter: string;
138
+ validBefore: string;
139
+ nonce: Hex;
140
+ }
141
+ interface TransferAuthorizationTypedData {
142
+ domain: {
143
+ name: string;
144
+ version: string;
145
+ chainId: number;
146
+ verifyingContract: Hex;
147
+ };
148
+ types: {
149
+ TransferWithAuthorization: [
150
+ {
151
+ name: "from";
152
+ type: "address";
153
+ },
154
+ {
155
+ name: "to";
156
+ type: "address";
157
+ },
158
+ {
159
+ name: "value";
160
+ type: "uint256";
161
+ },
162
+ {
163
+ name: "validAfter";
164
+ type: "uint256";
165
+ },
166
+ {
167
+ name: "validBefore";
168
+ type: "uint256";
169
+ },
170
+ {
171
+ name: "nonce";
172
+ type: "bytes32";
173
+ }
174
+ ];
175
+ };
176
+ primaryType: "TransferWithAuthorization";
177
+ message: {
178
+ from: Hex;
179
+ to: Hex;
180
+ value: bigint;
181
+ validAfter: bigint;
182
+ validBefore: bigint;
183
+ nonce: Hex;
184
+ };
185
+ }
186
+ /** Builds the EIP-712 typed-data object for a TransferWithAuthorization -
187
+ * field-for-field identical to what SAGG's own Go signer and the sibling
188
+ * Python adapters build, minus the EIP712Domain entry viem derives on
189
+ * its own (see this file's own top comment). Exported separately from
190
+ * signTransferAuthorization so tests can independently re-derive the
191
+ * same digest viem signs, the same "recover the signer independently"
192
+ * discipline this whole project's own test suites already use. */
193
+ declare function buildTypedData(domainName: string, domainVersion: string, chainId: number, tokenAddress: Hex, fromAddress: Hex, toAddress: Hex, value: bigint, validAfter: bigint, validBefore: bigint, nonce: Hex): TransferAuthorizationTypedData;
194
+ /** Signs a fresh EIP-3009 authorization for exactly `value` atomic units
195
+ * of tokenAddress, payable to toAddress, valid from ~now until
196
+ * validTimeoutSeconds from now (mirrors the Go/Python signers' own
197
+ * 10-second clock-drift tolerance on validAfter). */
198
+ declare function signTransferAuthorization(privateKey: Hex, domainName: string, domainVersion: string, chainId: number, tokenAddress: Hex, toAddress: Hex, value: bigint, validTimeoutSeconds: number): Promise<SignedAuthorization>;
199
+
200
+ /**
201
+ * plugin-sagg-x402 - pays for SAGG (https://api.privatedeskai.com) LLM
202
+ * chat-completion requests per-request via the x402 protocol (a signed
203
+ * USDC micropayment) instead of an API key, registered as an ElizaOS
204
+ * model handler for ModelType.TEXT_LARGE/TEXT_SMALL. See README.md for
205
+ * why this exists and how it fits alongside the sibling LangChain/
206
+ * CrewAI/MCP adapters built for the same SAGG x402 endpoint.
207
+ */
208
+
209
+ declare const saggX402Plugin: Plugin;
210
+
211
+ export { SaggX402Client, SaggX402PaymentError, buildTypedData, saggX402Plugin as default, handleGenerateText, saggX402Plugin, signTransferAuthorization };
package/dist/index.js ADDED
@@ -0,0 +1,349 @@
1
+ // src/index.ts
2
+ import { logger, ModelType } from "@elizaos/core";
3
+
4
+ // src/signer.ts
5
+ import { privateKeyToAccount } from "viem/accounts";
6
+ function randomNonceHex() {
7
+ const bytes = new Uint8Array(32);
8
+ globalThis.crypto.getRandomValues(bytes);
9
+ return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
10
+ ""
11
+ );
12
+ }
13
+ function buildTypedData(domainName, domainVersion, chainId, tokenAddress, fromAddress, toAddress, value, validAfter, validBefore, nonce) {
14
+ return {
15
+ domain: {
16
+ name: domainName,
17
+ version: domainVersion,
18
+ chainId,
19
+ verifyingContract: tokenAddress
20
+ },
21
+ types: {
22
+ TransferWithAuthorization: [
23
+ { name: "from", type: "address" },
24
+ { name: "to", type: "address" },
25
+ { name: "value", type: "uint256" },
26
+ { name: "validAfter", type: "uint256" },
27
+ { name: "validBefore", type: "uint256" },
28
+ { name: "nonce", type: "bytes32" }
29
+ ]
30
+ },
31
+ primaryType: "TransferWithAuthorization",
32
+ message: {
33
+ from: fromAddress,
34
+ to: toAddress,
35
+ value,
36
+ validAfter,
37
+ validBefore,
38
+ nonce
39
+ }
40
+ };
41
+ }
42
+ async function signTransferAuthorization(privateKey, domainName, domainVersion, chainId, tokenAddress, toAddress, value, validTimeoutSeconds) {
43
+ const account = privateKeyToAccount(privateKey);
44
+ const now = BigInt(Math.floor(Date.now() / 1e3));
45
+ const validAfter = now - 10n;
46
+ const validBefore = now + BigInt(validTimeoutSeconds);
47
+ const nonce = randomNonceHex();
48
+ const typedData = buildTypedData(
49
+ domainName,
50
+ domainVersion,
51
+ chainId,
52
+ tokenAddress,
53
+ account.address,
54
+ toAddress,
55
+ value,
56
+ validAfter,
57
+ validBefore,
58
+ nonce
59
+ );
60
+ const signature = await account.signTypedData(typedData);
61
+ return {
62
+ signature,
63
+ fromAddress: account.address,
64
+ toAddress,
65
+ value: value.toString(),
66
+ validAfter: validAfter.toString(),
67
+ validBefore: validBefore.toString(),
68
+ nonce
69
+ };
70
+ }
71
+
72
+ // src/client.ts
73
+ var CHAIN_IDS = {
74
+ base: 8453,
75
+ "base-sepolia": 84532
76
+ };
77
+ var SaggX402PaymentError = class extends Error {
78
+ };
79
+ var SaggX402Client = class {
80
+ privateKey;
81
+ maxAmountAtomic;
82
+ network;
83
+ chainId;
84
+ timeoutMs;
85
+ constructor(options) {
86
+ const network = options.network ?? "base-sepolia";
87
+ const chainId = CHAIN_IDS[network];
88
+ if (chainId === void 0) {
89
+ throw new Error(
90
+ `unsupported network ${JSON.stringify(network)}, want one of ${Object.keys(CHAIN_IDS).join(", ")}`
91
+ );
92
+ }
93
+ this.privateKey = options.privateKey;
94
+ this.maxAmountAtomic = options.maxAmountAtomic;
95
+ this.network = network;
96
+ this.chainId = chainId;
97
+ this.timeoutMs = options.timeoutMs ?? 6e4;
98
+ }
99
+ /** POSTs jsonBody to url. If the first response is a real x402 402
100
+ * challenge, signs the first accepted requirement (refusing if it
101
+ * exceeds maxAmountAtomic) and reposts a FRESH request with the
102
+ * resulting X-PAYMENT header. Any other first response (2xx, or a
103
+ * non-402 error) is returned as-is, unpaid. */
104
+ async postWithPayment(url, jsonBody) {
105
+ const probe = await this.postJson(url, jsonBody);
106
+ if (probe.status !== 402) {
107
+ return probe;
108
+ }
109
+ const challenge = await probe.json();
110
+ const accepts = challenge.accepts ?? [];
111
+ if (accepts.length === 0) {
112
+ throw new SaggX402PaymentError(
113
+ "402 challenge carried no accepted payment methods"
114
+ );
115
+ }
116
+ const requirement = accepts[0];
117
+ const amount = BigInt(requirement.maxAmountRequired);
118
+ if (amount > this.maxAmountAtomic) {
119
+ throw new SaggX402PaymentError(
120
+ `requirement asks for ${amount} atomic units, exceeds this client's configured max ${this.maxAmountAtomic}`
121
+ );
122
+ }
123
+ const extra = requirement.extra ?? {};
124
+ if (!extra.name || !extra.version) {
125
+ throw new SaggX402PaymentError(
126
+ "requirement missing EIP-3009 domain name/version in its 'extra' field"
127
+ );
128
+ }
129
+ if (requirement.network !== this.network) {
130
+ throw new SaggX402PaymentError(
131
+ `requirement's network ${JSON.stringify(requirement.network)} does not match this client's configured network ${JSON.stringify(this.network)} - refusing to sign. This client was likely constructed with the wrong \`network\` option for the URL it's actually talking to.`
132
+ );
133
+ }
134
+ const signed = await signTransferAuthorization(
135
+ this.privateKey,
136
+ extra.name,
137
+ extra.version,
138
+ this.chainId,
139
+ requirement.asset,
140
+ requirement.payTo,
141
+ amount,
142
+ requirement.maxTimeoutSeconds ?? 60
143
+ );
144
+ const paymentPayload = {
145
+ x402Version: 1,
146
+ scheme: "exact",
147
+ network: requirement.network,
148
+ payload: {
149
+ signature: signed.signature,
150
+ authorization: {
151
+ from: signed.fromAddress,
152
+ to: signed.toAddress,
153
+ value: signed.value,
154
+ validAfter: signed.validAfter,
155
+ validBefore: signed.validBefore,
156
+ nonce: signed.nonce
157
+ }
158
+ }
159
+ };
160
+ const headerValue = Buffer.from(JSON.stringify(paymentPayload)).toString(
161
+ "base64"
162
+ );
163
+ return this.postJson(url, jsonBody, { "X-PAYMENT": headerValue });
164
+ }
165
+ /** Extracts settlement info from SAGG's OWN response headers.
166
+ *
167
+ * A real, live-testing-only finding, not something assumed from the
168
+ * spec or copied from the sibling adapters: the generic x402 spec's
169
+ * `X-PAYMENT-RESPONSE` header (base64 JSON, what x402-go's OWN stock
170
+ * middleware sets, and what x402protocol.GetSettlement (client.go)
171
+ * and the sibling LangChain/CrewAI adapters' get_settlement look for)
172
+ * is NEVER set by SAGG's actual `/v1/chat/completions` x402 path -
173
+ * confirmed by inspecting real response headers from a live local
174
+ * gateway instance, not by reading the handler source alone. SAGG's
175
+ * x402ChatCompletionsHandler (x402billing.go) is built directly on
176
+ * DynamicServer (needed for per-request dynamic pricing - x402-go's
177
+ * own stock middleware only supports one fixed price set at startup),
178
+ * and sets its OWN plain headers instead:
179
+ * `X-SAGG-X402-Ceiling-Tx` (always present on a served response) and
180
+ * `X-SAGG-X402-Refund-Tx` (present only when a refund was actually
181
+ * sent - absent, not empty, when the refund amount was zero OR when
182
+ * sending it failed, e.g. the server wallet lacking gas - see this
183
+ * plugin's README for what that means for a real refund's
184
+ * observability from the client side). No `success`/`errorReason`
185
+ * fields exist in this scheme at all - a served 200 response with a
186
+ * ceiling-tx header IS the success signal. */
187
+ static getSettlement(response) {
188
+ const ceilingTx = response.headers.get("X-SAGG-X402-Ceiling-Tx");
189
+ if (!ceilingTx) {
190
+ return null;
191
+ }
192
+ return {
193
+ ceilingTx,
194
+ refundTx: response.headers.get("X-SAGG-X402-Refund-Tx")
195
+ };
196
+ }
197
+ async postJson(url, jsonBody, extraHeaders) {
198
+ const controller = new AbortController();
199
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
200
+ try {
201
+ return await fetch(url, {
202
+ method: "POST",
203
+ headers: { "Content-Type": "application/json", ...extraHeaders },
204
+ body: JSON.stringify(jsonBody),
205
+ signal: controller.signal
206
+ });
207
+ } finally {
208
+ clearTimeout(timer);
209
+ }
210
+ }
211
+ };
212
+
213
+ // src/model.ts
214
+ var DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731";
215
+ var DEFAULT_MAX_AMOUNT_ATOMIC = 1000000n;
216
+ var DEFAULT_NETWORK = "base-sepolia";
217
+ var cachedClient = null;
218
+ var cachedClientKey = null;
219
+ function requireSetting(runtime, key) {
220
+ const value = runtime.getSetting(key);
221
+ if (value === null || value === void 0 || value === "") {
222
+ throw new Error(
223
+ `plugin-sagg-x402: required setting ${key} is not configured`
224
+ );
225
+ }
226
+ return String(value);
227
+ }
228
+ function optionalSetting(runtime, key) {
229
+ const value = runtime.getSetting(key);
230
+ if (value === null || value === void 0 || value === "") {
231
+ return null;
232
+ }
233
+ return String(value);
234
+ }
235
+ function resolvePrivateKey(runtime) {
236
+ const dedicated = optionalSetting(runtime, "SAGG_X402_PRIVATE_KEY");
237
+ const fallback = dedicated ?? optionalSetting(runtime, "EVM_PRIVATE_KEY");
238
+ if (!fallback) {
239
+ throw new Error(
240
+ "plugin-sagg-x402: neither SAGG_X402_PRIVATE_KEY nor EVM_PRIVATE_KEY is configured"
241
+ );
242
+ }
243
+ return fallback.startsWith("0x") ? fallback : `0x${fallback}`;
244
+ }
245
+ function getClient(runtime) {
246
+ const privateKey = resolvePrivateKey(runtime);
247
+ const network = optionalSetting(runtime, "SAGG_X402_NETWORK") ?? DEFAULT_NETWORK;
248
+ const maxAmountSetting = optionalSetting(
249
+ runtime,
250
+ "SAGG_X402_MAX_AMOUNT_ATOMIC"
251
+ );
252
+ const maxAmountAtomic = maxAmountSetting ? BigInt(maxAmountSetting) : DEFAULT_MAX_AMOUNT_ATOMIC;
253
+ const cacheKey = `${privateKey}:${network}:${maxAmountAtomic}`;
254
+ if (cachedClient && cachedClientKey === cacheKey) {
255
+ return cachedClient;
256
+ }
257
+ cachedClient = new SaggX402Client({ privateKey, network, maxAmountAtomic });
258
+ cachedClientKey = cacheKey;
259
+ return cachedClient;
260
+ }
261
+ function buildMessages(params) {
262
+ const messages = [];
263
+ if (params.system) {
264
+ messages.push({ role: "system", content: params.system });
265
+ }
266
+ if (params.messages && params.messages.length > 0) {
267
+ for (const m of params.messages) {
268
+ messages.push({ role: m.role, content: m.content });
269
+ }
270
+ return messages;
271
+ }
272
+ if (params.prompt) {
273
+ messages.push({ role: "user", content: params.prompt });
274
+ return messages;
275
+ }
276
+ throw new Error(
277
+ "plugin-sagg-x402: GenerateTextParams carried neither `messages` nor `prompt`"
278
+ );
279
+ }
280
+ async function handleGenerateText(runtime, params) {
281
+ const saggUrl = requireSetting(runtime, "SAGG_X402_URL").replace(/\/+$/, "");
282
+ const model = optionalSetting(runtime, "SAGG_X402_MODEL") ?? DEFAULT_MODEL;
283
+ const client = getClient(runtime);
284
+ const body = {
285
+ model,
286
+ messages: buildMessages(params)
287
+ };
288
+ if (params.maxTokens !== void 0) body.max_tokens = params.maxTokens;
289
+ if (params.temperature !== void 0) body.temperature = params.temperature;
290
+ if (params.stopSequences && params.stopSequences.length > 0) {
291
+ body.stop = params.stopSequences;
292
+ }
293
+ let response;
294
+ try {
295
+ response = await client.postWithPayment(
296
+ `${saggUrl}/v1/chat/completions`,
297
+ body
298
+ );
299
+ } catch (err) {
300
+ if (err instanceof SaggX402PaymentError) {
301
+ throw new Error(`plugin-sagg-x402: payment refused: ${err.message}`);
302
+ }
303
+ throw err;
304
+ }
305
+ if (!response.ok) {
306
+ const text = await response.text().catch(() => "");
307
+ throw new Error(
308
+ `plugin-sagg-x402: SAGG request failed (${response.status}): ${text}`
309
+ );
310
+ }
311
+ const data = await response.json();
312
+ const content = data.choices?.[0]?.message?.content;
313
+ if (typeof content !== "string") {
314
+ throw new Error(
315
+ "plugin-sagg-x402: SAGG response had no choices[0].message.content"
316
+ );
317
+ }
318
+ return content;
319
+ }
320
+
321
+ // src/index.ts
322
+ var saggX402Plugin = {
323
+ name: "sagg-x402",
324
+ description: "Pays for SAGG LLM completions per-request via the x402 protocol (signed USDC micropayment, no API key). Testnet (Base Sepolia) only as shipped.",
325
+ async init(_config, runtime) {
326
+ const hasUrl = Boolean(runtime.getSetting("SAGG_X402_URL"));
327
+ const hasKey = Boolean(runtime.getSetting("SAGG_X402_PRIVATE_KEY")) || Boolean(runtime.getSetting("EVM_PRIVATE_KEY"));
328
+ if (!hasUrl || !hasKey) {
329
+ logger.warn(
330
+ "plugin-sagg-x402: SAGG_X402_URL and/or a private key (SAGG_X402_PRIVATE_KEY or EVM_PRIVATE_KEY) are not configured - TEXT_LARGE/TEXT_SMALL calls routed to this plugin will fail."
331
+ );
332
+ }
333
+ },
334
+ models: {
335
+ [ModelType.TEXT_LARGE]: handleGenerateText,
336
+ [ModelType.TEXT_SMALL]: handleGenerateText
337
+ }
338
+ };
339
+ var index_default = saggX402Plugin;
340
+ export {
341
+ SaggX402Client,
342
+ SaggX402PaymentError,
343
+ buildTypedData,
344
+ index_default as default,
345
+ handleGenerateText,
346
+ saggX402Plugin,
347
+ signTransferAuthorization
348
+ };
349
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/signer.ts","../src/client.ts","../src/model.ts"],"sourcesContent":["/**\n * plugin-sagg-x402 - pays for SAGG (https://api.privatedeskai.com) LLM\n * chat-completion requests per-request via the x402 protocol (a signed\n * USDC micropayment) instead of an API key, registered as an ElizaOS\n * model handler for ModelType.TEXT_LARGE/TEXT_SMALL. See README.md for\n * why this exists and how it fits alongside the sibling LangChain/\n * CrewAI/MCP adapters built for the same SAGG x402 endpoint.\n */\n\nimport type { IAgentRuntime, Plugin } from \"@elizaos/core\";\nimport { logger, ModelType } from \"@elizaos/core\";\nimport { handleGenerateText } from \"./model.js\";\n\nexport const saggX402Plugin: Plugin = {\n name: \"sagg-x402\",\n description:\n \"Pays for SAGG LLM completions per-request via the x402 protocol (signed USDC micropayment, no API key). Testnet (Base Sepolia) only as shipped.\",\n\n async init(_config: Record<string, string>, runtime: IAgentRuntime) {\n // Deliberately does NOT validate/throw here - see model.ts's own\n // comment on why a missing/bad private key surfaces at first real\n // call instead of at plugin load time. This only logs a clear,\n // early warning so a misconfiguration is visible in the agent's\n // own startup logs rather than a silent surprise on first use.\n const hasUrl = Boolean(runtime.getSetting(\"SAGG_X402_URL\"));\n const hasKey =\n Boolean(runtime.getSetting(\"SAGG_X402_PRIVATE_KEY\")) ||\n Boolean(runtime.getSetting(\"EVM_PRIVATE_KEY\"));\n if (!hasUrl || !hasKey) {\n logger.warn(\n \"plugin-sagg-x402: SAGG_X402_URL and/or a private key \" +\n \"(SAGG_X402_PRIVATE_KEY or EVM_PRIVATE_KEY) are not configured - \" +\n \"TEXT_LARGE/TEXT_SMALL calls routed to this plugin will fail.\",\n );\n }\n },\n\n models: {\n [ModelType.TEXT_LARGE]: handleGenerateText,\n [ModelType.TEXT_SMALL]: handleGenerateText,\n },\n};\n\nexport default saggX402Plugin;\nexport { handleGenerateText } from \"./model.js\";\nexport { SaggX402Client, SaggX402PaymentError } from \"./client.js\";\nexport { signTransferAuthorization, buildTypedData } from \"./signer.js\";\n","/**\n * EIP-3009 (transferWithAuthorization) signing for the x402 \"exact\"\n * payment scheme, via viem's own EIP-712 typed-data support.\n *\n * Mirrors the EXACT typed-data shape SAGG's Go gateway builds\n * (x402module/poc/pkg/x402protocol/signer_adapter.go, WalletSigner.Sign)\n * and this project's own Python adapters (langchain-x402-sagg/signer.py,\n * crewai-x402-sagg) - same field names, same types, same domain\n * structure - so a signature produced here is verified identically by\n * the same facilitator/on-chain contract, regardless of which language\n * signed it. EIP-712/EIP-3009 are open cryptographic standards, not\n * anything Go- or Python-specific; viem is the standard, widely-used\n * TypeScript implementation (the same library @elizaos/plugin-evm\n * itself is built on) - this mirrors the earlier adapters' own choice\n * of the host ecosystem's standard web3 library over a cross-language\n * bridge to the Go module.\n *\n * One real difference from the Go/Python versions, not a bug: viem's\n * signTypedData derives the \"EIP712Domain\" type entry itself from the\n * `domain` object - it must NOT be listed again in `types` (unlike\n * go-ethereum's apitypes.TypedData and eth_account's\n * encode_typed_data/sign_typed_data, which both require it spelled out\n * explicitly). Verified against viem's own docs before writing this.\n */\n\nimport { privateKeyToAccount, type PrivateKeyAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\n\nexport interface SignedAuthorization {\n signature: Hex;\n fromAddress: Hex;\n toAddress: Hex;\n value: string;\n validAfter: string;\n validBefore: string;\n nonce: Hex;\n}\n\n/** A fresh, cryptographically random 32-byte EIP-3009 nonce - a new one\n * MUST be generated per authorization; reusing one with the same signer\n * produces a signature the on-chain contract rejects as already-used on\n * the second attempt (PROJECT_STATE.md's x402 Stage 3 audit, sec 3.147,\n * item 1 - EIP-3009's own nonce tracking is the real replay protection\n * here, not anything this client does). Uses the Web Crypto API\n * (globalThis.crypto), available in both Node 19+ and browsers - no\n * separate crypto import needed. */\nexport function randomNonceHex(): Hex {\n const bytes = new Uint8Array(32);\n globalThis.crypto.getRandomValues(bytes);\n return (\"0x\" +\n Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\n \"\",\n )) as Hex;\n}\n\ninterface TransferAuthorizationTypedData {\n domain: {\n name: string;\n version: string;\n chainId: number;\n verifyingContract: Hex;\n };\n types: {\n TransferWithAuthorization: [\n { name: \"from\"; type: \"address\" },\n { name: \"to\"; type: \"address\" },\n { name: \"value\"; type: \"uint256\" },\n { name: \"validAfter\"; type: \"uint256\" },\n { name: \"validBefore\"; type: \"uint256\" },\n { name: \"nonce\"; type: \"bytes32\" },\n ];\n };\n primaryType: \"TransferWithAuthorization\";\n message: {\n from: Hex;\n to: Hex;\n value: bigint;\n validAfter: bigint;\n validBefore: bigint;\n nonce: Hex;\n };\n}\n\n/** Builds the EIP-712 typed-data object for a TransferWithAuthorization -\n * field-for-field identical to what SAGG's own Go signer and the sibling\n * Python adapters build, minus the EIP712Domain entry viem derives on\n * its own (see this file's own top comment). Exported separately from\n * signTransferAuthorization so tests can independently re-derive the\n * same digest viem signs, the same \"recover the signer independently\"\n * discipline this whole project's own test suites already use. */\nexport function buildTypedData(\n domainName: string,\n domainVersion: string,\n chainId: number,\n tokenAddress: Hex,\n fromAddress: Hex,\n toAddress: Hex,\n value: bigint,\n validAfter: bigint,\n validBefore: bigint,\n nonce: Hex,\n): TransferAuthorizationTypedData {\n return {\n domain: {\n name: domainName,\n version: domainVersion,\n chainId,\n verifyingContract: tokenAddress,\n },\n types: {\n TransferWithAuthorization: [\n { name: \"from\", type: \"address\" },\n { name: \"to\", type: \"address\" },\n { name: \"value\", type: \"uint256\" },\n { name: \"validAfter\", type: \"uint256\" },\n { name: \"validBefore\", type: \"uint256\" },\n { name: \"nonce\", type: \"bytes32\" },\n ],\n },\n primaryType: \"TransferWithAuthorization\",\n message: {\n from: fromAddress,\n to: toAddress,\n value,\n validAfter,\n validBefore,\n nonce,\n },\n };\n}\n\n/** Signs a fresh EIP-3009 authorization for exactly `value` atomic units\n * of tokenAddress, payable to toAddress, valid from ~now until\n * validTimeoutSeconds from now (mirrors the Go/Python signers' own\n * 10-second clock-drift tolerance on validAfter). */\nexport async function signTransferAuthorization(\n privateKey: Hex,\n domainName: string,\n domainVersion: string,\n chainId: number,\n tokenAddress: Hex,\n toAddress: Hex,\n value: bigint,\n validTimeoutSeconds: number,\n): Promise<SignedAuthorization> {\n const account: PrivateKeyAccount = privateKeyToAccount(privateKey);\n const now = BigInt(Math.floor(Date.now() / 1000));\n const validAfter = now - 10n;\n const validBefore = now + BigInt(validTimeoutSeconds);\n const nonce = randomNonceHex();\n\n const typedData = buildTypedData(\n domainName,\n domainVersion,\n chainId,\n tokenAddress,\n account.address,\n toAddress,\n value,\n validAfter,\n validBefore,\n nonce,\n );\n\n const signature = await account.signTypedData(typedData);\n\n return {\n signature,\n fromAddress: account.address,\n toAddress,\n value: value.toString(),\n validAfter: validAfter.toString(),\n validBefore: validBefore.toString(),\n nonce,\n };\n}\n","/**\n * HTTP client implementing the client side of the x402 \"exact\" scheme:\n * probe -> 402 challenge -> sign -> pay -> response. Mirrors\n * x402module/poc/pkg/x402protocol/dynamicclient.go's own two-request\n * design (a fresh, unpaid probe carrying the real body, then a SEPARATE\n * paid retry with its own fresh body) and the sibling Python adapters'\n * client.py (langchain-x402-sagg, crewai-x402-sagg) field-for-field.\n * Node's global `fetch` builds a brand-new request body from the object\n * passed to it on each call, so - like the Python adapters using\n * `requests` - there is no equivalent of the mark3labs/x402-go\n * single-consumed-body bug this project found and worked around in Go\n * (PROJECT_STATE.md sec 3.145); noted for comparison, not because a\n * workaround was needed here.\n */\n\nimport { signTransferAuthorization } from \"./signer.js\";\nimport type { Hex } from \"viem\";\n\nconst CHAIN_IDS: Record<string, number> = {\n base: 8453,\n \"base-sepolia\": 84532,\n};\n\nexport class SaggX402PaymentError extends Error {}\n\n/** Shaped around what SAGG's own x402 handler actually reports, not the\n * generic x402 spec's success/errorReason/transaction Settlement shape\n * (see getSettlement's own doc comment for why those two differ). */\nexport interface Settlement {\n ceilingTx: string;\n refundTx: string | null;\n}\n\ninterface PaymentRequirement {\n maxAmountRequired: string;\n asset: Hex;\n payTo: Hex;\n network: string;\n maxTimeoutSeconds?: number;\n extra?: { name?: string; version?: string };\n}\n\ninterface X402Challenge {\n x402Version: number;\n error?: string;\n accepts?: PaymentRequirement[];\n}\n\nexport interface SaggX402ClientOptions {\n privateKey: Hex;\n maxAmountAtomic: bigint;\n network?: string;\n timeoutMs?: number;\n}\n\n/** Pays for x402-gated SAGG requests using an EVM private key, enforcing\n * a hard per-call maximum (maxAmountAtomic) independent of whatever a\n * server's own requirement asks for - the same \"never auto-pay more\n * than this\" discipline as the Go module's WalletSigner and both Python\n * adapters. */\nexport class SaggX402Client {\n private readonly privateKey: Hex;\n private readonly maxAmountAtomic: bigint;\n private readonly network: string;\n private readonly chainId: number;\n private readonly timeoutMs: number;\n\n constructor(options: SaggX402ClientOptions) {\n const network = options.network ?? \"base-sepolia\";\n const chainId = CHAIN_IDS[network];\n if (chainId === undefined) {\n throw new Error(\n `unsupported network ${JSON.stringify(network)}, want one of ${Object.keys(CHAIN_IDS).join(\", \")}`,\n );\n }\n this.privateKey = options.privateKey;\n this.maxAmountAtomic = options.maxAmountAtomic;\n this.network = network;\n this.chainId = chainId;\n this.timeoutMs = options.timeoutMs ?? 60_000;\n }\n\n /** POSTs jsonBody to url. If the first response is a real x402 402\n * challenge, signs the first accepted requirement (refusing if it\n * exceeds maxAmountAtomic) and reposts a FRESH request with the\n * resulting X-PAYMENT header. Any other first response (2xx, or a\n * non-402 error) is returned as-is, unpaid. */\n async postWithPayment(\n url: string,\n jsonBody: Record<string, unknown>,\n ): Promise<Response> {\n const probe = await this.postJson(url, jsonBody);\n if (probe.status !== 402) {\n return probe;\n }\n\n const challenge = (await probe.json()) as X402Challenge;\n const accepts = challenge.accepts ?? [];\n if (accepts.length === 0) {\n throw new SaggX402PaymentError(\n \"402 challenge carried no accepted payment methods\",\n );\n }\n const requirement = accepts[0];\n\n const amount = BigInt(requirement.maxAmountRequired);\n if (amount > this.maxAmountAtomic) {\n throw new SaggX402PaymentError(\n `requirement asks for ${amount} atomic units, exceeds this client's configured max ${this.maxAmountAtomic}`,\n );\n }\n\n const extra = requirement.extra ?? {};\n if (!extra.name || !extra.version) {\n throw new SaggX402PaymentError(\n \"requirement missing EIP-3009 domain name/version in its 'extra' field\",\n );\n }\n\n // this.chainId was fixed once at construction from this.network -\n // correct ONLY as long as the requirement the server actually\n // issued is for that same network. Real bug found live in the\n // sibling langchain-x402-sagg package on the first real mainnet\n // attempt (PROJECT_STATE.md sec 3.172, then confirmed to be the\n // same shape here too): a client built with the default\n // network=\"base-sepolia\" but pointed at a real mainnet SAGG_X402_URL\n // signs with chainId 84532 while the server's requirement is for\n // chainId 8453 - the facilitator correctly rejects the resulting\n // signature as invalid (\"invalid_exact_evm_signature\"), a confusing\n // failure with no clue as to the real cause. Refusing outright on a\n // mismatch (rather than silently trusting whatever the server\n // claims and re-deriving chainId from it) is the safer failure mode\n // too: a testnet-configured client must never be tricked into\n // producing a valid signature for a real mainnet transfer just\n // because it was pointed at the wrong URL.\n if (requirement.network !== this.network) {\n throw new SaggX402PaymentError(\n `requirement's network ${JSON.stringify(requirement.network)} does not match this client's configured network ${JSON.stringify(this.network)} - refusing to sign. This client was likely constructed with the wrong \\`network\\` option for the URL it's actually talking to.`,\n );\n }\n\n const signed = await signTransferAuthorization(\n this.privateKey,\n extra.name,\n extra.version,\n this.chainId,\n requirement.asset,\n requirement.payTo,\n amount,\n requirement.maxTimeoutSeconds ?? 60,\n );\n\n const paymentPayload = {\n x402Version: 1,\n scheme: \"exact\",\n network: requirement.network,\n payload: {\n signature: signed.signature,\n authorization: {\n from: signed.fromAddress,\n to: signed.toAddress,\n value: signed.value,\n validAfter: signed.validAfter,\n validBefore: signed.validBefore,\n nonce: signed.nonce,\n },\n },\n };\n const headerValue = Buffer.from(JSON.stringify(paymentPayload)).toString(\n \"base64\",\n );\n\n return this.postJson(url, jsonBody, { \"X-PAYMENT\": headerValue });\n }\n\n /** Extracts settlement info from SAGG's OWN response headers.\n *\n * A real, live-testing-only finding, not something assumed from the\n * spec or copied from the sibling adapters: the generic x402 spec's\n * `X-PAYMENT-RESPONSE` header (base64 JSON, what x402-go's OWN stock\n * middleware sets, and what x402protocol.GetSettlement (client.go)\n * and the sibling LangChain/CrewAI adapters' get_settlement look for)\n * is NEVER set by SAGG's actual `/v1/chat/completions` x402 path -\n * confirmed by inspecting real response headers from a live local\n * gateway instance, not by reading the handler source alone. SAGG's\n * x402ChatCompletionsHandler (x402billing.go) is built directly on\n * DynamicServer (needed for per-request dynamic pricing - x402-go's\n * own stock middleware only supports one fixed price set at startup),\n * and sets its OWN plain headers instead:\n * `X-SAGG-X402-Ceiling-Tx` (always present on a served response) and\n * `X-SAGG-X402-Refund-Tx` (present only when a refund was actually\n * sent - absent, not empty, when the refund amount was zero OR when\n * sending it failed, e.g. the server wallet lacking gas - see this\n * plugin's README for what that means for a real refund's\n * observability from the client side). No `success`/`errorReason`\n * fields exist in this scheme at all - a served 200 response with a\n * ceiling-tx header IS the success signal. */\n static getSettlement(response: Response): Settlement | null {\n const ceilingTx = response.headers.get(\"X-SAGG-X402-Ceiling-Tx\");\n if (!ceilingTx) {\n return null;\n }\n return {\n ceilingTx,\n refundTx: response.headers.get(\"X-SAGG-X402-Refund-Tx\"),\n };\n }\n\n private async postJson(\n url: string,\n jsonBody: Record<string, unknown>,\n extraHeaders?: Record<string, string>,\n ): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n return await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...extraHeaders },\n body: JSON.stringify(jsonBody),\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n }\n}\n","/**\n * The actual SAGG-backed TEXT_LARGE/TEXT_SMALL model handler. SAGG's own\n * gateway already does provider tiering/failover server-side (Standard\n * vs Super Deal lines - see the main sagg-project repo) - this plugin\n * deliberately does not reimplement a large/small model distinction of\n * its own; both ModelType.TEXT_LARGE and ModelType.TEXT_SMALL are wired\n * to the SAME handler and the same configured model id (see README's\n * \"What this plugin does NOT do\" for this and other scope calls).\n */\n\nimport type { IAgentRuntime } from \"@elizaos/core\";\nimport type { Hex } from \"viem\";\nimport { SaggX402Client, SaggX402PaymentError } from \"./client.js\";\n\n// The real, documented full-form model id (PROJECT_STATE.md sec 3.164) -\n// NOT the short \"deepseek-v4-flash-0731\" form the sibling LangChain/\n// CrewAI adapters default to. That short form is exactly the class of\n// bug sec 3.164 found and fixed in bench/superdeal_vs_direct.go and\n// cmd/warmup/main.go: it breaks model-name translation for the\n// eterial-deepseek/gonka24-deepseek fallback tiers specifically. Worth\n// fixing in the sibling adapters too, but out of this plugin's own\n// scope - flagged in this plugin's README instead.\nexport const DEFAULT_MODEL = \"deepseek-ai/DeepSeek-V4-Flash-0731\";\nexport const DEFAULT_MAX_AMOUNT_ATOMIC = 1_000_000n; // 1.00 USDC (6 decimals)\nexport const DEFAULT_NETWORK = \"base-sepolia\";\n\ninterface GenerateTextParamsLike {\n prompt?: string;\n system?: string;\n messages?: Array<{ role: string; content: string }>;\n maxTokens?: number;\n temperature?: number;\n stopSequences?: string[];\n}\n\ninterface ChatMessage {\n role: string;\n content: string;\n}\n\n/** One client per plugin instance, built lazily from runtime settings on\n * first use rather than in `init()` - a private key that's missing or\n * wrong should surface as a clear error on the first real request, not\n * silently disable the plugin at startup (ElizaOS loads plugins whose\n * `init` throws inconsistently across versions; failing at call time is\n * the more predictable contract). */\nlet cachedClient: SaggX402Client | null = null;\nlet cachedClientKey: string | null = null;\n\nfunction requireSetting(runtime: IAgentRuntime, key: string): string {\n const value = runtime.getSetting(key);\n if (value === null || value === undefined || value === \"\") {\n throw new Error(\n `plugin-sagg-x402: required setting ${key} is not configured`,\n );\n }\n return String(value);\n}\n\nfunction optionalSetting(runtime: IAgentRuntime, key: string): string | null {\n const value = runtime.getSetting(key);\n if (value === null || value === undefined || value === \"\") {\n return null;\n }\n return String(value);\n}\n\nfunction resolvePrivateKey(runtime: IAgentRuntime): Hex {\n const dedicated = optionalSetting(runtime, \"SAGG_X402_PRIVATE_KEY\");\n const fallback = dedicated ?? optionalSetting(runtime, \"EVM_PRIVATE_KEY\");\n if (!fallback) {\n throw new Error(\n \"plugin-sagg-x402: neither SAGG_X402_PRIVATE_KEY nor EVM_PRIVATE_KEY is configured\",\n );\n }\n return (fallback.startsWith(\"0x\") ? fallback : `0x${fallback}`) as Hex;\n}\n\nfunction getClient(runtime: IAgentRuntime): SaggX402Client {\n const privateKey = resolvePrivateKey(runtime);\n const network = optionalSetting(runtime, \"SAGG_X402_NETWORK\") ?? DEFAULT_NETWORK;\n const maxAmountSetting = optionalSetting(\n runtime,\n \"SAGG_X402_MAX_AMOUNT_ATOMIC\",\n );\n const maxAmountAtomic = maxAmountSetting\n ? BigInt(maxAmountSetting)\n : DEFAULT_MAX_AMOUNT_ATOMIC;\n\n // Cache key covers every input that changes what the client does -\n // a settings change (e.g. rotating the key) must produce a fresh\n // client, not silently keep signing with a stale one.\n const cacheKey = `${privateKey}:${network}:${maxAmountAtomic}`;\n if (cachedClient && cachedClientKey === cacheKey) {\n return cachedClient;\n }\n cachedClient = new SaggX402Client({ privateKey, network, maxAmountAtomic });\n cachedClientKey = cacheKey;\n return cachedClient;\n}\n\nfunction buildMessages(params: GenerateTextParamsLike): ChatMessage[] {\n const messages: ChatMessage[] = [];\n if (params.system) {\n messages.push({ role: \"system\", content: params.system });\n }\n if (params.messages && params.messages.length > 0) {\n for (const m of params.messages) {\n messages.push({ role: m.role, content: m.content });\n }\n return messages;\n }\n if (params.prompt) {\n messages.push({ role: \"user\", content: params.prompt });\n return messages;\n }\n throw new Error(\n \"plugin-sagg-x402: GenerateTextParams carried neither `messages` nor `prompt`\",\n );\n}\n\n/** The handler wired to ModelType.TEXT_LARGE/TEXT_SMALL in index.ts.\n * Pays via x402 on every call - SAGG's x402 path is a genuinely separate\n * accounting mechanism from its API-key/Stripe path (no quota, no\n * subscription), so there is no \"free\" request to fall back to if a\n * payment fails; a payment/HTTP error here is a real, user-visible\n * failure, not something to swallow. */\nexport async function handleGenerateText(\n runtime: IAgentRuntime,\n params: GenerateTextParamsLike,\n): Promise<string> {\n const saggUrl = requireSetting(runtime, \"SAGG_X402_URL\").replace(/\\/+$/, \"\");\n const model = optionalSetting(runtime, \"SAGG_X402_MODEL\") ?? DEFAULT_MODEL;\n const client = getClient(runtime);\n\n const body: Record<string, unknown> = {\n model,\n messages: buildMessages(params),\n };\n if (params.maxTokens !== undefined) body.max_tokens = params.maxTokens;\n if (params.temperature !== undefined) body.temperature = params.temperature;\n if (params.stopSequences && params.stopSequences.length > 0) {\n body.stop = params.stopSequences;\n }\n\n let response: Response;\n try {\n response = await client.postWithPayment(\n `${saggUrl}/v1/chat/completions`,\n body,\n );\n } catch (err) {\n if (err instanceof SaggX402PaymentError) {\n throw new Error(`plugin-sagg-x402: payment refused: ${err.message}`);\n }\n throw err;\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new Error(\n `plugin-sagg-x402: SAGG request failed (${response.status}): ${text}`,\n );\n }\n\n const data = (await response.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n };\n const content = data.choices?.[0]?.message?.content;\n if (typeof content !== \"string\") {\n throw new Error(\n \"plugin-sagg-x402: SAGG response had no choices[0].message.content\",\n );\n }\n return content;\n}\n\n/** Exported for tests only - lets a test reset the module-level cache\n * between cases instead of caching a client across unrelated tests. */\nexport function _resetClientCacheForTests(): void {\n cachedClient = null;\n cachedClientKey = null;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "plugin-sagg-x402",
3
+ "version": "0.1.0",
4
+ "description": "ElizaOS plugin: pay for SAGG LLM completions per-request via the x402 protocol (signed USDC micropayment) instead of an API key.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ "./package.json": "./package.json",
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "test": "vitest run src/__tests__/signer.test.ts src/__tests__/client.test.ts",
23
+ "test:live": "vitest run src/__tests__/live.test.ts"
24
+ },
25
+ "agentConfig": {
26
+ "pluginType": "elizaos:plugin:1.0.0",
27
+ "pluginParameters": {
28
+ "SAGG_X402_URL": {
29
+ "type": "string",
30
+ "description": "Base URL of the SAGG gateway, e.g. https://your-sagg-gateway.example.com (no trailing /v1/...)."
31
+ },
32
+ "SAGG_X402_MODEL": {
33
+ "type": "string",
34
+ "description": "Model id to request from SAGG. Defaults to deepseek-ai/DeepSeek-V4-Flash-0731."
35
+ },
36
+ "SAGG_X402_PRIVATE_KEY": {
37
+ "type": "string",
38
+ "description": "Dedicated hex EVM private key for x402 payments (TESTNET ONLY). Recommended over reusing EVM_PRIVATE_KEY so a compromised/overspending LLM-payment key can't touch the agent's other funds."
39
+ },
40
+ "EVM_PRIVATE_KEY": {
41
+ "type": "string",
42
+ "description": "Fallback: the same private key plugin-evm already reads, used only if SAGG_X402_PRIVATE_KEY is not set."
43
+ },
44
+ "SAGG_X402_MAX_AMOUNT_ATOMIC": {
45
+ "type": "string",
46
+ "description": "Hard per-call payment ceiling in USDC's smallest unit (6 decimals). Defaults to 1000000 (1.00 USDC). A 402 challenge asking for more is refused before any signature is produced."
47
+ },
48
+ "SAGG_X402_NETWORK": {
49
+ "type": "string",
50
+ "description": "\"base-sepolia\" (default, the only network this plugin has been tested against) or \"base\" (untested, out of scope as shipped)."
51
+ }
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "viem": "^2.21.0"
56
+ },
57
+ "peerDependencies": {
58
+ "@elizaos/core": "^1.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@elizaos/core": "^1.0.0",
62
+ "@types/node": "^22.0.0",
63
+ "tsup": "^8.3.5",
64
+ "typescript": "^5.6.0",
65
+ "vitest": "^2.1.0"
66
+ },
67
+ "license": "MIT"
68
+ }