diff --git a/.env.example b/.env.example
index d187c91..fe69f21 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,3 @@
SYNAPSE_ENV=staging
SYNAPSE_GATEWAY=
-SYNAPSE_API_KEY=agt_xxx
+SYNAPSE_AGENT_KEY=agt_xxx
diff --git a/README.md b/README.md
index 989b9f2..0c1b462 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,24 @@ SynapseNetwork lets an agent discover services, invoke them through a gateway, a
3. Search, invoke, and read the receipt.
> Public Preview default: SDK examples use `staging`, backed by `https://api-staging.synapse-network.ai`.
-> The `prod` preset points to `https://api.synapse-network.ai`, but production should only be used after official DNS and `/health` are live.
+> After production launch, replace the public examples and tests with the `prod` environment.
+
+## Choose Your Integration Path
+
+| Goal | Use |
+|---|---|
+| Connect SynapseNetwork to an agent framework such as Cursor, Claude Desktop, or LangChain | Official MCP server: `@synapse-network/mcp-server` with `SYNAPSE_AGENT_KEY=agt_xxx` |
+| Write application code that invokes services directly | This SDK with `SynapseClient` |
+| Issue Agent Keys or publish provider APIs | Advanced owner/provider APIs: `SynapseAuth` and `auth.provider()` |
+
+## Two Invocation Modes
+
+| Mode | Use for | SDK method | Required cost parameter | Billing result |
+|---|---|---|---|---|
+| Fixed-price API invoke | Normal API services discovered from the marketplace | Python `invoke()` / TypeScript `invoke()` | Pass latest discovery price as `cost_usdc` / `costUsdc` | Gateway rejects with `PRICE_MISMATCH` if the live price changed |
+| Token-metered LLM invoke | LLM services registered with `serviceKind=llm` and `priceModel=token_metered` | Python `invoke_llm()` / TypeScript `invokeLlm()` | Do not pass `cost_usdc` / `costUsdc`; optional cap is `max_cost_usdc` / `maxCostUsdc` | Gateway holds a cap, then charges final provider-reported token usage |
+
+Do not recompute money with floating-point math. Pass discovered prices and spend caps through exactly; prefer string amounts such as `"0.05"` when the SDK method accepts strings.
## Gateway Docs
@@ -40,8 +57,6 @@ SynapseNetwork lets an agent discover services, invoke them through a gateway, a
| Environment | Gateway URL | Intended use |
|---|---|---|
| `staging` | `https://api-staging.synapse-network.ai` | Public preview, test assets, integration trials |
-| `local` | `http://127.0.0.1:8000` | Local gateway development |
-| `prod` | `https://api.synapse-network.ai` | Production preset, pending official DNS and health verification |
Resolution rules:
@@ -74,7 +89,7 @@ Step 2: let your agent discover and work.
```bash
pip install synapse-client
export SYNAPSE_ENV=staging
-export SYNAPSE_API_KEY=agt_xxx
+export SYNAPSE_AGENT_KEY=agt_xxx
```
```python
@@ -88,7 +103,7 @@ service = services[0]
result = client.invoke(
service.service_id,
{"prompt": "hello"},
- cost_usdc=float(service.price_usdc),
+ cost_usdc=str(service.price_usdc),
idempotency_key="agent-job-001",
)
@@ -113,8 +128,13 @@ npm install @synapse-network/sdk
```ts
import { SynapseClient } from "@synapse-network/sdk";
+const agentKey = process.env.SYNAPSE_AGENT_KEY;
+if (!agentKey) {
+ throw new Error("SYNAPSE_AGENT_KEY is required");
+}
+
const client = new SynapseClient({
- credential: "agt_xxx",
+ credential: agentKey,
environment: "staging",
});
@@ -127,7 +147,7 @@ const result = await client.invoke(
service.serviceId ?? service.id!,
{ prompt: "hello" },
{
- costUsdc: Number(service.pricing?.amount ?? 0),
+ costUsdc: String(service.pricing?.amount ?? "0"),
idempotencyKey: "agent-job-001",
}
);
@@ -138,6 +158,8 @@ console.log(receipt.invocationId, receipt.status, receipt.chargedUsdc);
TypeScript does not read environment variables by itself. Read them in your app and pass `environment` or `gatewayUrl` explicitly.
+Python reads `SYNAPSE_AGENT_KEY` by default. `SYNAPSE_API_KEY` remains a legacy compatibility alias, but new examples should use `SYNAPSE_AGENT_KEY`.
+
### LLM token-metered calls
LLM services registered with `serviceKind=llm` and `priceModel=token_metered` use `invoke_llm()` / `invokeLlm()`. Do not pass `cost_usdc` / `costUsdc`; pass optional `max_cost_usdc` / `maxCostUsdc` or let Gateway compute the automatic hold. Streaming is rejected in V1 so Gateway can capture final usage safely.
@@ -267,7 +289,7 @@ PYTHONPATH="$PWD" .venv/bin/python examples/provider_staging_onboarding.py \
Call a provider service with an existing Agent Key:
```bash
-export SYNAPSE_API_KEY=agt_xxx
+export SYNAPSE_AGENT_KEY=agt_xxx
PYTHONPATH="$PWD" .venv/bin/python examples/consumer_call_provider.py \
--service-id "weather_api" \
--payload-json '{"prompt":"hello"}'
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 536bd5a..6d59ff5 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -24,7 +24,24 @@ SynapseNetwork 让 Agent 可以发现服务、通过 gateway 调用服务,并
3. 搜索服务、调用服务、读取 receipt。
> Public Preview 默认使用 `staging`,对应 `https://api-staging.synapse-network.ai`。
-> `prod` 预设指向 `https://api.synapse-network.ai`,但只有在官方 DNS 和 `/health` 验证通过后才应使用生产环境。
+> 生产环境上线后,再把公开示例和测试统一切换到 `prod`。
+
+## 选择你的接入路径
+
+| 目标 | 使用 |
+|---|---|
+| 把 SynapseNetwork 接入 Cursor、Claude Desktop、LangChain 等 Agent framework | 官方 MCP server:`@synapse-network/mcp-server`,设置 `SYNAPSE_AGENT_KEY=agt_xxx` |
+| 在应用代码里直接调用服务 | 本 SDK 的 `SynapseClient` |
+| 签发 Agent Key 或发布 Provider API | 高级 owner/provider API:`SynapseAuth` 和 `auth.provider()` |
+
+## 两种调用模式
+
+| 模式 | 适用场景 | SDK 方法 | 必传费用参数 | 计费结果 |
+|---|---|---|---|---|
+| Fixed-price API invoke | marketplace 发现的普通 API 服务 | Python `invoke()` / TypeScript `invoke()` | 传最新 discovery price:`cost_usdc` / `costUsdc` | 如果 live price 已变化,Gateway 用 `PRICE_MISMATCH` 拒绝 |
+| Token-metered LLM invoke | `serviceKind=llm` 且 `priceModel=token_metered` 的 LLM 服务 | Python `invoke_llm()` / TypeScript `invokeLlm()` | 不传 `cost_usdc` / `costUsdc`;可选上限是 `max_cost_usdc` / `maxCostUsdc` | Gateway 先冻结上限,再按 Provider 返回的 final token usage 扣费 |
+
+不要用浮点数重新计算金额。调用时传 discovery 得到的价格或预算上限;SDK 方法支持时优先使用字符串金额,例如 `"0.05"`。
## Gateway 文档
@@ -40,8 +57,6 @@ SynapseNetwork 让 Agent 可以发现服务、通过 gateway 调用服务,并
| 环境 | Gateway URL | 用途 |
|---|---|---|
| `staging` | `https://api-staging.synapse-network.ai` | Public preview、测试资产和接入试跑 |
-| `local` | `http://127.0.0.1:8000` | 本地 gateway 开发 |
-| `prod` | `https://api.synapse-network.ai` | 生产预设,等待官方 DNS 和 health 验证 |
解析优先级:
@@ -74,7 +89,7 @@ Provider 是 owner scope 下的供给侧角色,不是第二套根账户体系
```bash
pip install synapse-client
export SYNAPSE_ENV=staging
-export SYNAPSE_API_KEY=agt_xxx
+export SYNAPSE_AGENT_KEY=agt_xxx
```
```python
@@ -88,7 +103,7 @@ service = services[0]
result = client.invoke(
service.service_id,
{"prompt": "hello"},
- cost_usdc=float(service.price_usdc),
+ cost_usdc=str(service.price_usdc),
idempotency_key="agent-job-001",
)
@@ -113,8 +128,13 @@ npm install @synapse-network/sdk
```ts
import { SynapseClient } from "@synapse-network/sdk";
+const agentKey = process.env.SYNAPSE_AGENT_KEY;
+if (!agentKey) {
+ throw new Error("SYNAPSE_AGENT_KEY is required");
+}
+
const client = new SynapseClient({
- credential: "agt_xxx",
+ credential: agentKey,
environment: "staging",
});
@@ -127,7 +147,7 @@ const result = await client.invoke(
service.serviceId ?? service.id!,
{ prompt: "hello" },
{
- costUsdc: Number(service.pricing?.amount ?? 0),
+ costUsdc: String(service.pricing?.amount ?? "0"),
idempotencyKey: "agent-job-001",
}
);
@@ -138,6 +158,8 @@ console.log(receipt.invocationId, receipt.status, receipt.chargedUsdc);
TypeScript SDK 不会自动读取环境变量。请在你的应用中读取环境变量,然后显式传入 `environment` 或 `gatewayUrl`。
+Python 默认读取 `SYNAPSE_AGENT_KEY`。`SYNAPSE_API_KEY` 只作为 legacy 兼容别名保留,新示例统一使用 `SYNAPSE_AGENT_KEY`。
+
### LLM 按 token 计费调用
使用 `serviceKind=llm` 和 `priceModel=token_metered` 注册的 LLM 服务,需要调用 `invoke_llm()` / `invokeLlm()`。不要传 `cost_usdc` / `costUsdc`;可以传可选的 `max_cost_usdc` / `maxCostUsdc`,也可以交给 Gateway 自动冻结。V1 会拒绝 streaming,确保 Gateway 能拿到 final usage 后再扣费。
@@ -267,7 +289,7 @@ PYTHONPATH="$PWD" .venv/bin/python examples/provider_staging_onboarding.py \
使用已有 Agent Key 调用 provider service:
```bash
-export SYNAPSE_API_KEY=agt_xxx
+export SYNAPSE_AGENT_KEY=agt_xxx
PYTHONPATH="$PWD" .venv/bin/python examples/consumer_call_provider.py \
--service-id "weather_api" \
--payload-json '{"prompt":"hello"}'
diff --git a/docs/agent-map/README.md b/docs/agent-map/README.md
index c218296..b499e19 100644
--- a/docs/agent-map/README.md
+++ b/docs/agent-map/README.md
@@ -64,7 +64,7 @@ Domain 摘要。
| `sdk_runtime_client` | discovery, invoke, receipt, usage, runtime errors |
| `sdk_owner_auth` | wallet auth, credential issue/list/status/quota, owner control plane |
| `sdk_provider_lifecycle` | provider facade, provider secrets, service registration, service lifecycle, provider health |
-| `sdk_environment_config` | staging/prod/local presets, gateway URL resolution, public preview defaults |
+| `sdk_environment_config` | staging defaults, future prod switch, gateway URL resolution, public preview defaults |
| `sdk_public_docs` | README, integration guides, capability inventory, examples |
| `sdk_ci_quality_gates` | GitHub Actions, shell CI scripts, coverage gates |
| `sdk_examples_and_e2e` | examples, smoke tests, onboarding e2e plans |
diff --git a/docs/agent-map/index.json b/docs/agent-map/index.json
index 7c368c4..3522d8a 100644
--- a/docs/agent-map/index.json
+++ b/docs/agent-map/index.json
@@ -42,7 +42,8 @@
"bash scripts/ci/typescript_checks.sh"
],
"notes": [
- "Canonical runtime flow is discovery/search plus price-asserted invoke plus receipt.",
+ "Canonical runtime flow is discovery/search plus one of two invoke modes: fixed-price invoke with costUsdc/cost_usdc, or token-metered LLM invoke with maxCostUsdc/max_cost_usdc.",
+ "Public examples should use SYNAPSE_AGENT_KEY for agent runtime credentials; SYNAPSE_API_KEY is only a Python legacy fallback.",
"Do not restore old quote-first gateway calls."
]
},
@@ -108,7 +109,7 @@
"id": "sdk_environment_config",
"title": "Gateway environment configuration",
"use_when": [
- "local, staging, prod, gateway URL, environment resolver, or public preview defaults change",
+ "staging, future prod, gateway URL, environment resolver, or public preview defaults change",
"README or SECURITY guidance for staging/prod changes"
],
"primary_files": [
@@ -132,6 +133,7 @@
],
"notes": [
"Default environment is staging.",
+ "Do not reintroduce removed non-staging gateway presets or gateway setup docs.",
"Do not reintroduce the retired gateway domain from the old .network namespace.",
"Explicit gateway URL always wins over environment preset."
]
@@ -167,7 +169,8 @@
"notes": [
"Public docs should say Public Preview unless production DNS and health checks are verified.",
"Use agt_xxx or REPLACE_ME placeholders only.",
- "README and gateway docs must show Agent Key -> SynapseClient first, with owner auth -> JWT -> credential issuance under Advanced."
+ "README and gateway docs must show SYNAPSE_AGENT_KEY -> SynapseClient first, with owner auth -> JWT -> credential issuance under Advanced.",
+ "README and gateway docs must explain fixed-price API invoke versus token-metered LLM invoke before advanced owner/provider flows."
]
},
{
@@ -226,7 +229,7 @@
"bash scripts/ci/typescript_checks.sh"
],
"notes": [
- "Provider staging examples require a public HTTPS endpoint, not localhost.",
+ "Provider staging examples require a public HTTPS endpoint reachable by the staging gateway.",
"Consumer wallet-to-invoke examples default to free services unless --allow-paid is explicit.",
"E2E flows may require real staging credentials; PR CI should keep e2e out of the default gate unless explicitly configured."
]
diff --git a/docs/bugfix/typeScript/bugs.md b/docs/bugfix/typeScript/bugs.md
index e2f5c63..1409367 100644
--- a/docs/bugfix/typeScript/bugs.md
+++ b/docs/bugfix/typeScript/bugs.md
@@ -1,158 +1,11 @@
# TypeScript SDK Bug Log
-## BUG-SDK-001 — PROVIDER_KEY invalid length (63 hex chars, ethers v6 rejects)
+Historical TypeScript E2E bugs from the retired non-staging test harness have been archived out of the public SDK guidance.
-**Status:** FIXED
-**Severity:** error (blocks beforeAll setup)
-**File:** `sdk/typescript/tests/e2e/consumer.test.ts`
+Current SDK validation uses:
-### Symptom
-```
-TypeError: invalid BytesLike value (argument="value", value="0x5de4...0ecf",
-code=INVALID_ARGUMENT, version=6.16.0)
-```
+1. Unit tests in the default PR gate.
+2. Staging-gated consumer E2E tests when `RUN_STAGING_E2E=1`.
+3. Staging-gated provider E2E tests when `RUN_STAGING_PROVIDER_E2E=1`.
-### Root Cause
-`PROVIDER_KEY = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ea870594801966b8ea0ecf"` is 63 hex chars (odd length, not 32 bytes).
-
-Python `eth_account` silently zero-pads the key; ethers v6 is strict and rejects it.
-
-### Fix
-Use the correct 64-char Hardhat #2 private key:
-```
-0x5de4111afa1a4b94908f83103eb1f1706367c2e68ea870594801966b8ea0ec4f
-```
-(The `4` before the final `f` was missing in the tests.)
-
----
-
-## BUG-SDK-002 — Deposit nonce conflict (NONCE_EXPIRED) on re-runs
-
-**Status:** FIXED
-**Severity:** error (blocks beforeAll setup)
-**File:** `sdk/typescript/tests/e2e/consumer.test.ts`
-
-### Symptom
-```
-NONCE_EXPIRED: nonce has already been used
-code=-32003, message="nonce too low"
-```
-
-### Root Cause
-If Python E2E tests ran first, the owner account's on-chain nonce advanced.
-Ethers v6 uses a caching nonce manager and the previous run may have left
-the nonce tracking dirty. Submitting a fresh deposit conflicts with already-used nonces.
-
-### Fix
-Before attempting on-chain deposit, check the current gateway balance.
-If `consumerAvailableBalance >= DEPOSIT_USDC / 2` (i.e., enough balance already
-credited from a prior run), skip the blockchain deposit entirely.
-This makes the test idempotent and re-run safe.
-
----
-
-## BUG-SDK-004 — Deployer nonce conflict between ETH transfer and mint (Hardhat)
-
-**Status:** FIXED
-**Severity:** error (blocks beforeAll setup)
-**File:** `sdk/typescript/tests/e2e/new-consumer.test.ts`
-
-### Symptom
-```
-NONCE_EXPIRED: nonce has already been used
-code=-32003, message="nonce too low"
-```
-Transaction is the `mint()` call to MockUSDC (nonce 0x3d = 61).
-
-### Root Cause
-In ethers v6, when `deployerWallet.sendTransaction({value: 0.5 ETH})` is sent
-and mined (via `await ethTx.wait()`), the subsequent `usdc.mint()` call queries
-`eth_getTransactionCount(deployer, "latest")` internally. Hardhat's auto-mine
-mode can expose a brief window where the transaction has been mined but the
-`eth_getTransactionCount` response hasn't propagated, causing the next call to
-receive the stale nonce (N) instead of N+1. Both the ETH transfer and mint end
-up broadcast with the same nonce, causing the second to fail.
-
-### Fix
-Fetch the deployer's pending nonce once before the first transaction, then
-pass explicit `{ nonce: deployerNonce++ }` overrides to every deployer tx:
-```typescript
-let deployerNonce = await rpcProvider.getTransactionCount(deployerWallet.address, "pending");
-await deployerWallet.sendTransaction({..., nonce: deployerNonce++});
-await (usdc as Contract).mint(fresh, amount, { nonce: deployerNonce++ });
-```
-
-
-
-**Status:** FIXED
-**Severity:** assertion error (1 test fails)
-**File:** `sdk/typescript/tests/e2e/consumer.test.ts`
-
-### Symptom
-```
-expect(received).toBeLessThan(expected)
-Expected: < 10 (DEPOSIT_USDC constant)
-Received: 59.9934
-```
-
-### Root Cause
-Test asserts `balance < DEPOSIT_USDC (10)` but the existing gateway balance was ~60 USDC
-from previous Python E2E runs. The deposit was correctly skipped (BUG-002 fix),
-so the starting balance was 60, not 10.
-
-### Fix
-Capture `initialBalance` before invocations run and assert
-`finalBalance < initialBalance` instead of `finalBalance < DEPOSIT_USDC`.
-
----
-
-## BUG-SDK-004 — Deployer nonce conflict between ETH transfer and mint (new-consumer test)
-
-**Status:** FIXED
-**Severity:** error (NONCE_EXPIRED, blocks beforeAll)
-**File:** `sdk/typescript/tests/e2e/new-consumer.test.ts`
-
-### Symptom
-```
-NONCE_EXPIRED: nonce has already been used, code=-32003, message="nonce too low"
-```
-Transaction is the `mint()` call with nonce 0x3d = 61.
-
-### Root Cause
-After `deployerWallet.sendTransaction({ETH})` and `await ethTx.wait()`, the subsequent
-`usdc.mint()` call queries `eth_getTransactionCount(deployer, "latest")` internally.
-Hardhat auto-mine can expose a brief stale window where that query still returns N
-instead of N+1, causing both transactions to use the same nonce.
-
-### Fix
-Fetch deployer's pending nonce ONCE before first tx, then pass explicit
-`{ nonce: deployerNonce++ }` to every deployer transaction:
-```typescript
-let deployerNonce = await rpcProvider.getTransactionCount(deployerWallet.address, "pending");
-await deployerWallet.sendTransaction({..., nonce: deployerNonce++});
-await (usdc as Contract).mint(fresh, amount, { nonce: deployerNonce++ });
-```
-
----
-
-## BUG-SDK-005 — confirmDeposit wrong endpoint URL and body
-
-**Status:** FIXED
-**Severity:** error (HTTP 404 — blocks deposit confirmation)
-**File:** `sdk/typescript/src/auth.ts`
-
-### Symptom
-```
-HTTP 404: Not Found
-```
-Thrown from `SynapseAuth.confirmDeposit()` during beforeAll setup.
-
-### Root Cause
-`confirmDeposit` was calling `POST /api/v1/balance/deposit/confirm` with body
-`{ intentId, eventKey }`. The actual gateway endpoint is:
-`POST /api/v1/balance/deposit/intents/{intentId}/confirm` with body `{ eventKey, confirmations: 1 }`.
-
-### Fix
-Updated `confirmDeposit` in `auth.ts`:
-- URL: `/api/v1/balance/deposit/intents/${intentId}/confirm`
-- Body: `{ eventKey, confirmations: 1 }`
+Do not reintroduce non-staging gateway setup instructions into public SDK docs or tests.
diff --git a/docs/ops/SDK_Python_Local_Development.md b/docs/ops/SDK_Python_Local_Development.md
index 82fc972..a3d9d45 100644
--- a/docs/ops/SDK_Python_Local_Development.md
+++ b/docs/ops/SDK_Python_Local_Development.md
@@ -1,6 +1,6 @@
-# SDK Python Local Development
+# SDK Python Staging Development
-`sdk/python/` 是 Synapse 的官方 Python SDK 分发与联调目录。
+`python/` 是 SynapseNetwork 的官方 Python SDK 分发与联调目录。当前所有公开验证都指向 staging;生产环境上线后再统一切换到 prod。
## 安装
@@ -10,66 +10,44 @@
pip install synapse-client
```
-本地开发安装:
+开发安装:
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
+cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/python
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
```
-## 常用验证
-
-### Live smoke test
+## Staging Smoke Test
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
-export SYNAPSE_API_KEY="agt_xxxxx..."
-PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py --query "名人名言"
+cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/python
+export SYNAPSE_ENV=staging
+export SYNAPSE_AGENT_KEY="agt_xxxxx..."
+PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py --query "quotes"
```
如需输出可复现 curl:
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
-export SYNAPSE_API_KEY="agt_xxxxx..."
-PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py --query "名人名言" --print-curl
+PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py --query "quotes" --print-curl
```
如已知 `service_id`:
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
-export SYNAPSE_API_KEY="agt_xxxxx..."
PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py \
--service-id svc_quotes_famous_top3 \
- --cost-usdc 0.001
-```
-
-## 端到端本地联调
-
-```bash
-bash /home/alex/Documents/cliff/Synapse-Network/scripts/local/restart_gateway.sh
-
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
-export SYNAPSE_API_KEY="agt_xxxxx..."
-export SYNAPSE_GATEWAY="http://127.0.0.1:8000"
-PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py \
- --query "名人名言" \
- --text "想要放弃的时候,请给我一句关于坚持的名人名言"
+ --cost-usdc "0.001"
```
-成功时脚本会打印 `Invocation succeeded`、`request_id`、`idempotency_key` 和最终 `invocationId`。
-
## 环境变量
-1. `SYNAPSE_API_KEY`: Agent credential。
-2. `SYNAPSE_GATEWAY`: 默认 gateway 地址。
+1. `SYNAPSE_ENV=staging`
+2. `SYNAPSE_AGENT_KEY`: Agent credential。`SYNAPSE_API_KEY` 仅作为 legacy Python fallback。
## 相关文档
-1. [README.md](./README.md)
-2. [../../sdk/README.md](../../sdk/README.md)
-3. [../../06_Reference/04_Development_Testing_and_Integration_Reference.md](../../06_Reference/04_Development_Testing_and_Integration_Reference.md)
-4. [../../06_Reference/01_API_Contract_Index.md](../../06_Reference/01_API_Contract_Index.md)
+1. [README.md](../../README.md)
+2. [SDK Docs](../sdk/README.md)
diff --git a/docs/sdk/README.md b/docs/sdk/README.md
index 115bb53..294a513 100644
--- a/docs/sdk/README.md
+++ b/docs/sdk/README.md
@@ -15,7 +15,7 @@ This directory is the SDK-side source of truth for capabilities, integration gui
5. [TypeScript Provider Integration Guide](./typescript_provider_integration.md)
6. [Python Integration Guide](./python_integration.md)
7. [Python Provider Integration Guide](./python_provider_integration.md)
-8. [Python Local Development](../ops/SDK_Python_Local_Development.md)
+8. [Python Staging Development](../ops/SDK_Python_Local_Development.md)
9. [TypeScript Consumer E2E Plan](../test/consumer-e2e-plan.md)
10. [TypeScript Provider Onboarding E2E Plan](../test/typescript-provider-onboarding-e2e-plan.md)
11. [Python Consumer Cold-Start E2E Plan](../test/python-consumer-cold-start-e2e-plan.md)
@@ -37,6 +37,13 @@ Python quote-first methods `create_quote()`, `create_invocation()`, and `invoke_
LLM services use `serviceKind=llm` + `priceModel=token_metered`. Runtime code should call `invoke_llm()` / `invokeLlm()` and read `usage` plus `synapse` billing metadata. Do not pass `cost_usdc` / `costUsdc` for LLM calls; pass optional `max_cost_usdc` / `maxCostUsdc` or let Gateway compute the automatic hold. Streaming is disabled in V1.
+Consumer docs should present two invocation modes:
+
+| Mode | SDK method | Cost input |
+|---|---|---|
+| Fixed-price API | Python `invoke()` / TypeScript `invoke()` | latest discovery price as `cost_usdc` / `costUsdc` |
+| Token-metered LLM | Python `invoke_llm()` / TypeScript `invokeLlm()` | optional cap as `max_cost_usdc` / `maxCostUsdc`; never send `cost_usdc` / `costUsdc` |
+
## Staging Docs
The productized Gateway runbook lives in staging docs:
@@ -76,13 +83,13 @@ Provider publishing is a separate owner-authenticated flow:
Default environment is public preview/staging:
-- `local`: `http://127.0.0.1:8000`
- `staging`: `https://api-staging.synapse-network.ai`
-- `prod`: `https://api.synapse-network.ai`, only for real funds after official production DNS and `/health` verification.
+
+Production launch will switch public examples and tests from `staging` to `prod`.
Python:
-- `api_key`: explicit parameter first, then `SYNAPSE_API_KEY`.
+- `api_key`: explicit parameter first, then `SYNAPSE_AGENT_KEY`, then legacy `SYNAPSE_API_KEY`.
- `gateway_url`: explicit parameter first, then `SYNAPSE_GATEWAY`.
- `environment`: explicit parameter first, then `SYNAPSE_ENV`, then `staging`.
- `AgentWallet.connect()` no longer uses demo credential fallback; missing real credentials fail.
@@ -106,10 +113,10 @@ Runtime calls should include:
### `api_key is required`
-No `api_key` was passed and `SYNAPSE_API_KEY` is not set. New users should issue an agent credential first, then pass the returned token to `SynapseClient`.
+No `api_key` was passed and `SYNAPSE_AGENT_KEY` is not set. New users should issue an agent credential first, then pass the returned token to `SynapseClient`. `SYNAPSE_API_KEY` remains a legacy fallback for older Python users, but new docs and examples should use `SYNAPSE_AGENT_KEY`.
```bash
-export SYNAPSE_API_KEY='agt_xxx_your_real_key'
+export SYNAPSE_AGENT_KEY='agt_xxx_your_real_key'
```
### Discovery returns 0 results
@@ -132,7 +139,7 @@ The SDK maps `402` to balance, budget, or credential credit limit errors. Check
```bash
cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/python
PYTHONPATH="$PWD" .venv/bin/python -m pytest synapse_client/test/test_client_unit.py -q
-export SYNAPSE_API_KEY='agt_xxx_your_real_key'
+export SYNAPSE_AGENT_KEY='agt_xxx_your_real_key'
PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py --query 'quotes'
```
diff --git a/docs/sdk/README.zh-CN.md b/docs/sdk/README.zh-CN.md
index 13bf6f6..d0d9874 100644
--- a/docs/sdk/README.zh-CN.md
+++ b/docs/sdk/README.zh-CN.md
@@ -15,7 +15,7 @@
5. [TypeScript Provider Integration Guide](./typescript_provider_integration.md)
6. [Python Integration Guide](./python_integration.md)
7. [Python Provider Integration Guide](./python_provider_integration.md)
-8. [Python Local Development](../ops/SDK_Python_Local_Development.md)
+8. [Python Staging Development](../ops/SDK_Python_Local_Development.md)
9. [TypeScript Consumer E2E Plan](../test/consumer-e2e-plan.md)
10. [TypeScript Provider Onboarding E2E Plan](../test/typescript-provider-onboarding-e2e-plan.md)
11. [Python Consumer Cold-Start E2E Plan](../test/python-consumer-cold-start-e2e-plan.md)
@@ -37,6 +37,13 @@ Python 旧的 quote-first 方法 `create_quote()`、`create_invocation()`、`inv
LLM 服务使用 `serviceKind=llm` + `priceModel=token_metered`。Runtime 代码应调用 `invoke_llm()` / `invokeLlm()`,并读取返回里的 `usage` 与 `synapse` 计费元数据。LLM 调用不要传 `cost_usdc` / `costUsdc`;可以传可选的 `max_cost_usdc` / `maxCostUsdc`,也可以交给 Gateway 自动冻结。V1 禁用 streaming。
+Consumer 文档应统一呈现两种调用模式:
+
+| 模式 | SDK 方法 | 费用输入 |
+|---|---|---|
+| Fixed-price API | Python `invoke()` / TypeScript `invoke()` | 最新 discovery price:`cost_usdc` / `costUsdc` |
+| Token-metered LLM | Python `invoke_llm()` / TypeScript `invokeLlm()` | 可选上限:`max_cost_usdc` / `maxCostUsdc`;不要发送 `cost_usdc` / `costUsdc` |
+
## Staging 产品文档
Gateway 的产品化 runbook 以 staging docs 为准:
@@ -76,13 +83,13 @@ Provider publishing 是另一条 owner-authenticated flow:
默认环境是 public preview/staging:
-- `local`: `http://127.0.0.1:8000`,用于本地 gateway 开发。
- `staging`: `https://api-staging.synapse-network.ai`,用于 public preview、测试资产和接入试跑。
-- `prod`: `https://api.synapse-network.ai`,需等官方 production DNS 和 `/health` 验证后再用于真实资金流。
+
+生产环境上线后,再把公开示例和测试从 `staging` 统一切到 `prod`。
Python:
-- `api_key`: 显式参数优先,其次 `SYNAPSE_API_KEY`。
+- `api_key`: 显式参数优先,其次 `SYNAPSE_AGENT_KEY`,最后是 legacy `SYNAPSE_API_KEY`。
- `gateway_url`: 显式参数优先,其次 `SYNAPSE_GATEWAY`。
- `environment`: 显式参数优先,其次 `SYNAPSE_ENV`,最后 `staging`。
- `AgentWallet.connect()` 不再使用 demo credential fallback;缺少真实 credential 会失败。
@@ -106,10 +113,10 @@ TypeScript:
### `api_key is required`
-没有传 `api_key`,也没有设置 `SYNAPSE_API_KEY`。新用户应先签发 agent credential,再把返回的 token 交给 `SynapseClient`。
+没有传 `api_key`,也没有设置 `SYNAPSE_AGENT_KEY`。新用户应先签发 agent credential,再把返回的 token 交给 `SynapseClient`。`SYNAPSE_API_KEY` 仅作为旧 Python 用户的 legacy fallback 保留,新文档和示例统一使用 `SYNAPSE_AGENT_KEY`。
```bash
-export SYNAPSE_API_KEY='agt_xxx_your_real_key'
+export SYNAPSE_AGENT_KEY='agt_xxx_your_real_key'
```
### Discovery 返回 0 个结果
@@ -132,7 +139,7 @@ SDK 会把 `402` 映射到余额、预算或 credential credit limit 相关异
```bash
cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/python
PYTHONPATH="$PWD" .venv/bin/python -m pytest synapse_client/test/test_client_unit.py -q
-export SYNAPSE_API_KEY='agt_xxx_your_real_key'
+export SYNAPSE_AGENT_KEY='agt_xxx_your_real_key'
PYTHONPATH="$PWD" .venv/bin/python examples/smoke_test.py --query '名人名言'
```
diff --git a/docs/sdk/capability_inventory.md b/docs/sdk/capability_inventory.md
index c396cc4..0e304bd 100644
--- a/docs/sdk/capability_inventory.md
+++ b/docs/sdk/capability_inventory.md
@@ -8,8 +8,11 @@ Consumer runtime is:
1. owner auth / credential issue
2. agent discovery/search
-3. `POST /api/v1/agent/invoke`
-4. `GET /api/v1/agent/invocations/{id}`
+3. fixed-price API invoke through `POST /api/v1/agent/invoke` with latest discovery price as `cost_usdc` / `costUsdc`
+4. token-metered LLM invoke through `invoke_llm()` / `invokeLlm()` with optional `max_cost_usdc` / `maxCostUsdc`
+5. `GET /api/v1/agent/invocations/{id}`
+
+Agent runtime examples use `SYNAPSE_AGENT_KEY=agt_xxx`. Python keeps `SYNAPSE_API_KEY` only as a legacy fallback.
The old quote-first flow is not a current SDK main path. Python keeps deprecated compatibility methods that raise a clear error instead of calling removed endpoints.
@@ -34,17 +37,18 @@ Supported:
13. check credential status alias
14. ensure credential
15. discovery/search
-16. invoke
+16. fixed-price invoke
17. invoke with rediscovery on `PRICE_MISMATCH`
-18. invocation receipt
-19. gateway health check
-20. empty discovery diagnostics
-21. owner profile
-22. usage logs
-23. voucher redeem
-24. finance audit logs
-25. finance risk overview
-26. spending limit through `AgentWallet`
+18. token-metered LLM invoke
+19. invocation receipt
+20. gateway health check
+21. empty discovery diagnostics
+22. owner profile
+23. usage logs
+24. voucher redeem
+25. finance audit logs
+26. finance risk overview
+27. spending limit through `AgentWallet`
Deprecated:
@@ -93,17 +97,18 @@ Supported:
9. credential quota update
10. credential audit logs
11. discovery/search
-12. invoke
+12. fixed-price invoke
13. invoke with rediscovery on `PRICE_MISMATCH`
-14. invocation receipt
-15. gateway health check
-16. empty discovery diagnostics
-17. owner profile
-18. usage logs
-19. voucher redeem
-20. finance audit logs
-21. finance risk overview
-22. spending limit through issued credential settings
+14. token-metered LLM invoke
+15. invocation receipt
+16. gateway health check
+17. empty discovery diagnostics
+18. owner profile
+19. usage logs
+20. voucher redeem
+21. finance audit logs
+22. finance risk overview
+23. spending limit through issued credential settings
Discovery/search sends the current gateway request body: `query`, `tags`, `page`, `pageSize`, and `sort`. `limit/offset` remain SDK convenience inputs and are converted to `page/pageSize`.
diff --git a/docs/sdk/python_integration.md b/docs/sdk/python_integration.md
index 1a9c3c4..9a287e1 100644
--- a/docs/sdk/python_integration.md
+++ b/docs/sdk/python_integration.md
@@ -11,7 +11,7 @@ Consumer runtime 主链固定为:
3. discovery/search
4. fixed API: `invoke(service_id, payload, cost_usdc=...)`
5. LLM service: `invoke_llm(service_id, payload, max_cost_usdc=...)`
-5. receipt 查询
+6. receipt 查询
旧的 quote-first 方法 `create_quote()`、`create_invocation()`、`invoke_service()` 已废弃,不再访问旧 endpoint。调用这些方法会直接提示改用 discovery/search + price-asserted invoke。
@@ -36,7 +36,8 @@ python -m pip install -e ".[dev]"
`SynapseClient` 读取顺序:
1. `api_key` 显式参数
-2. `SYNAPSE_API_KEY`
+2. `SYNAPSE_AGENT_KEY`
+3. legacy fallback: `SYNAPSE_API_KEY`
`gateway_url` 读取顺序:
@@ -51,15 +52,15 @@ python -m pip install -e ".[dev]"
环境 preset:
-1. `local`: `http://127.0.0.1:8000`
-2. `staging`: `https://api-staging.synapse-network.ai`
-3. `prod`: `https://api.synapse-network.ai`,需等官方 production DNS 和 `/health` 验证后再用于真实资金流
+1. `staging`: `https://api-staging.synapse-network.ai`
+
+生产环境上线后,公开示例和测试再统一切换到 `prod`。
`AgentWallet.connect()` 不再使用 `demo_key` fallback。没有真实 credential 时会失败。
## Agent-first 接入链路
-Fresh setup 不应从 `SYNAPSE_API_KEY` 开始。`SYNAPSE_API_KEY` 是 owner wallet 签发 agent credential 之后得到的 runtime token。
+Fresh setup 不应从硬编码 credential 开始。`SYNAPSE_AGENT_KEY` 是 owner wallet 签发 agent credential 之后得到的 runtime token。`SYNAPSE_API_KEY` 只作为 legacy alias 保留。
固定顺序:
@@ -121,7 +122,7 @@ service = services[0]
result = client.invoke(
service.service_id,
{"prompt": "hello"},
- cost_usdc=float(service.price_usdc),
+ cost_usdc=str(service.price_usdc),
idempotency_key="job-001",
poll_timeout_sec=60,
)
@@ -163,7 +164,7 @@ print(result.synapse.charged_usdc, result.synapse.released_usdc)
示例脚本位于 `python/examples`:
1. `provider_staging_onboarding.py`:使用 `SynapseAuth` + `auth.provider()` 在 staging 注册 provider service。
-2. `consumer_call_provider.py`:使用已有 `SYNAPSE_API_KEY=agt_xxx` 调用 provider service。
+2. `consumer_call_provider.py`:使用已有 `SYNAPSE_AGENT_KEY=agt_xxx` 调用 provider service。
3. `consumer_wallet_to_invoke.py`:创建新的 staging wallet,签发 credential,再调用免费服务。
示例命令:
@@ -178,7 +179,7 @@ PYTHONPATH="$PWD" .venv/bin/python examples/provider_staging_onboarding.py \
--description "Returns weather data for a city." \
--price-usdc 0
-export SYNAPSE_API_KEY=agt_xxx
+export SYNAPSE_AGENT_KEY=agt_xxx
PYTHONPATH="$PWD" .venv/bin/python examples/consumer_call_provider.py \
--service-id "weather_api" \
--payload-json '{"prompt":"hello"}'
diff --git a/docs/sdk/python_provider_integration.md b/docs/sdk/python_provider_integration.md
index 1091d4f..6a3dc29 100644
--- a/docs/sdk/python_provider_integration.md
+++ b/docs/sdk/python_provider_integration.md
@@ -54,7 +54,7 @@ PYTHONPATH="$PWD" .venv/bin/python examples/provider_staging_onboarding.py \
--price-usdc 0
```
-注意:staging gateway 必须能访问 provider endpoint,所以 `--endpoint-url` 应使用公网 HTTPS URL,不能使用 `localhost`。
+注意:staging gateway 必须能访问 provider endpoint,所以 `--endpoint-url` 应使用公网 HTTPS URL。
```python
from synapse_client import SynapseAuth
diff --git a/docs/sdk/typescript_integration.md b/docs/sdk/typescript_integration.md
index 32a703b..fdcbf01 100644
--- a/docs/sdk/typescript_integration.md
+++ b/docs/sdk/typescript_integration.md
@@ -11,7 +11,7 @@ Consumer runtime 主链固定为:
3. discovery/search
4. fixed API: `invoke(serviceId, payload, { costUsdc })`
5. LLM service: `invokeLlm(serviceId, payload, { maxCostUsdc })`
-5. receipt 查询
+6. receipt 查询
TypeScript SDK 不暴露 quote public API。当前 gateway 的正式运行时入口是单步 price-asserted invoke。
@@ -48,12 +48,19 @@ const client = new SynapseClient({
SDK 库内部不隐式读取环境变量;Node、browser、worker runtime 都由应用层决定如何传入配置。可用环境 preset:
-1. `local`: `http://127.0.0.1:8000`
-2. `staging`: `https://api-staging.synapse-network.ai`
-3. `prod`: `https://api.synapse-network.ai`,需等官方 production DNS 和 `/health` 验证后再用于真实资金流
+1. `staging`: `https://api-staging.synapse-network.ai`
+
+生产环境上线后,公开示例和测试再统一切换到 `prod`。
显式 `gatewayUrl` 会覆盖 `environment`。
+公开示例统一使用 `SYNAPSE_AGENT_KEY`:
+
+```ts
+const agentKey = process.env.SYNAPSE_AGENT_KEY;
+if (!agentKey) throw new Error("SYNAPSE_AGENT_KEY is required");
+```
+
## Agent-first 接入链路
Fresh setup 不应从 `credential: "agt_xxx"` 开始。agent credential 必须先由 owner wallet 通过 `SynapseAuth` 签发,然后再交给 `SynapseClient`。
@@ -150,7 +157,7 @@ const result = await client.invoke(
serviceId,
{ prompt: "hello" },
{
- costUsdc: Number(service.pricing?.amount ?? 0),
+ costUsdc: String(service.pricing?.amount ?? "0"),
idempotencyKey: "job-001",
pollTimeoutMs: 60_000,
}
@@ -166,7 +173,7 @@ const result = await client.invoke(
process.env.SYNAPSE_SERVICE_ID!,
{ prompt: "hello" },
{
- costUsdc: Number(process.env.SYNAPSE_SERVICE_PRICE_USDC!),
+ costUsdc: process.env.SYNAPSE_SERVICE_PRICE_USDC!,
idempotencyKey: "job-known-service-001",
}
);
diff --git a/docs/test/consumer-e2e-plan.md b/docs/test/consumer-e2e-plan.md
index 48e5176..b0bb877 100644
--- a/docs/test/consumer-e2e-plan.md
+++ b/docs/test/consumer-e2e-plan.md
@@ -1,126 +1,33 @@
-# TypeScript SDK — Consumer E2E 自动化测试方案
+# TypeScript SDK — Staging Consumer E2E Plan
-> **目标**:从零创建一个空钱包,全程通过 TypeScript SDK 完成 Consumer 完整链路:
-> 链下钱包创建 → 链上充值 → 网关认证 → Credential 颁发 → 服务发现 → 单步调用 → 余额验证
+## Goal
----
+Validate that an existing staging Agent Key can discover services, run a fixed-price API invoke, optionally run a token-metered LLM invoke, and read receipts against the staging gateway.
-## 1. 测试范围
+## Required Environment
-| # | 阶段 | 验证点 |
-|---|------|--------|
-| 0 | 环境检查 | Hardhat 节点可达、Gateway 健康、前端 3000/3002 可访问 |
-| 1 | 创建钱包 | 使用 `ethers.Wallet.createRandom()` 生成全新 EOA 地址 |
-| 2 | 链上 ETH 注入 | Deployer 向新钱包转 0.5 ETH,验证 `getBalance()` > 0 |
-| 3 | 链上 USDC Mint | Deployer 调用 `MockUSDC.mint()` 向新钱包注 10 USDC,验证 `balanceOf()` = 10e18 |
-| 4 | 链上充值 | 新钱包 `approve(SynapseCore, amount)` + `SynapseCore.deposit(amount)` |
-| 5 | 网关认证 | `SynapseAuth.fromWallet()` → challenge → EIP-191 签名 → JWT,缓存验证 |
-| 6 | 充值登记 | `registerDepositIntent(txHash, amount)` + `confirmDeposit(intentId, eventKey)` |
-| 7 | 余额确认 | `getBalance().consumerAvailableBalance >= DEPOSIT_USDC` |
-| 8 | Credential 颁发 | `issueCredential({ name, maxCalls, creditLimit })` → agentToken 不为空 |
-| 9 | 服务发现 | `SynapseClient.discover()` 返回列表;能找到本次注册的测试服务 |
-| 10 | 通过 discovery 选定 serviceId 调用 | `client.invoke(discoveredServiceId, payload, { costUsdc })` → status SUCCEEDED/SETTLED |
-| 11 | 通过已知 serviceId 直接调用 | `client.invoke(serviceId, payload, { costUsdc })` → status SUCCEEDED/SETTLED |
-| 12 | 费用扣除 | `invocationResult.chargedUsdc > 0` |
-| 13 | 调用凭证 | `client.getInvocation(invocationId)` → 状态可查 |
-| 14 | 结算后余额 | `getBalance().consumerAvailableBalance < balance_before_invoke` |
+| Variable | Purpose |
+|---|---|
+| `RUN_STAGING_E2E=1` | Opt in to live staging tests |
+| `SYNAPSE_AGENT_KEY=agt_xxx` | Agent runtime credential |
+| `SYNAPSE_STAGING_SERVICE_ID` | Fixed-price staging service ID |
+| `SYNAPSE_STAGING_SERVICE_PRICE_USDC` | Latest discovery price for that service |
+| `SYNAPSE_STAGING_LLM_SERVICE_ID` | Optional token-metered LLM service ID |
+| `SYNAPSE_STAGING_LLM_MAX_COST_USDC` | Optional LLM spend cap |
----
+## Flow
-## 2. 服务依赖
+1. Create `SynapseClient({ credential, environment: "staging" })`.
+2. Call `discover()` to confirm staging discovery responds.
+3. Call fixed-price `invoke(serviceId, payload, { costUsdc })`.
+4. Read the receipt with `getInvocation(invocationId)`.
+5. If LLM env is present, call `invokeLlm(serviceId, payload, { maxCostUsdc })`.
-```
-Hardhat local node http://127.0.0.1:8545 (npx hardhat node)
-Gateway http://127.0.0.1:8000 (sh scripts/local/restart_gateway.sh)
-Frontend (可选) http://localhost:3000 (yarn dev in apps/frontend)
-Mock Provider http://127.0.0.1:9299 (测试内部 Node.js HttpServer 自启动)
-```
-
-合约地址来自 `apps/frontend/src/contract-config.json`(由 Hardhat 部署脚本写入):
-- `MockUSDC` — ERC-20,有 `mint(address, uint256)` 方法,仅 Deployer 可调用
-- `SynapseCore` — `deposit(uint256)` 接收 USDC,调用前需 approve
-
----
-
-## 3. 关键角色
-
-| 角色 | 私钥来源 | 作用 |
-|------|----------|------|
-| Deployer | Hardhat #0 | 拥有 MockUSDC mint 权限;向新钱包转 ETH |
-| Fresh Consumer | `Wallet.createRandom()` | 本次测试主角 |
-| Provider | Hardhat #2 | 注册测试服务(供发现和调用使用) |
-
----
-
-## 4. 测试流程图
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ beforeAll (一次性 setup) │
-│ │
-│ [deployer] ──ETH 0.5──► [fresh wallet] │
-│ [deployer] ──mint 10 USDC──► [fresh wallet] │
-│ [fresh wallet] ──approve──► SynapseCore │
-│ [fresh wallet] ──deposit──► SynapseCore ──txHash──► │
-│ │
-│ [fresh wallet] ──auth──► Gateway JWT │
-│ registerDepositIntent(txHash) ──► confirmDeposit(intentId) │
-│ │
-│ [provider] ──auth──► registerService(mockUrl) ──► serviceId │
-└─────────────────────────────────────────────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────────┐
-│ Tests │
-│ │
-│ 1. Authentication (JWT + cache) │
-│ 2. Balance (≥ DEPOSIT_USDC after confirm) │
-│ 3. issueCredential → agentToken │
-│ 4. client.discover() → list non-empty; includes test service │
-│ 5. client.invoke(discoveredServiceId, { costUsdc }) → OK │
-│ 6. client.invoke(serviceId, { costUsdc }) direct path → OK │
-│ 7. client.getInvocation(id) → receipt by ID │
-│ 8. balance after < balance before invoke │
-└─────────────────────────────────────────────────────────────────┘
-```
-
----
-
-## 5. 验收标准
-
-- 所有 consumer E2E Jest test case 通过(`PASS`)
-- `chargedUsdc > 0`(实际结算发生)
-- `consumerAvailableBalance` 全程正确递减
-- discovery 选中的 `serviceId` 与已注册服务一致
-- 已知 `serviceId` 直调路径同样可用
-- 整体运行时间 < 120 秒
-
----
-
-## 6. 测试文件位置
-
-| 文件 | 说明 |
-|------|------|
-| `sdk/typescript/tests/e2e/new-consumer.test.ts` | 主测试文件 |
-| `docs/test/sdk/consumer-e2e-plan.md` | 本文档 |
-| `docs/bugfix/sdk/bugs.md` | Bug 记录 |
-
----
-
-## 7. 运行命令
+## Run
```bash
-cd sdk/typescript
-npm run test:new-consumer
-# 等价于: npx jest tests/e2e/new-consumer.test.ts --verbose
+cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/typescript
+npm run test:e2e
```
----
-
-## 8. 环境变量(可选覆盖)
-
-| 变量 | 默认值 | 说明 |
-|------|--------|------|
-| `SYNAPSE_GATEWAY` | `http://127.0.0.1:8000` | Gateway URL |
-| `RPC_URL` | `http://127.0.0.1:8545` | Hardhat JSON-RPC |
-| `DEPOSIT_USDC` | `10` | 充值金额(USDC) |
+The test suite is skipped unless `RUN_STAGING_E2E=1` is set. PR CI keeps staging E2E out of the default gate.
diff --git a/docs/test/python-consumer-cold-start-e2e-plan.md b/docs/test/python-consumer-cold-start-e2e-plan.md
index 8d6b42c..14eddd5 100644
--- a/docs/test/python-consumer-cold-start-e2e-plan.md
+++ b/docs/test/python-consumer-cold-start-e2e-plan.md
@@ -1,85 +1,32 @@
-# Python SDK — Consumer Cold-Start E2E 自动化测试方案
+# Python SDK — Staging Consumer E2E Plan
-> 目标:验证 Python SDK 能从 **新建钱包** 开始,完整走通
-> `钱包创建 -> 链上充值 -> 网关登录 -> 确认入账 -> 创建 agent -> 服务发现 -> 服务调用 -> 余额减少`
+## Goal
----
+Validate that an existing staging Agent Key can invoke staging services through the Python SDK. This replaces the old cold-start chain flow; funding, wallet setup, and credential issuance are handled before the test.
-## 1. 测试范围
+## Required Environment
-| # | 阶段 | 验证点 |
-|---|---|---|
-| 1 | 创建钱包 | `Account.create()` 生成全新 EOA |
-| 2 | ETH 注入 | Deployer 给新钱包转 0.5 ETH 作为 gas |
-| 3 | USDC Mint | Deployer 调 `MockUSDC.mint()` 给新钱包注入 10 USDC |
-| 4 | 链上充值 | 新钱包 `approve + deposit` 到 `SynapseCore` |
-| 5 | 登录 | `SynapseAuth.from_private_key()` 完成 challenge/sign/verify |
-| 6 | JWT 缓存 | 连续 `get_token()` 返回同一 token |
-| 7 | 充值登记 | `register_deposit_intent(txHash, amount)` |
-| 8 | 充值确认 | `confirm_deposit(intentId, eventKey)` |
-| 9 | 余额生效 | `consumerAvailableBalance >= 9.9` |
-| 10 | Provider 注册服务 | 通过 provider wallet 注册 mock 服务 |
-| 11 | 创建 agent | `issue_credential()` 成功,返回 agent token |
-| 12 | 列举 credential | `list_credentials()` 能找到刚创建的 agent |
-| 13 | 服务发现 | `client.discover()` 返回服务列表,包含刚注册服务 |
-| 14 | 调用 | `client.invoke(serviceId, payload, cost_usdc=...)` 返回 `SUCCEEDED/SETTLED` |
-| 15 | Receipt | `client.get_invocation()` 能按 invocation id 取回状态 |
-| 16 | 余额扣减 | 调用后余额小于调用前余额 |
+| Variable | Purpose |
+|---|---|
+| `RUN_STAGING_E2E=1` | Opt in to live staging tests |
+| `SYNAPSE_AGENT_KEY=agt_xxx` | Agent runtime credential |
+| `SYNAPSE_STAGING_SERVICE_ID` | Fixed-price staging service ID |
+| `SYNAPSE_STAGING_SERVICE_PRICE_USDC` | Latest discovery price for that service |
+| `SYNAPSE_STAGING_LLM_SERVICE_ID` | Optional token-metered LLM service ID |
+| `SYNAPSE_STAGING_LLM_MAX_COST_USDC` | Optional LLM spend cap |
----
+## Flow
-## 2. 依赖环境
+1. Create `SynapseClient(api_key=..., environment="staging")`.
+2. Call fixed-price `invoke(service_id, payload, cost_usdc=...)`.
+3. Read the receipt with `get_invocation_receipt(invocation_id)`.
+4. If LLM env is present, call `invoke_llm(service_id, payload, max_cost_usdc=...)`.
-| 组件 | 地址 | 说明 |
-|---|---|---|
-| Hardhat node | `http://127.0.0.1:8545` | 本地链 |
-| Gateway | `http://127.0.0.1:8000` | Synapse gateway |
-| Mock Provider | `http://127.0.0.1:9399` | 测试内启动的 HTTP server |
-
-合约配置来自:
-
-1. `/home/alex/Documents/cliff/Synapse-Network/services/user-front/src/contract-config.json`
-2. `/home/alex/Documents/cliff/Synapse-Network/services/user-front/src/MockUSDCABI.json`
-3. `/home/alex/Documents/cliff/Synapse-Network/services/user-front/src/SynapseCoreABI.json`
-
----
-
-## 3. 关键角色
-
-| 角色 | 来源 | 作用 |
-|---|---|---|
-| Deployer | Hardhat #0 | 转 ETH、mint USDC |
-| Provider | Hardhat #2 | 注册测试服务 |
-| Fresh Consumer | `eth_account.Account.create()` | 本次测试主角 |
-
----
-
-## 4. 测试文件
-
-1. `python/synapse_client/test/test_auth_unit.py`
-2. `python/synapse_client/test/test_client_unit.py`
-3. `python/synapse_client/test/test_consumer_e2e.py`
-
----
-
-## 5. 运行命令
+## Run
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
-python3.11 -m venv .venv
-source .venv/bin/activate
-python -m pip install -e ".[dev]"
-
-PYTHONPATH="$PWD" .venv/bin/python -m pytest synapse_client/test/test_auth_unit.py synapse_client/test/test_client_unit.py -q
+cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/python
PYTHONPATH="$PWD" .venv/bin/python -m pytest synapse_client/test/test_consumer_e2e.py -q -s
```
----
-
-## 6. 验收标准
-
-1. 全部 unit test 通过
-2. Python 冷启动 e2e 通过
-3. `charged_usdc > 0`
-4. `consumer_available_balance` 在调用后下降
-5. 测试总时长稳定在本地 30 秒内
+The test suite is skipped unless `RUN_STAGING_E2E=1` is set. PR CI keeps staging E2E out of the default gate.
diff --git a/docs/test/python-provider-onboarding-e2e-plan.md b/docs/test/python-provider-onboarding-e2e-plan.md
index cf644ae..1456632 100644
--- a/docs/test/python-provider-onboarding-e2e-plan.md
+++ b/docs/test/python-provider-onboarding-e2e-plan.md
@@ -1,71 +1,29 @@
-# Python SDK — Provider Onboarding E2E 自动化测试方案
+# Python SDK — Staging Provider E2E Plan
-> 目标:验证 Python SDK 能从 **新钱包 Provider 冷启动** 开始,完整走通
-> `创建钱包 -> 登录 -> 创建 provider credentials -> 注册服务 -> 读取注册状态`
+## Goal
----
+Validate provider onboarding against staging with a real provider wallet and a public HTTPS provider endpoint.
-## 1. 测试范围
+## Required Environment
-| # | 阶段 | 验证点 |
-|---|---|---|
-| 1 | 创建钱包 | `Account.create()` 生成全新 Provider EOA |
-| 2 | 登录 | `SynapseAuth.from_private_key()` 完成 challenge/sign/verify |
-| 3 | JWT 缓存 | `get_token()` 返回稳定 token |
-| 4 | 创建 provider secret | `issue_provider_secret()` 成功返回 `secret.id` 与 `secretKey` |
-| 5 | 查询 provider secret | `list_provider_secrets()` 能找到刚签发的 secret |
-| 6 | 注册服务 | `register_provider_service()` 成功返回 `serviceId` |
-| 7 | 查询 owner service list | `list_provider_services()` 包含刚注册服务 |
-| 8 | 查询服务状态 | `get_provider_service_status()` 返回 lifecycle + runtime 状态 |
+| Variable | Purpose |
+|---|---|
+| `RUN_STAGING_PROVIDER_E2E=1` | Opt in to live staging provider tests |
+| `SYNAPSE_PROVIDER_PRIVATE_KEY` | Provider owner private key for staging |
+| `SYNAPSE_PROVIDER_ENDPOINT_URL` | Public HTTPS endpoint reachable by staging |
----
+## Flow
-## 2. 依赖环境
+1. Authenticate with `SynapseAuth.from_private_key(..., environment="staging")`.
+2. Issue a provider secret.
+3. Register a provider service with the public HTTPS endpoint.
+4. List services and read service status.
-| 组件 | 地址 | 说明 |
-|---|---|---|
-| Gateway | `http://127.0.0.1:8000` | Synapse gateway |
-| Mock Provider | `http://127.0.0.1:9499` | 测试内启动的 HTTP server |
-
-这个 Provider E2E 不依赖链上充值,因此比 consumer 链更轻、更快。
-
----
-
-## 3. 关键角色
-
-| 角色 | 来源 | 作用 |
-|---|---|---|
-| Fresh Provider | `eth_account.Account.create()` | 本次测试主角 |
-| Mock Provider Server | test fixture | 对 `/health` 和 invoke 返回 200 |
-
----
-
-## 4. 测试文件
-
-1. `python/synapse_client/test/test_auth_unit.py`
-2. `python/synapse_client/test/test_provider_e2e.py`
-
----
-
-## 5. 运行命令
+## Run
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/python
-python3.11 -m venv .venv
-source .venv/bin/activate
-python -m pip install -e ".[dev]"
-
-PYTHONPATH="$PWD" .venv/bin/python -m pytest synapse_client/test/test_auth_unit.py -q
+cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/python
PYTHONPATH="$PWD" .venv/bin/python -m pytest synapse_client/test/test_provider_e2e.py -q -s
```
----
-
-## 6. 验收标准
-
-1. unit test 通过
-2. provider onboarding e2e 通过
-3. fresh wallet 能拿到 JWT
-4. provider secret 创建成功
-5. service register 成功返回 `serviceId`
-6. `get_provider_service_status()` 能查到刚注册服务的状态
+The test suite is skipped unless `RUN_STAGING_PROVIDER_E2E=1` is set.
diff --git a/docs/test/typescript-provider-onboarding-e2e-plan.md b/docs/test/typescript-provider-onboarding-e2e-plan.md
index 6193e2d..3d4cc52 100644
--- a/docs/test/typescript-provider-onboarding-e2e-plan.md
+++ b/docs/test/typescript-provider-onboarding-e2e-plan.md
@@ -1,67 +1,29 @@
-# TypeScript SDK — Provider Onboarding E2E 自动化测试方案
+# TypeScript SDK — Staging Provider E2E Plan
-> 目标:验证 TypeScript SDK 能从 **新钱包 Provider 冷启动** 开始,完整走通
-> `创建钱包 -> 登录 -> 创建 provider credentials -> 注册服务 -> 读取注册状态`
+## Goal
----
+Validate provider onboarding against staging with a real provider wallet and a public HTTPS provider endpoint.
-## 1. 测试范围
+## Required Environment
-| # | 阶段 | 验证点 |
-|---|---|---|
-| 1 | 创建钱包 | `Wallet.createRandom()` 生成全新 Provider EOA |
-| 2 | 登录 | `SynapseAuth.fromWallet()` 完成 challenge/sign/verify |
-| 3 | JWT 缓存 | `getToken()` 返回稳定 token |
-| 4 | 创建 provider secret | `issueProviderSecret()` 成功返回 `secret.id` 与 `secretKey` |
-| 5 | 查询 provider secret | `listProviderSecrets()` 能找到刚签发的 secret |
-| 6 | 注册服务 | `registerProviderService()` 成功返回 `serviceId` |
-| 7 | 查询 owner service list | `listProviderServices()` 包含刚注册服务 |
-| 8 | 查询服务状态 | `getProviderServiceStatus()` 返回 lifecycle + runtime 状态 |
+| Variable | Purpose |
+|---|---|
+| `RUN_STAGING_PROVIDER_E2E=1` | Opt in to live staging provider tests |
+| `SYNAPSE_PROVIDER_PRIVATE_KEY` | Provider owner private key for staging |
+| `SYNAPSE_PROVIDER_ENDPOINT_URL` | Public HTTPS endpoint reachable by staging |
----
+## Flow
-## 2. 依赖环境
+1. Authenticate with `SynapseAuth.fromWallet(wallet, { environment: "staging" })`.
+2. Issue a provider secret.
+3. Register a provider service with the public HTTPS endpoint.
+4. List services and read service status.
-| 组件 | 地址 | 说明 |
-|---|---|---|
-| Gateway | `http://127.0.0.1:8000` | Synapse gateway |
-| Mock Provider | `http://127.0.0.1:9498` | 测试内启动的 HTTP server |
-
-这个 Provider E2E 不依赖链上充值,因此执行速度比 consumer 链更快。
-
----
-
-## 3. 关键角色
-
-| 角色 | 来源 | 作用 |
-|---|---|---|
-| Fresh Provider | `ethers.Wallet.createRandom()` | 本次测试主角 |
-| Mock Provider Server | test fixture | 对 `/health` 与 invoke 返回 200 |
-
----
-
-## 4. 测试文件
-
-1. `typescript/tests/e2e/provider.test.ts`
-
----
-
-## 5. 运行命令
+## Run
```bash
-cd /home/alex/Documents/cliff/Synapse-Network-Sdk/typescript
-npm install
-npm run lint
+cd /Users/cliff/workspace/agent/Synapse-Network-Sdk/typescript
npm run test:provider
```
----
-
-## 6. 验收标准
-
-1. TypeScript 编译通过
-2. provider onboarding e2e 通过
-3. fresh wallet 能拿到 JWT
-4. provider secret 创建成功
-5. service register 成功返回 `serviceId`
-6. `getProviderServiceStatus()` 能查到刚注册服务的状态
+The test suite is skipped unless `RUN_STAGING_PROVIDER_E2E=1` is set.
diff --git a/llm-instructions.md b/llm-instructions.md
index edf2dbc..701e334 100644
--- a/llm-instructions.md
+++ b/llm-instructions.md
@@ -10,12 +10,13 @@ CRITICAL RULES FOR AI AGENTS:
2. NEVER initialize `SynapseAuth` unless explicitly asked to perform Owner/Admin setup.
3. NEVER ask the user for their wallet private keys.
4. You MUST use an Agent Key (format: `agt_xxx`).
-5. ALWAYS call `.search()` to discover the service before invoking.
-6. ALWAYS extract `price_usdc` from the search result and pass it to the invoke method.
-7. ALWAYS provide an `idempotency_key`.
-8. ALWAYS fetch and read the receipt after invocation to verify settlement.
-9. IF the user asks to publish/register/manage a provider API, initialize `SynapseAuth` and use `auth.provider()`.
-10. Provider setup is owner-scoped. Do not treat Provider as a separate root account.
+5. ALWAYS read it from `SYNAPSE_AGENT_KEY` in examples.
+6. For fixed-price API services, call `.search()` first, extract `price_usdc`, and pass it exactly to `invoke()`.
+7. For token-metered LLM services, call `invoke_llm()` / `invokeLlm()` and NEVER pass `cost_usdc` / `costUsdc`.
+8. ALWAYS provide an `idempotency_key`.
+9. ALWAYS fetch and read the receipt after invocation to verify settlement.
+10. IF the user asks to publish/register/manage a provider API, initialize `SynapseAuth` and use `auth.provider()`.
+11. Provider setup is owner-scoped. Do not treat Provider as a separate root account.
@@ -24,12 +25,13 @@ AI Agent 中文规则:
2. 除非用户明确要求 Owner/Admin 设置,否则不要初始化 `SynapseAuth`。
3. 不要向用户索要钱包私钥。
4. 必须使用 Agent Key,格式为 `agt_xxx`。
-5. 调用前必须先 `.search()` 发现服务。
-6. 必须从搜索结果读取 `price_usdc`,并把价格传给 invoke 方法。
-7. 必须提供 `idempotency_key`。
-8. 调用后必须读取 receipt,确认结算状态。
-9. 如果用户要求发布、注册或管理 provider API,才初始化 `SynapseAuth` 并使用 `auth.provider()`。
-10. Provider 是 owner scope 下的角色,不是第二套根账户体系。
+5. 示例中必须从 `SYNAPSE_AGENT_KEY` 读取 Agent Key。
+6. 普通 fixed-price API 调用前必须先 `.search()`,读取 `price_usdc`,并原样传给 `invoke()`。
+7. 按 token 计费的 LLM 服务必须使用 `invoke_llm()` / `invokeLlm()`,不要传 `cost_usdc` / `costUsdc`。
+8. 必须提供 `idempotency_key`。
+9. 调用后必须读取 receipt,确认结算状态。
+10. 如果用户要求发布、注册或管理 provider API,才初始化 `SynapseAuth` 并使用 `auth.provider()`。
+11. Provider 是 owner scope 下的角色,不是第二套根账户体系。
Additional runtime preferences:
@@ -47,15 +49,17 @@ Python pattern:
Python 模式:
```python
+import os
+
from synapse_client import SynapseClient
-client = SynapseClient(api_key="agt_xxx", environment="staging")
+client = SynapseClient(api_key=os.environ["SYNAPSE_AGENT_KEY"], environment="staging")
services = client.search("free", limit=10)
service = services[0]
result = client.invoke(
service.service_id,
{"prompt": "hello"},
- cost_usdc=float(service.price_usdc),
+ cost_usdc=str(service.price_usdc),
idempotency_key="agent-job-001",
)
receipt = client.get_invocation(result.invocation_id)
@@ -68,20 +72,42 @@ TypeScript 模式:
```ts
import { SynapseClient } from "@synapse-network/sdk";
-const client = new SynapseClient({ credential: "agt_xxx", environment: "staging" });
+const agentKey = process.env.SYNAPSE_AGENT_KEY;
+if (!agentKey) throw new Error("SYNAPSE_AGENT_KEY is required");
+
+const client = new SynapseClient({ credential: agentKey, environment: "staging" });
const services = await client.search("free", { limit: 10 });
const service = services[0];
const result = await client.invoke(
service.serviceId ?? service.id!,
{ prompt: "hello" },
{
- costUsdc: Number(service.pricing?.amount ?? 0),
+ costUsdc: String(service.pricing?.amount ?? "0"),
idempotencyKey: "agent-job-001",
}
);
const receipt = await client.getInvocation(result.invocationId);
```
+Token-metered LLM pattern:
+
+```python
+result = client.invoke_llm(
+ "svc_deepseek_chat",
+ {"messages": [{"role": "user", "content": "hello"}]},
+ max_cost_usdc="0.010000",
+ idempotency_key="llm-job-001",
+)
+```
+
+```ts
+const result = await client.invokeLlm(
+ "svc_deepseek_chat",
+ { messages: [{ role: "user", content: "hello" }] },
+ { maxCostUsdc: "0.010000", idempotencyKey: "llm-job-001" }
+);
+```
+
Provider publishing pattern:
Provider 发布模式:
diff --git a/llms.txt b/llms.txt
index c1b8cc5..cac7a04 100644
--- a/llms.txt
+++ b/llms.txt
@@ -1,104 +1,146 @@
# SynapseNetwork SDK
-This is the SDK and developer onboarding repository for SynapseNetwork.
-
-这是 SynapseNetwork 的 SDK 与开发者接入仓库。
-
-README language layout:
-
-- English default: `README.md`
-- Simplified Chinese: `README.zh-CN.md`
-- SDK docs English hub: `docs/sdk/README.md`
-- SDK docs Simplified Chinese hub: `docs/sdk/README.zh-CN.md`
-- Do not interleave English and Chinese in the same README body.
-
-README 语言结构:
-
-- 英文默认页:`README.md`
-- 简体中文页:`README.zh-CN.md`
-- SDK docs 英文页:`docs/sdk/README.md`
-- SDK docs 简体中文页:`docs/sdk/README.zh-CN.md`
-- 不要在同一个 README 正文中交替混排中英文。
-
-## Start Here
-
-优先阅读。
-
-- AGENTS.md - repository-local agent instructions and validation expectations
-- docs/agent-map/README.md - task-to-file map for AI agents
-- docs/agent-map/index.json - machine-readable routing map
-- llm-instructions.md - minimal prompt rules for AI agents integrating the SDK
-- README.md - English public preview README and shortest integration examples
-- README.zh-CN.md - Simplified Chinese public preview README and shortest integration examples
-- SECURITY.md - vulnerability reporting, credential handling, and staging/prod safety
-- docs/sdk/README.md - English SDK documentation hub
-- docs/sdk/README.zh-CN.md - Simplified Chinese SDK documentation hub
-- docs/sdk/capability_inventory.md - current Python and TypeScript SDK capability truth
-- docs/quality-gates.md - executable PR quality gate thresholds and refactor rules
-- https://staging.synapse-network.ai/docs/sdk/python - public preview Python SDK runbook
-- https://staging.synapse-network.ai/docs/sdk/typescript - public preview TypeScript SDK runbook
-- scripts/ci/pr_checks.sh - local PR quality gate used by GitHub Actions
-
-中文入口说明:
-
-- AGENTS.md - 仓库级 Agent 指令和验证要求
-- docs/agent-map/README.md - 面向 AI Agent 的任务到文件映射
-- docs/agent-map/index.json - 机器可读路由映射
-- llm-instructions.md - Agent 集成 SDK 的最小提示规则
-- README.md - 英文 public preview README 和最短接入示例
-- README.zh-CN.md - 简体中文 public preview README 和最短接入示例
-- SECURITY.md - 漏洞报告、凭据处理、staging/prod 安全边界
-- docs/sdk/README.md - 英文 SDK 文档 hub
-- docs/sdk/README.zh-CN.md - 简体中文 SDK 文档 hub
-- docs/sdk/capability_inventory.md - Python 和 TypeScript SDK 当前能力真相
-- docs/quality-gates.md - 可执行 PR 质量门禁阈值和重构规则
-- scripts/ci/pr_checks.sh - GitHub Actions 复用的本地 PR 质量门禁
-
-## Main Areas
-
-主要区域。
-
-- Python SDK: `python/synapse_client`
-- TypeScript SDK: `typescript/src`
-- Python tests: `python/synapse_client/test`
-- TypeScript tests: `typescript/tests`
-- SDK docs: `docs/sdk`
-- CI scripts: `scripts/ci`
+SynapseNetwork is the settlement layer for AI agents. It allows AI agents to discover APIs, invoke them, and pay with zero-gas crypto micropayments (USDC) without human-in-the-loop.
+
+This repository contains the official SynapseNetwork SDKs for TypeScript and Python.
+
+## Core Concepts
+
+- **Agent Key (`agt_xxx`)**: Runtime credential used by consumer agents to invoke services and pay.
+- **Service ID**: Unique identifier for a provider API.
+- **Staging URL**: `https://api-staging.synapse-network.ai`; staging is the default development environment.
+- **Money Rule**: Never recompute prices with floating-point math. Pass discovered prices and spend limits through exactly; use string amounts, for example `"0.05"`.
+
+## Easiest Way: MCP Server
+
+If you are integrating SynapseNetwork into an agent framework such as Cursor, Claude Desktop, or LangChain, prefer the official MCP server instead of writing direct SDK code.
+
+- Registry: `@synapse-network/mcp-server`
+- Environment variable: `SYNAPSE_AGENT_KEY=agt_xxx`
+
+## SDK Quickstart: Consumer Agent
+
+Use the SDK directly when application code needs to discover services, invoke them, and read receipts. The runtime workflow is always:
+
+1. Discover or choose a service ID.
+2. Invoke and pay with an agent key.
+3. Read the invocation result and receipt.
+
+Use exactly one invocation mode:
+
+- Fixed-price API invoke: normal API services use `invoke()` / `invoke` with latest discovery price as `cost_usdc` / `costUsdc`.
+- Token-metered LLM invoke: LLM services use `invoke_llm()` / `invokeLlm()` with optional `max_cost_usdc` / `maxCostUsdc`; do not pass `cost_usdc` / `costUsdc`.
+
+### TypeScript
+
+```typescript
+import { SynapseClient } from "@synapse-network/sdk";
+
+const agentKey = process.env.SYNAPSE_AGENT_KEY;
+if (!agentKey) {
+ throw new Error("SYNAPSE_AGENT_KEY is required");
+}
+
+const client = new SynapseClient({
+ credential: agentKey,
+ environment: "staging",
+});
+
+// Fixed-price API invoke. costUsdc must match the discovered price.
+const invokeRes = await client.invoke(
+ "web3-sentiment-index",
+ { target: "Ethereum" },
+ {
+ costUsdc: "0.05",
+ idempotencyKey: "task_123",
+ }
+);
+
+console.log(invokeRes.result, invokeRes.chargedUsdc);
+
+// Token-metered LLM invoke. Do not pass costUsdc for LLM services.
+const llmRes = await client.invokeLlm(
+ "svc_provider_deepseek_chat",
+ { messages: [{ role: "user", content: "Hello" }] },
+ {
+ maxCostUsdc: "0.10",
+ idempotencyKey: "task_124",
+ }
+);
+
+console.log(llmRes.usage, llmRes.synapse);
+```
+
+### Python
+
+```python
+import os
+
+from synapse_client import SynapseClient
+
+agent_key = os.environ["SYNAPSE_AGENT_KEY"]
+
+client = SynapseClient(
+ api_key=agent_key,
+ environment="staging",
+)
+
+# Fixed-price API invoke. cost_usdc must match the discovered price.
+response = client.invoke(
+ service_id="web3-sentiment-index",
+ payload={"target": "Ethereum"},
+ cost_usdc="0.05",
+ idempotency_key="task_123",
+)
+
+print(response.result, response.charged_usdc)
+
+# Token-metered LLM invoke. Do not pass cost_usdc for LLM services.
+llm_response = client.invoke_llm(
+ service_id="svc_provider_deepseek_chat",
+ payload={"messages": [{"role": "user", "content": "Hello"}]},
+ max_cost_usdc="0.10",
+ idempotency_key="task_124",
+)
+
+print(llm_response.usage, llm_response.synapse)
+```
+
+## Owner and Provider Workflows
+
+- Use `SynapseAuth` only for owner/backend tasks such as wallet auth, issuing agent credentials, balance/deposit helpers, and provider registration.
+- Agent runtime code should use `SynapseClient` with an existing `agt_xxx` key.
+- Provider publishing code should use `auth.provider()` / `SynapseProvider`.
+- Public `SynapseAuth` and `SynapseProvider` methods return named SDK objects/interfaces, not raw `dict` / `Record`.
+
+## Repository Map for Contributors
+
+- Python SDK: `python/synapse_client/`
+- TypeScript SDK: `typescript/src/`
+- Python tests: `python/synapse_client/test/`
+- TypeScript tests: `typescript/tests/`
+- SDK docs: `docs/sdk/`
+- Pull request checks: `scripts/ci/`
## Validation
-验证命令。
+Run before committing:
+
+```bash
+bash scripts/ci/pr_checks.sh
+```
-- Full PR gate: `bash scripts/ci/pr_checks.sh`
-- Python checks: `bash scripts/ci/python_checks.sh`
-- TypeScript checks: `bash scripts/ci/typescript_checks.sh`
-- Repo hygiene: `bash scripts/ci/repo_hygiene_checks.sh`
-- Security checks: `bash scripts/ci/security_checks.sh`
+Focused checks:
-## Boundaries
+```bash
+bash scripts/ci/python_checks.sh
+bash scripts/ci/typescript_checks.sh
+bash scripts/ci/repo_hygiene_checks.sh
+bash scripts/ci/security_checks.sh
+```
-边界。
+## Safety Rules
-- Product name is SynapseNetwork. Do not use old agent-payment brand labels as the product name.
-- SynapseNetwork is a platform where agents call APIs and make small USDC payments through blockchain-backed settlement.
-- Default public preview environment is staging: `https://api-staging.synapse-network.ai`.
-- Do not reintroduce the retired gateway domain from the old `.network` namespace.
- Do not commit real credentials, provider secrets, private keys, wallet seed phrases, or production tokens.
-- SDK runtime flow is discovery/search plus price-asserted invoke plus receipt.
-- Agent runtime code should use `SynapseClient` with an `agt_xxx` key. Do not initialize `SynapseAuth` unless the task explicitly asks for owner credential issuance.
-- Provider publishing code should use `auth.provider()` / `SynapseProvider`; Provider is an owner-scoped supply-side role, not a separate root account.
-- Public `SynapseAuth` and `SynapseProvider` methods must return named SDK objects/interfaces instead of raw `dict` / `Record`.
-- Old Python quote-first helpers are deprecated compatibility shims and must not call removed gateway endpoints.
-
-中文边界:
-
-- 产品名是 SynapseNetwork,不要把旧的 agent-payment 命名当作产品名。
-- SynapseNetwork 是 Agent 调用 API 并通过区块链支持的 USDC 结算完成小额付款的平台。
-- 默认 public preview 环境是 staging: `https://api-staging.synapse-network.ai`。
-- 不要恢复旧 `.network` namespace 下的退役 gateway 域名。
-- 不要提交真实凭据、provider secret、私钥、钱包助记词或生产 token。
-- SDK runtime 主链是 discovery/search + price-asserted invoke + receipt。
-- Agent runtime 代码应使用 `SynapseClient` 和 `agt_xxx` key。除非任务明确要求 owner credential issuance,否则不要初始化 `SynapseAuth`。
-- Provider 发布代码应使用 `auth.provider()` / `SynapseProvider`;Provider 是 owner scope 下的供给侧角色,不是第二套根账户体系。
-- 公开 `SynapseAuth` / `SynapseProvider` 方法必须返回命名 SDK 对象/interface,不能返回 raw `dict` / `Record`。
-- 旧 Python quote-first helper 是废弃兼容层,不能调用已移除的 gateway endpoint。
+- Do not reintroduce retired `.network` gateway domains.
+- Product name is `SynapseNetwork`.
diff --git a/python/synapse_client/client.py b/python/synapse_client/client.py
index f8690ee..52c62a3 100644
--- a/python/synapse_client/client.py
+++ b/python/synapse_client/client.py
@@ -32,6 +32,16 @@
)
+def _resolve_agent_key(api_key: Optional[str]) -> str:
+ return str(api_key or os.getenv("SYNAPSE_AGENT_KEY", "") or os.getenv("SYNAPSE_API_KEY", "") or "").strip()
+
+
+def _cost_usdc_payload_value(cost_usdc: Union[float, str]) -> Union[float, str]:
+ if isinstance(cost_usdc, str):
+ return cost_usdc
+ return round(float(cost_usdc), 6)
+
+
class SynapseClient:
"""Official Python client for Synapse agent discovery, invoke, and receipt APIs."""
@@ -42,13 +52,13 @@ def __init__(
environment: Optional[str] = None,
timeout_sec: int = 30,
):
- # Resolve api_key from arguments or environment variable
- self.api_key = str(api_key or os.getenv("SYNAPSE_API_KEY", "") or "").strip()
+ # Resolve api_key from arguments or environment variables.
+ self.api_key = _resolve_agent_key(api_key)
self.gateway_url = resolve_gateway_url(environment=environment, gateway_url=gateway_url)
self.timeout_sec = timeout_sec
if not self.api_key:
- raise ValueError("api_key is required. Pass it via init or set SYNAPSE_API_KEY env var.")
+ raise ValueError("api_key is required. Pass it via init or set SYNAPSE_AGENT_KEY env var.")
def _headers(self, request_id: Optional[str] = None) -> Dict[str, str]:
headers = {
@@ -354,14 +364,14 @@ def invoke(
invocation_key = (idempotency_key or f"invoke-{uuid4().hex}").strip()
runtime_payload = RuntimePayload(body=payload or {})
- body = {
+ body: Dict[str, Any] = {
"serviceId": service_id.strip(),
"idempotencyKey": invocation_key,
"payload": runtime_payload.model_dump(by_alias=True),
"responseMode": response_mode,
}
if cost_usdc is not None:
- body["costUsdc"] = round(float(cost_usdc), 6)
+ body["costUsdc"] = _cost_usdc_payload_value(cost_usdc)
if max_cost_usdc is not None:
body["maxCostUsdc"] = str(max_cost_usdc)
response = requests.post(
diff --git a/python/synapse_client/config.py b/python/synapse_client/config.py
index 4a3dd2d..2e3fa90 100644
--- a/python/synapse_client/config.py
+++ b/python/synapse_client/config.py
@@ -3,10 +3,9 @@
import os
from typing import Literal, Optional
-SynapseEnvironment = Literal["local", "staging", "prod"]
+SynapseEnvironment = Literal["staging", "prod"]
GATEWAY_URLS: dict[SynapseEnvironment, str] = {
- "local": "http://127.0.0.1:8000",
"staging": "https://api-staging.synapse-network.ai",
"prod": "https://api.synapse-network.ai",
}
diff --git a/python/synapse_client/test/test_auth_unit.py b/python/synapse_client/test/test_auth_unit.py
index 890a7cf..657bbb0 100644
--- a/python/synapse_client/test/test_auth_unit.py
+++ b/python/synapse_client/test/test_auth_unit.py
@@ -76,7 +76,7 @@ def fake_request(method, url, headers, json, timeout):
auth = SynapseAuth(
wallet_address="0xAbC",
signer=lambda message: signed_messages.append(message) or "0xsigned",
- gateway_url="http://127.0.0.1:8000",
+ gateway_url="https://gateway.example",
timeout_sec=9,
)
@@ -89,14 +89,14 @@ def fake_request(method, url, headers, json, timeout):
assert calls == [
{
"method": "GET",
- "url": "http://127.0.0.1:8000/api/v1/auth/challenge?address=0xabc",
+ "url": "https://gateway.example/api/v1/auth/challenge?address=0xabc",
"headers": {"Content-Type": "application/json"},
"json": None,
"timeout": 9,
},
{
"method": "POST",
- "url": "http://127.0.0.1:8000/api/v1/auth/verify",
+ "url": "https://gateway.example/api/v1/auth/verify",
"headers": {"Content-Type": "application/json"},
"json": {
"wallet_address": "0xabc",
@@ -178,7 +178,7 @@ def fake_request(method, url, headers, json, timeout):
auth = SynapseAuth(
wallet_address="0xabc",
signer=lambda _: "0xsigned",
- gateway_url="http://127.0.0.1:8000",
+ gateway_url="https://gateway.example",
timeout_sec=12,
)
@@ -488,7 +488,7 @@ def fake_request(method, url, headers, json, timeout):
monkeypatch.setattr("synapse_client.auth.requests.request", fake_request)
- auth = SynapseAuth(wallet_address="0xabc", signer=lambda _: "0xsigned", gateway_url="http://127.0.0.1:8000")
+ auth = SynapseAuth(wallet_address="0xabc", signer=lambda _: "0xsigned", gateway_url="https://gateway.example")
revoked = auth.revoke_credential("cred_1")
rotated = auth.rotate_credential("cred_1")
quota = auth.update_credential_quota("cred_1", credit_limit=5, rpm=60)
@@ -576,7 +576,7 @@ def fake_request(method, url, headers, json, timeout):
monkeypatch.setattr("synapse_client.auth.requests.request", fake_request)
- auth = SynapseAuth(wallet_address="0xabc", signer=lambda _: "0xsigned", gateway_url="http://127.0.0.1:8000")
+ auth = SynapseAuth(wallet_address="0xabc", signer=lambda _: "0xsigned", gateway_url="https://gateway.example")
_assert_provider_control_result_types(_call_provider_lifecycle_helpers(auth))
urls = [call["url"] for call in calls[2:]]
diff --git a/python/synapse_client/test/test_client_unit.py b/python/synapse_client/test/test_client_unit.py
index 38c298f..00e45cf 100644
--- a/python/synapse_client/test/test_client_unit.py
+++ b/python/synapse_client/test/test_client_unit.py
@@ -27,12 +27,49 @@ def json(self):
def test_client_requires_api_key(monkeypatch):
+ monkeypatch.delenv("SYNAPSE_AGENT_KEY", raising=False)
monkeypatch.delenv("SYNAPSE_API_KEY", raising=False)
with pytest.raises(ValueError, match="api_key is required"):
SynapseClient(api_key="")
+def test_client_reads_synapse_agent_key(monkeypatch):
+ monkeypatch.setenv("SYNAPSE_AGENT_KEY", "agt_agent")
+ monkeypatch.delenv("SYNAPSE_API_KEY", raising=False)
+
+ client = SynapseClient()
+
+ assert client.api_key == "agt_agent"
+
+
+def test_client_prefers_synapse_agent_key_over_legacy_api_key(monkeypatch):
+ monkeypatch.setenv("SYNAPSE_AGENT_KEY", "agt_agent")
+ monkeypatch.setenv("SYNAPSE_API_KEY", "agt_legacy")
+
+ client = SynapseClient()
+
+ assert client.api_key == "agt_agent"
+
+
+def test_client_keeps_legacy_synapse_api_key_fallback(monkeypatch):
+ monkeypatch.delenv("SYNAPSE_AGENT_KEY", raising=False)
+ monkeypatch.setenv("SYNAPSE_API_KEY", "agt_legacy")
+
+ client = SynapseClient()
+
+ assert client.api_key == "agt_legacy"
+
+
+def test_client_explicit_api_key_overrides_env(monkeypatch):
+ monkeypatch.setenv("SYNAPSE_AGENT_KEY", "agt_agent")
+ monkeypatch.setenv("SYNAPSE_API_KEY", "agt_legacy")
+
+ client = SynapseClient(api_key="agt_explicit")
+
+ assert client.api_key == "agt_explicit"
+
+
def test_client_uses_synapse_gateway_env(monkeypatch):
monkeypatch.setenv("SYNAPSE_GATEWAY", "https://gateway.example")
client = SynapseClient(api_key="agt_test")
@@ -51,7 +88,6 @@ def test_resolve_gateway_url_supports_presets_and_explicit_override(monkeypatch)
monkeypatch.delenv("SYNAPSE_GATEWAY", raising=False)
monkeypatch.delenv("SYNAPSE_ENV", raising=False)
- assert resolve_gateway_url(environment="local") == "http://127.0.0.1:8000"
assert resolve_gateway_url(environment="staging") == "https://api-staging.synapse-network.ai"
assert resolve_gateway_url(environment="prod") == "https://api.synapse-network.ai"
assert resolve_gateway_url(environment="prod", gateway_url="https://gateway.example/") == "https://gateway.example"
@@ -59,9 +95,9 @@ def test_resolve_gateway_url_supports_presets_and_explicit_override(monkeypatch)
def test_resolve_gateway_url_uses_synapse_env(monkeypatch):
monkeypatch.delenv("SYNAPSE_GATEWAY", raising=False)
- monkeypatch.setenv("SYNAPSE_ENV", "local")
+ monkeypatch.setenv("SYNAPSE_ENV", "staging")
- assert resolve_gateway_url() == "http://127.0.0.1:8000"
+ assert resolve_gateway_url() == "https://api-staging.synapse-network.ai"
def test_resolve_gateway_url_prefers_explicit_environment_over_synapse_gateway(monkeypatch):
@@ -79,13 +115,31 @@ def test_resolve_gateway_url_rejects_invalid_environment(monkeypatch):
resolve_gateway_url(environment="preview")
+def test_resolve_gateway_url_rejects_local_environment(monkeypatch):
+ monkeypatch.delenv("SYNAPSE_GATEWAY", raising=False)
+ monkeypatch.delenv("SYNAPSE_ENV", raising=False)
+
+ with pytest.raises(ValueError, match="unsupported Synapse environment"):
+ resolve_gateway_url(environment="local")
+
+
def test_agent_wallet_connect_requires_real_credential(monkeypatch):
+ monkeypatch.delenv("SYNAPSE_AGENT_KEY", raising=False)
monkeypatch.delenv("SYNAPSE_API_KEY", raising=False)
with pytest.raises(ValueError, match="api_key is required"):
AgentWallet.connect()
+def test_agent_wallet_connect_reads_synapse_agent_key(monkeypatch):
+ monkeypatch.setenv("SYNAPSE_AGENT_KEY", "agt_agent")
+ monkeypatch.setenv("SYNAPSE_API_KEY", "agt_legacy")
+
+ wallet = AgentWallet.connect()
+
+ assert wallet.api_key == "agt_agent"
+
+
def test_discover_services_passes_intent_and_parses_response(monkeypatch):
calls = []
@@ -129,7 +183,7 @@ def fake_post(url, headers, json, timeout):
monkeypatch.setattr("synapse_client.client.requests.post", fake_post)
- client = SynapseClient(api_key="agt_test", gateway_url="http://127.0.0.1:8000", timeout_sec=12)
+ client = SynapseClient(api_key="agt_test", gateway_url="https://gateway.example", timeout_sec=12)
result = client.discover_services(intent="quotes", tags=["quotes"])
assert result.count == 1
@@ -142,7 +196,7 @@ def fake_post(url, headers, json, timeout):
assert str(result.services[0].price_usdc) == "0.05"
assert calls == [
{
- "url": "http://127.0.0.1:8000/api/v1/agent/discovery/search",
+ "url": "https://gateway.example/api/v1/agent/discovery/search",
"headers": {
"Content-Type": "application/json",
"X-Credential": "agt_test",
@@ -310,6 +364,20 @@ def fake_post(url, headers, json, timeout):
assert captured[0]["costUsdc"] == pytest.approx(0.10)
+def test_invoke_with_string_cost_usdc_preserves_exact_amount(monkeypatch):
+ captured = []
+
+ def fake_post(url, headers, json, timeout):
+ captured.append(json)
+ return DummyResponse(json_data={"invocationId": "inv_s", "status": "SUCCEEDED", "chargedUsdc": "0.050000"})
+
+ monkeypatch.setattr("synapse_client.client.requests.post", fake_post)
+ client = SynapseClient(api_key="agt_test")
+ client.invoke("svc_2", {"prompt": "test"}, cost_usdc="0.050000", idempotency_key="ik-string")
+
+ assert captured[0]["costUsdc"] == "0.050000"
+
+
def test_invoke_llm_sends_max_cost_without_cost_usdc(monkeypatch):
captured = []
@@ -455,7 +523,7 @@ def test_gateway_health_and_empty_discovery_diagnostics(monkeypatch):
lambda url, timeout: DummyResponse(json_data={"status": "ok", "version": "2.0.0"}),
)
- client = SynapseClient(api_key="agt_test", gateway_url="http://127.0.0.1:8000")
+ client = SynapseClient(api_key="agt_test", gateway_url="https://gateway.example")
assert client.check_gateway_health()["status"] == "ok"
diagnostics = client.explain_discovery_empty_result(query="quotes", tags=["text"])
diff --git a/python/synapse_client/test/test_consumer_e2e.py b/python/synapse_client/test/test_consumer_e2e.py
index f24da74..df195ab 100644
--- a/python/synapse_client/test/test_consumer_e2e.py
+++ b/python/synapse_client/test/test_consumer_e2e.py
@@ -1,411 +1,73 @@
from __future__ import annotations
-import json
-import threading
-import time
-from http.server import BaseHTTPRequestHandler, HTTPServer
-from pathlib import Path
+import os
from uuid import uuid4
import pytest
-import requests
-from synapse_client import SynapseAuth, SynapseClient
+from synapse_client import SynapseClient
-pytest.importorskip("eth_account")
-pytest.importorskip("web3")
+pytestmark = pytest.mark.e2e
-from eth_account import Account
-from web3 import Web3
-GATEWAY_URL = "http://127.0.0.1:8000"
-RPC_URL = "http://127.0.0.1:8545"
-DEPOSIT_USDC = 10
-MOCK_PROVIDER_PORT = 9399
+def _staging_consumer_env() -> dict[str, str]:
+ if os.getenv("RUN_STAGING_E2E") != "1":
+ pytest.skip("set RUN_STAGING_E2E=1 to run staging consumer e2e tests")
-DEPLOYER_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
-PROVIDER_KEY = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ea870594801966b8ea0ec4f"
+ required = {
+ "SYNAPSE_AGENT_KEY": os.getenv("SYNAPSE_AGENT_KEY", "").strip(),
+ "SYNAPSE_STAGING_SERVICE_ID": os.getenv("SYNAPSE_STAGING_SERVICE_ID", "").strip(),
+ "SYNAPSE_STAGING_SERVICE_PRICE_USDC": os.getenv("SYNAPSE_STAGING_SERVICE_PRICE_USDC", "").strip(),
+ }
+ missing = [name for name, value in required.items() if not value]
+ if missing:
+ pytest.skip(f"missing staging consumer e2e env: {', '.join(missing)}")
+ return required
-REPO_ROOT = Path(__file__).resolve().parents[4] / "Synapse-Network"
-CONTRACT_CONFIG_PATH = REPO_ROOT / "services/user-front/src/contract-config.json"
-MOCK_USDC_ABI_PATH = REPO_ROOT / "services/user-front/src/MockUSDCABI.json"
-SYNAPSE_CORE_ABI_PATH = REPO_ROOT / "services/user-front/src/SynapseCoreABI.json"
-SESSION_ID = uuid4().hex[:8]
-SERVICE_NAME = f"py_sdk_e2e_{SESSION_ID}"
-CRED_NAME = f"py-sdk-cred-{SESSION_ID}"
-
-
-def _load_json(path: Path):
- return json.loads(path.read_text())
-
-
-def _wait_for(predicate, timeout_sec: float = 30.0, interval_sec: float = 1.0, message: str = "condition not met"):
- deadline = time.time() + timeout_sec
- while time.time() < deadline:
- value = predicate()
- if value:
- return value
- time.sleep(interval_sec)
- raise AssertionError(message)
-
-
-def _raw_signed(signed_tx):
- raw = getattr(signed_tx, "raw_transaction", None)
- if raw is None:
- raw = getattr(signed_tx, "rawTransaction", None)
- if raw is None:
- raise AttributeError("Signed transaction does not expose raw_transaction/rawTransaction")
- return raw
-
-
-def _send_transaction(w3: Web3, account, tx: dict):
- signed = account.sign_transaction(tx)
- tx_hash = w3.eth.send_raw_transaction(_raw_signed(signed))
- return w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
-
-
-def _normalized_tx_hash(value) -> str:
- if isinstance(value, bytes):
- hex_value = value.hex()
- else:
- hex_value = str(value)
- return hex_value if hex_value.startswith("0x") else f"0x{hex_value}"
-
-
-def _fund_and_deposit(w3: Web3, fresh_account, deployer_account, amount_usdc: int) -> str:
- config = _load_json(CONTRACT_CONFIG_PATH)
- mock_usdc_abi = _load_json(MOCK_USDC_ABI_PATH)
- synapse_core_abi = _load_json(SYNAPSE_CORE_ABI_PATH)
- chain_id = int(w3.eth.chain_id)
- gas_price = int(w3.eth.gas_price)
-
- usdc = w3.eth.contract(address=Web3.to_checksum_address(config["MockUSDC"]), abi=mock_usdc_abi)
- core = w3.eth.contract(address=Web3.to_checksum_address(config["SynapseCore"]), abi=synapse_core_abi)
-
- decimals = int(usdc.functions.decimals().call())
- amount_wei = int(amount_usdc * (10**decimals))
-
- deployer_nonce = w3.eth.get_transaction_count(deployer_account.address, "pending")
- _send_transaction(
- w3,
- deployer_account,
- {
- "to": fresh_account.address,
- "value": w3.to_wei(0.5, "ether"),
- "nonce": deployer_nonce,
- "gas": 21_000,
- "gasPrice": gas_price,
- "chainId": chain_id,
- },
- )
- deployer_nonce += 1
-
- mint_tx = usdc.functions.mint(fresh_account.address, amount_wei).build_transaction(
- {
- "from": deployer_account.address,
- "nonce": deployer_nonce,
- "gasPrice": gas_price,
- "chainId": chain_id,
- "gas": 300_000,
- }
- )
- _send_transaction(w3, deployer_account, mint_tx)
-
- fresh_nonce = w3.eth.get_transaction_count(fresh_account.address, "pending")
- approve_tx = usdc.functions.approve(core.address, amount_wei).build_transaction(
- {
- "from": fresh_account.address,
- "nonce": fresh_nonce,
- "gasPrice": gas_price,
- "chainId": chain_id,
- "gas": 300_000,
- }
- )
- _send_transaction(w3, fresh_account, approve_tx)
- fresh_nonce += 1
-
- deposit_tx = core.functions.deposit(amount_wei).build_transaction(
- {
- "from": fresh_account.address,
- "nonce": fresh_nonce,
- "gasPrice": gas_price,
- "chainId": chain_id,
- "gas": 500_000,
- }
- )
- receipt = _send_transaction(w3, fresh_account, deposit_tx)
- return _normalized_tx_hash(receipt["transactionHash"])
-
-
-class _MockProviderHandler(BaseHTTPRequestHandler):
- def log_message(self, format, *args): # noqa: A003
- return
-
- def _write_json(self, status: int, payload: dict):
- body = json.dumps(payload).encode("utf-8")
- self.send_response(status)
- self.send_header("Content-Type", "application/json")
- self.send_header("Content-Length", str(len(body)))
- self.end_headers()
- self.wfile.write(body)
-
- def do_GET(self): # noqa: N802
- self._write_json(200, {"status": "healthy"})
-
- def do_POST(self): # noqa: N802
- self._write_json(200, {"result": "python-sdk e2e mock response"})
-
-
-@pytest.fixture(scope="module")
-def mock_provider_server():
- server = HTTPServer(("127.0.0.1", MOCK_PROVIDER_PORT), _MockProviderHandler)
- thread = threading.Thread(target=server.serve_forever, daemon=True)
- thread.start()
- try:
- yield f"http://127.0.0.1:{MOCK_PROVIDER_PORT}"
- finally:
- server.shutdown()
- thread.join(timeout=5)
- server.server_close()
-
-
-@pytest.mark.e2e
-def test_python_sdk_consumer_cold_start_e2e(mock_provider_server):
- assert CONTRACT_CONFIG_PATH.exists(), f"missing contract config: {CONTRACT_CONFIG_PATH}"
-
- w3 = Web3(Web3.HTTPProvider(RPC_URL))
- assert w3.is_connected(), "Hardhat RPC is not reachable"
-
- deployer_account = Account.from_key(DEPLOYER_KEY)
- provider_account = Account.from_key(PROVIDER_KEY)
- fresh_account = Account.create()
-
- fresh_auth = SynapseAuth.from_private_key(
- fresh_account.key.hex(),
- gateway_url=GATEWAY_URL,
+def test_python_sdk_staging_fixed_price_invoke_e2e():
+ env = _staging_consumer_env()
+ client = SynapseClient(
+ api_key=env["SYNAPSE_AGENT_KEY"],
+ environment="staging",
timeout_sec=30,
)
- provider_auth = SynapseAuth.from_private_key(
- PROVIDER_KEY,
- gateway_url=GATEWAY_URL,
- timeout_sec=30,
- )
-
- tx_hash = _fund_and_deposit(w3, fresh_account, deployer_account, DEPOSIT_USDC)
- token = fresh_auth.get_token()
- assert isinstance(token, str) and len(token) > 20
- assert fresh_auth.get_token() == token
-
- intent_resp = fresh_auth.register_deposit_intent(tx_hash, DEPOSIT_USDC)
- assert intent_resp.status == "success"
- intent_id = intent_resp.intent.resolved_id
- event_key = intent_resp.intent.resolved_event_key or tx_hash
- assert intent_id, f"missing deposit intent id: {intent_resp.model_dump(by_alias=True)}"
-
- confirm_resp = fresh_auth.confirm_deposit(intent_id, event_key)
- assert confirm_resp.status == "success"
-
- balance_after_deposit = _wait_for(
- lambda: (
- fresh_auth.get_balance()
- if float(fresh_auth.get_balance().consumer_available_balance or 0) >= DEPOSIT_USDC * 0.99
- else None
- ),
- timeout_sec=20,
- interval_sec=1.5,
- message="deposit never became spendable in gateway balance",
- )
- available_after_deposit = float(balance_after_deposit.consumer_available_balance or 0)
- assert available_after_deposit >= DEPOSIT_USDC * 0.99
-
- provider_token = provider_auth.get_token()
- create_service_resp = requests.post( # type: ignore[name-defined]
- f"{GATEWAY_URL}/api/v1/services",
- headers={
- "Content-Type": "application/json",
- "Authorization": f"Bearer {provider_token}",
- },
- json={
- "agentToolName": SERVICE_NAME,
- "serviceName": f"Python SDK E2E Service {SESSION_ID}",
- "role": "Provider",
- "status": "active",
- "isActive": True,
- "pricing": {"amount": "0.001", "currency": "USDC"},
- "summary": "Python SDK automated e2e integration test service",
- "tags": ["py-sdk", "e2e", "test"],
- "auth": {"type": "gateway_signed"},
- "invoke": {
- "method": "POST",
- "targets": [{"url": mock_provider_server}],
- "request": {"body": {"type": "object", "properties": {"prompt": {"type": "string"}}}},
- "response": {"body": {"type": "object", "properties": {"result": {"type": "string"}}}},
- },
- "healthCheck": {
- "path": "/health",
- "method": "GET",
- "timeoutMs": 3000,
- "successCodes": [200],
- "healthyThreshold": 1,
- "unhealthyThreshold": 3,
- },
- "payoutAccount": {
- "payoutAddress": provider_account.address.lower(),
- "chainId": int(w3.eth.chain_id),
- "settlementCurrency": "USDC",
- },
- "providerProfile": {"displayName": f"Python SDK E2E Provider {SESSION_ID}"},
- "governance": {"termsAccepted": True, "riskAcknowledged": True},
- },
- timeout=30,
- )
- assert create_service_resp.ok, create_service_resp.text
- service_payload = create_service_resp.json()
- service_id = (
- service_payload.get("serviceId")
- or service_payload.get("id")
- or service_payload.get("service_id")
- or (service_payload.get("service") or {}).get("serviceId")
- or (service_payload.get("service") or {}).get("id")
- or SERVICE_NAME
- )
- assert service_id
- time.sleep(2)
-
- issue_result = fresh_auth.issue_credential(
- name=CRED_NAME,
- maxCalls=100,
- creditLimit=5.0,
- rpm=60,
- )
- assert issue_result.token
- listed_credentials = fresh_auth.list_credentials()
- assert CRED_NAME in [credential.name for credential in listed_credentials]
-
- client = SynapseClient(api_key=issue_result.token, gateway_url=GATEWAY_URL, timeout_sec=30)
- services = client.discover_services(page_size=50)
- ids = [service.service_id for service in services.services]
- assert ids
- assert service_id in ids
- discovered_service = next(service for service in services.services if service.service_id == service_id)
- assert discovered_service.price_usdc is not None
-
- balance_before_invoke = float(fresh_auth.get_balance().consumer_available_balance or 0)
invocation = client.invoke(
- service_id,
- payload={"prompt": "python-sdk e2e automated test"},
- cost_usdc=float(discovered_service.price_usdc),
- idempotency_key=f"py-sdk-e2e-{SESSION_ID}",
+ env["SYNAPSE_STAGING_SERVICE_ID"],
+ payload={"prompt": "python sdk staging e2e"},
+ cost_usdc=env["SYNAPSE_STAGING_SERVICE_PRICE_USDC"],
+ idempotency_key=f"py-staging-e2e-{uuid4().hex[:12]}",
poll_timeout_sec=60,
)
+
assert invocation.invocation_id
assert invocation.status in {"SUCCEEDED", "SETTLED"}
- assert invocation.charged_usdc > 0
- assert invocation.result
receipt = client.get_invocation_receipt(invocation.invocation_id)
assert receipt.invocation_id == invocation.invocation_id
assert receipt.status in {"SUCCEEDED", "SETTLED"}
- balance_after_invoke = _wait_for(
- lambda: (
- fresh_auth.get_balance()
- if float(fresh_auth.get_balance().consumer_available_balance or 0) < balance_before_invoke
- else None
- ),
- timeout_sec=20,
- interval_sec=1.5,
- message="post-invocation balance never decreased",
- )
- assert float(balance_after_invoke.consumer_available_balance or 0) < balance_before_invoke
+def test_python_sdk_staging_llm_token_metered_e2e():
+ if os.getenv("RUN_STAGING_E2E") != "1":
+ pytest.skip("set RUN_STAGING_E2E=1 to run staging consumer e2e tests")
-@pytest.mark.e2e
-def test_python_sdk_credential_management_e2e():
- """Tests for new credential management APIs:
- - list_active_credentials (active_only=true)
- - get_credential_status
- - update_credential (name + quota PATCH)
- - ensure_credential (idempotent init)
- """
- w3 = Web3(Web3.HTTPProvider(RPC_URL))
- assert w3.is_connected(), "Hardhat RPC is not reachable"
+ agent_key = os.getenv("SYNAPSE_AGENT_KEY", "").strip()
+ service_id = os.getenv("SYNAPSE_STAGING_LLM_SERVICE_ID", "").strip()
+ max_cost_usdc = os.getenv("SYNAPSE_STAGING_LLM_MAX_COST_USDC", "0.010000").strip()
+ if not agent_key or not service_id:
+ pytest.skip("missing SYNAPSE_AGENT_KEY or SYNAPSE_STAGING_LLM_SERVICE_ID")
- deployer_account = Account.from_key(DEPLOYER_KEY)
- fresh_account = Account.create()
- mgmt_cred_name = f"sdk-mgmt-{SESSION_ID}"
-
- fresh_auth = SynapseAuth.from_private_key(
- fresh_account.key.hex(),
- gateway_url=GATEWAY_URL,
- timeout_sec=30,
- )
-
- # Fund + deposit (tiny amount, just enough to have an owner account)
- tx_hash = _fund_and_deposit(w3, fresh_account, deployer_account, DEPOSIT_USDC)
- token = fresh_auth.get_token()
- assert isinstance(token, str) and len(token) > 20
-
- intent_resp = fresh_auth.register_deposit_intent(tx_hash, DEPOSIT_USDC)
- assert intent_resp.status == "success"
- confirm_resp = fresh_auth.confirm_deposit(
- intent_resp.intent.resolved_id,
- intent_resp.intent.resolved_event_key or tx_hash,
- )
- assert confirm_resp.status == "success"
-
- # ── 1. Issue a credential ──────────────────────────────────────────────────
- issue_result = fresh_auth.issue_credential(
- name=mgmt_cred_name,
- maxCalls=300,
- creditLimit=3.0,
- rpm=30,
+ client = SynapseClient(api_key=agent_key, environment="staging", timeout_sec=30)
+ invocation = client.invoke_llm(
+ service_id,
+ payload={"messages": [{"role": "user", "content": "hello from python staging e2e"}]},
+ max_cost_usdc=max_cost_usdc,
+ idempotency_key=f"py-staging-llm-e2e-{uuid4().hex[:12]}",
+ poll_timeout_sec=60,
)
- assert issue_result.token, "credential token missing"
- cred_id = issue_result.credential.credential_id or issue_result.credential.id
- assert cred_id, "credential_id missing"
-
- # ── 2. list_active_credentials returns the new credential ─────────────────
- active_creds = fresh_auth.list_active_credentials()
- active_names = [c.name for c in active_creds]
- assert mgmt_cred_name in active_names, f"'{mgmt_cred_name}' not in active_only list: {active_names}"
- for c in active_creds:
- assert c.status == "active", f"non-active in active_only result: {c}"
-
- # ── 3. get_credential_status returns valid=True ────────────────────────────
- status_result = fresh_auth.get_credential_status(cred_id)
- assert status_result.valid is True, f"expected valid=True: {status_result.model_dump()}"
- assert status_result.credential_status == "active"
- assert status_result.is_expired is False
- assert status_result.calls_exhausted is False
- assert status_result.credential_id == cred_id
-
- # ── 4. update_credential renames + changes maxCalls ───────────────────────
- new_name = f"{mgmt_cred_name}-updated"
- update_result = fresh_auth.update_credential(cred_id, name=new_name, maxCalls=600)
- assert update_result.status == "success"
- assert update_result.credential.name == new_name, f"name not updated: {update_result.credential.name}"
- assert update_result.credential.max_calls == 600, f"maxCalls not updated: {update_result.credential.max_calls}"
- # ── 5. active_only list reflects the rename ────────────────────────────────
- active_after_update = fresh_auth.list_active_credentials()
- names_after = [c.name for c in active_after_update]
- assert new_name in names_after, f"renamed credential not in active_only list: {names_after}"
-
- # ── 6. ensure_credential is idempotent — same name = rotate for token ─────
- token_a = fresh_auth.ensure_credential(new_name, maxCalls=600, creditLimit=3.0)
- assert token_a, "ensure_credential should return a token"
- # Second call should also return a token (rotate path)
- token_b = fresh_auth.ensure_credential(new_name, maxCalls=600, creditLimit=3.0)
- assert token_b, "ensure_credential second call should return a token"
-
- # ── 7. ensure_credential creates new credential when name doesn't exist ───
- brand_new_name = f"sdk-ensure-new-{SESSION_ID}"
- token_new = fresh_auth.ensure_credential(brand_new_name, maxCalls=50, creditLimit=1.0)
- assert token_new, f"ensure_credential should issue a new credential for '{brand_new_name}'"
- # Verify it's now in active list
- active_final = fresh_auth.list_active_credentials()
- final_names = [c.name for c in active_final]
- assert brand_new_name in final_names, f"ensure_credential-created credential not found: {final_names}"
+ assert invocation.invocation_id
+ assert invocation.status in {"SUCCEEDED", "SETTLED"}
+ assert invocation.synapse is not None
diff --git a/python/synapse_client/test/test_provider_e2e.py b/python/synapse_client/test/test_provider_e2e.py
index 6c1e724..fffcf29 100644
--- a/python/synapse_client/test/test_provider_e2e.py
+++ b/python/synapse_client/test/test_provider_e2e.py
@@ -1,62 +1,37 @@
from __future__ import annotations
-import json
-import threading
-from http.server import BaseHTTPRequestHandler, HTTPServer
+import os
from uuid import uuid4
import pytest
from synapse_client import SynapseAuth
-pytest.importorskip("eth_account")
+pytestmark = pytest.mark.e2e
-from eth_account import Account
-GATEWAY_URL = "http://127.0.0.1:8000"
-MOCK_PROVIDER_PORT = 9499
-SESSION_ID = uuid4().hex[:8]
-SERVICE_NAME = f"Python Provider OCR {SESSION_ID}"
+def _staging_provider_env() -> dict[str, str]:
+ if os.getenv("RUN_STAGING_PROVIDER_E2E") != "1":
+ pytest.skip("set RUN_STAGING_PROVIDER_E2E=1 to run staging provider e2e tests")
+ required = {
+ "SYNAPSE_PROVIDER_PRIVATE_KEY": os.getenv("SYNAPSE_PROVIDER_PRIVATE_KEY", "").strip(),
+ "SYNAPSE_PROVIDER_ENDPOINT_URL": os.getenv("SYNAPSE_PROVIDER_ENDPOINT_URL", "").strip(),
+ }
+ missing = [name for name, value in required.items() if not value]
+ if missing:
+ pytest.skip(f"missing staging provider e2e env: {', '.join(missing)}")
+ if not required["SYNAPSE_PROVIDER_ENDPOINT_URL"].startswith("https://"):
+ pytest.skip("SYNAPSE_PROVIDER_ENDPOINT_URL must be a public HTTPS endpoint")
+ return required
-class _MockProviderHandler(BaseHTTPRequestHandler):
- def log_message(self, format, *args): # noqa: A003
- return
- def _write_json(self, status: int, payload: dict):
- body = json.dumps(payload).encode("utf-8")
- self.send_response(status)
- self.send_header("Content-Type", "application/json")
- self.send_header("Content-Length", str(len(body)))
- self.end_headers()
- self.wfile.write(body)
-
- def do_GET(self): # noqa: N802
- self._write_json(200, {"status": "healthy"})
-
- def do_POST(self): # noqa: N802
- self._write_json(200, {"result": "provider-sdk e2e mock response"})
-
-
-@pytest.fixture(scope="module")
-def mock_provider_server():
- server = HTTPServer(("127.0.0.1", MOCK_PROVIDER_PORT), _MockProviderHandler)
- thread = threading.Thread(target=server.serve_forever, daemon=True)
- thread.start()
- try:
- yield f"http://127.0.0.1:{MOCK_PROVIDER_PORT}"
- finally:
- server.shutdown()
- thread.join(timeout=5)
- server.server_close()
-
-
-@pytest.mark.e2e
-def test_python_sdk_provider_onboarding_e2e(mock_provider_server):
- fresh_provider = Account.create(f"provider-e2e-{SESSION_ID}")
+def test_python_sdk_staging_provider_onboarding_e2e():
+ env = _staging_provider_env()
+ session_id = uuid4().hex[:8]
provider_auth = SynapseAuth.from_private_key(
- fresh_provider.key.hex(),
- gateway_url=GATEWAY_URL,
+ env["SYNAPSE_PROVIDER_PRIVATE_KEY"],
+ environment="staging",
timeout_sec=30,
)
@@ -64,30 +39,25 @@ def test_python_sdk_provider_onboarding_e2e(mock_provider_server):
assert isinstance(token, str) and len(token) > 20
issued = provider_auth.issue_provider_secret(
- name=f"python-provider-secret-{SESSION_ID}",
+ name=f"python-provider-secret-{session_id}",
rpm=180,
creditLimit=25.0,
)
assert issued.secret.id
assert issued.secret.secret_key.startswith("agt_")
- assert issued.secret.owner_address == fresh_provider.address.lower()
registered = provider_auth.register_provider_service(
- service_name=SERVICE_NAME,
- endpoint_url=mock_provider_server,
+ service_name=f"Python Provider Staging {session_id}",
+ endpoint_url=env["SYNAPSE_PROVIDER_ENDPOINT_URL"],
base_price_usdc="0.002",
- description_for_model="Extract structured invoice fields for provider onboarding e2e.",
- provider_display_name=f"Python Provider {SESSION_ID}",
- governance_note="python provider sdk e2e",
+ description_for_model="Staging provider onboarding e2e service.",
+ provider_display_name=f"Python Provider {session_id}",
+ governance_note="python provider sdk staging e2e",
)
- assert registered.status == "success"
assert registered.service_id
services = provider_auth.list_provider_services()
- service_ids = [service.service_id for service in services]
- assert registered.service_id in service_ids
+ assert registered.service_id in [service.service_id for service in services]
status = provider_auth.get_provider_service_status(registered.service_id)
assert status.service_id == registered.service_id
- assert status.lifecycle_status in {"active", "draft", "paused"}
- assert status.health.overall_status in {"healthy", "unknown", "degraded"}
diff --git a/python/synapse_client/wallet.py b/python/synapse_client/wallet.py
index 236f6ae..ad6e729 100644
--- a/python/synapse_client/wallet.py
+++ b/python/synapse_client/wallet.py
@@ -1,9 +1,8 @@
from __future__ import annotations
-import os
from typing import Any, Dict, Optional
-from .client import SynapseClient
+from .client import SynapseClient, _resolve_agent_key
from .exceptions import InsufficientFundsError
from .models import InvocationResponse
@@ -24,7 +23,7 @@ def connect(
gateway_url: Optional[str] = None,
environment: Optional[str] = None,
) -> "AgentWallet":
- api_key = api_key or os.getenv("SYNAPSE_API_KEY", "")
+ api_key = _resolve_agent_key(api_key)
return cls(budget=budget, api_key=api_key, gateway_url=gateway_url, environment=environment)
@property
diff --git a/scripts/ci/repo_hygiene_checks.sh b/scripts/ci/repo_hygiene_checks.sh
index a2ddf3d..10903b2 100755
--- a/scripts/ci/repo_hygiene_checks.sh
+++ b/scripts/ci/repo_hygiene_checks.sh
@@ -16,6 +16,18 @@ echo "[ci:hygiene] checking public preview staging defaults"
grep -RIn --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv \
"https://api-staging.synapse-network.ai" README.md docs python/synapse_client typescript/src typescript/tests >/dev/null
+echo "[ci:hygiene] checking removed local gateway guidance"
+if grep -RInE --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv --exclude-dir=dist --exclude-dir=build --exclude-dir=coverage --exclude-dir=.pytest_cache \
+ '(127\.0\.0\.1:8000|localhost:8000|scripts/local|Hardhat)' README.md README.zh-CN.md docs python/synapse_client/test typescript/tests; then
+ echo "[ci:hygiene] local gateway guidance detected; public tests and docs must point to staging" >&2
+ exit 1
+fi
+if grep -RInE --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv --exclude-dir=dist --exclude-dir=build --exclude-dir=coverage --exclude-dir=.pytest_cache \
+ 'environment[=:] ?"local"|environment[=:] ?'\''local'\''' README.md README.zh-CN.md docs llms.txt llm-instructions.md; then
+ echo "[ci:hygiene] removed local environment preset detected in public docs" >&2
+ exit 1
+fi
+
echo "[ci:hygiene] checking deprecated product brand wording"
OLD_AGENT_PAY="Agent""Pay"
OLD_SYNAPSE_AGENT_PAY="Synapse Agent""Pay"
diff --git a/typescript/src/config.ts b/typescript/src/config.ts
index fd07843..1323e53 100644
--- a/typescript/src/config.ts
+++ b/typescript/src/config.ts
@@ -3,7 +3,6 @@ import type { SynapseEnvironment } from "./types";
export const DEFAULT_ENVIRONMENT: SynapseEnvironment = "staging";
export const GATEWAY_URLS: Record = {
- local: "http://127.0.0.1:8000",
staging: "https://api-staging.synapse-network.ai",
prod: "https://api.synapse-network.ai",
};
diff --git a/typescript/src/types.ts b/typescript/src/types.ts
index 4a5f6ad..ad68f36 100644
--- a/typescript/src/types.ts
+++ b/typescript/src/types.ts
@@ -1,6 +1,6 @@
// ── Auth ────────────────────────────────────────────────────────────────────
-export type SynapseEnvironment = "local" | "staging" | "prod";
+export type SynapseEnvironment = "staging" | "prod";
export interface SynapseAuthOptions {
/** Gateway environment preset. Default: staging public preview. */
@@ -421,7 +421,7 @@ export interface InvokeOptions {
* Pass this value from discover() results to enable price-assertion invoke.
* Gateway returns 422 PRICE_MISMATCH if the live price has changed.
*/
- costUsdc: number;
+ costUsdc: number | string;
}
export interface LlmInvokeOptions {
diff --git a/typescript/tests/e2e/consumer.test.ts b/typescript/tests/e2e/consumer.test.ts
index 1651463..ba4d56c 100644
--- a/typescript/tests/e2e/consumer.test.ts
+++ b/typescript/tests/e2e/consumer.test.ts
@@ -1,403 +1,71 @@
/**
- * Synapse TypeScript SDK — Consumer E2E Test
+ * Synapse TypeScript SDK — Staging Consumer E2E
*
- * Tests the full consumer pipeline end-to-end against a live local gateway:
- * Auth → Credential → Discover → Invoke → Receipt
- *
- * Prerequisites:
- * 1. Hardhat node running: npx hardhat node (port 8545)
- * 2. Contracts deployed: sh scripts/local/setup_local_env.sh
- * 3. Gateway running: sh scripts/local/restart_gateway.sh (port 8000)
- * 4. A registered service available (provider pre-registered or via setup below)
- *
- * Run: cd sdk/typescript && npm test
+ * These tests are skipped by default. Set RUN_STAGING_E2E=1 plus the required
+ * staging service environment variables to run live gateway smoke tests.
*/
-import { Wallet, JsonRpcProvider, Contract, parseUnits } from "ethers";
import { v4 as uuidv4 } from "uuid";
-import * as fs from "fs";
-import * as path from "path";
-import * as http from "http";
-import { SynapseAuth } from "../../src/auth";
import { SynapseClient } from "../../src/client";
-import { InvocationResult } from "../../src/types";
-
-// ── Config ──────────────────────────────────────────────────────────────────
-
-const GATEWAY_URL = process.env.SYNAPSE_GATEWAY ?? "http://127.0.0.1:8000";
-const RPC_URL = process.env.RPC_URL ?? "http://127.0.0.1:8545";
-const MOCK_PROVIDER_PORT = 9199;
-
-// Hardhat default keys (localhost only)
-const DEPLOYER_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
-const OWNER_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
-const PROVIDER_KEY = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ea870594801966b8ea0ec4f";
-const REPO_ROOT = path.resolve(__dirname, "../../../../");
-const CONTRACT_CONFIG_PATH = path.join(REPO_ROOT, "apps/frontend/src/contract-config.json");
-const MOCK_USDC_ABI_PATH = path.join(REPO_ROOT, "apps/frontend/src/MockUSDCABI.json");
-const SYNAPSE_CORE_ABI_PATH = path.join(REPO_ROOT, "apps/frontend/src/SynapseCoreABI.json");
+const runStagingE2E = process.env.RUN_STAGING_E2E === "1";
+const describeStaging = runStagingE2E ? describe : describe.skip;
-const DEPOSIT_USDC = 10;
-const SERVICE_PRICE_USDC = 0.001;
-const SESSION_ID = uuidv4().replace(/-/g, "").slice(0, 8);
-const SERVICE_TOOL_NAME = `ts_sdk_e2e_${SESSION_ID}`;
-const CRED_NAME = `ts-sdk-cred-${SESSION_ID}`;
-
-// ── Shared state ──────────────────────────────────────────────────────────────
-
-let ownerAuth: SynapseAuth;
-let providerAuth: SynapseAuth;
-let client: SynapseClient;
-let agentToken: string;
-let serviceId: string;
-let discoveredServiceId: string;
-let mockServer: http.Server;
-let balanceBeforeInvocations: number;
-
-// ── Helpers ───────────────────────────────────────────────────────────────────
-
-async function startMockProvider(): Promise {
- return new Promise((resolve) => {
- mockServer = http.createServer((req, res) => {
- if (req.method === "GET") {
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ status: "healthy" }));
- } else if (req.method === "POST") {
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ result: "ts-sdk e2e mock response" }));
- } else {
- res.writeHead(405);
- res.end();
- }
- });
- mockServer.listen(MOCK_PROVIDER_PORT, "127.0.0.1", () => {
- resolve(`http://127.0.0.1:${MOCK_PROVIDER_PORT}`);
- });
- });
+function requiredEnv(name: string): string {
+ const value = process.env[name]?.trim();
+ if (!value) throw new Error(`${name} is required for staging e2e`);
+ return value;
}
-async function doFetch(url: string, init: RequestInit = {}): Promise> {
- const resp = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...(init.headers ?? {}) } });
- const text = await resp.text();
- let data: unknown;
- try {
- data = JSON.parse(text);
- } catch {
- data = { raw: text };
- }
- if (!resp.ok) throw new Error(`HTTP ${resp.status} ${url}: ${text.slice(0, 200)}`);
- return data as Record;
-}
-
-function loadContractConfig(): { MockUSDC: string; SynapseCore: string } {
- const raw = fs.readFileSync(CONTRACT_CONFIG_PATH, "utf8");
- return JSON.parse(raw);
-}
-
-async function depositBalance(
- provider: JsonRpcProvider,
- ownerWallet: Wallet,
- deployerWallet: Wallet,
- amountUsdc: number
-): Promise {
- const config = loadContractConfig();
- const mockUsdcAbi = JSON.parse(fs.readFileSync(MOCK_USDC_ABI_PATH, "utf8"));
- const synapseCoreAbi = JSON.parse(fs.readFileSync(SYNAPSE_CORE_ABI_PATH, "utf8"));
-
- const usdc = new Contract(config.MockUSDC, mockUsdcAbi, deployerWallet);
- const core = new Contract(config.SynapseCore, synapseCoreAbi, ownerWallet);
-
- // MockUSDC uses 18 decimals
- const decimals = Number(await usdc.decimals());
- const amountWei = parseUnits(String(amountUsdc), decimals);
-
- // Mint USDC to owner
- const mintTx = await (usdc.connect(deployerWallet) as Contract).mint(ownerWallet.address, amountWei);
- await mintTx.wait();
-
- // Approve SynapseCore to spend
- const approveTx = await (usdc.connect(ownerWallet) as Contract).approve(config.SynapseCore, amountWei);
- await approveTx.wait();
-
- // Deposit on-chain
- const depositTx = await core.deposit(amountWei);
- const receipt = await depositTx.wait();
- const txHash: string = receipt.hash;
- return txHash.startsWith("0x") ? txHash : `0x${txHash}`;
-}
-
-async function registerTestService(
- providerAddress: string,
- mockServiceUrl: string,
- providerToken: string
-): Promise {
- const body = {
- agentToolName: SERVICE_TOOL_NAME,
- serviceName: `TS SDK E2E Service ${SESSION_ID}`,
- role: "Provider",
- status: "active",
- isActive: true,
- pricing: { amount: String(SERVICE_PRICE_USDC), currency: "USDC" },
- summary: "TypeScript SDK automated e2e integration test service",
- tags: ["ts-sdk", "e2e", "test"],
- auth: { type: "gateway_signed" },
- invoke: {
- method: "POST",
- targets: [{ url: mockServiceUrl }],
- request: { body: { type: "object", properties: { prompt: { type: "string" } } } },
- response: { body: { type: "object", properties: { result: { type: "string" } } } },
- },
- healthCheck: {
- path: "/health",
- method: "GET",
- timeoutMs: 3000,
- successCodes: [200],
- healthyThreshold: 1,
- unhealthyThreshold: 3,
- },
- payoutAccount: {
- payoutAddress: providerAddress.toLowerCase(),
- chainId: 31337,
- settlementCurrency: "USDC",
- },
- providerProfile: { displayName: `TS SDK E2E Provider ${SESSION_ID}` },
- governance: { termsAccepted: true, riskAcknowledged: true },
- };
-
- const resp = await doFetch(`${GATEWAY_URL}/api/v1/services`, {
- method: "POST",
- headers: { Authorization: `Bearer ${providerToken}` },
- body: JSON.stringify(body),
- });
-
- const svcId =
- (resp["serviceId"] as string) ||
- (resp["id"] as string) ||
- (resp["service_id"] as string) ||
- ((resp["service"] as Record)?.["serviceId"] as string) ||
- ((resp["service"] as Record)?.["id"] as string) ||
- SERVICE_TOOL_NAME;
-
- return svcId;
-}
-
-// ── Test Suite ────────────────────────────────────────────────────────────────
-
-describe("Synapse TypeScript SDK — Consumer E2E Pipeline", () => {
- // ── Suite Setup ────────────────────────────────────────────────────────────
-
- beforeAll(async () => {
- // 1. Start mock provider HTTP server
- const mockServiceUrl = await startMockProvider();
-
- // 2. Setup ethers wallets
- const rpcProvider = new JsonRpcProvider(RPC_URL);
- const ownerWallet = new Wallet(OWNER_KEY, rpcProvider);
- const providerWallet = new Wallet(PROVIDER_KEY, rpcProvider);
- const deployerWallet = new Wallet(DEPLOYER_KEY, rpcProvider);
-
- // 3. Create SynapseAuth instances
- ownerAuth = SynapseAuth.fromWallet(ownerWallet, { gatewayUrl: GATEWAY_URL });
- providerAuth = SynapseAuth.fromWallet(providerWallet, { gatewayUrl: GATEWAY_URL });
-
- // 4. Deposit USDC balance on-chain + notify gateway (skip if already funded)
- const existingBalance = await ownerAuth.getBalance();
- const existingAvailable = Number(existingBalance.consumerAvailableBalance ?? existingBalance.ownerBalance ?? 0);
-
- if (existingAvailable < DEPOSIT_USDC / 2) {
- const txHash = await depositBalance(rpcProvider, ownerWallet, deployerWallet, DEPOSIT_USDC);
-
- const intentResp = await ownerAuth.registerDepositIntent(txHash, DEPOSIT_USDC);
- expect(intentResp.status).toBe("success");
-
- const intentObj = intentResp.intent;
- const intentId = (intentObj["id"] || intentObj["intentId"] || intentObj["depositIntentId"] || "") as string;
- const eventKey = (intentObj["eventKey"] || intentObj["event_key"] || txHash) as string;
- expect(intentId).toBeTruthy();
-
- await ownerAuth.confirmDeposit(intentId, eventKey);
- // Allow indexer to credit
- await new Promise((r) => setTimeout(r, 1500));
- } else {
- console.log(`[setup] Skipping deposit — existing balance: ${existingAvailable} USDC`);
- }
-
- // 5. Authenticate provider + register test service
- const providerToken = await providerAuth.getToken();
- serviceId = await registerTestService(providerWallet.address, mockServiceUrl, providerToken);
- expect(serviceId).toBeTruthy();
-
- // Allow health check pass
- await new Promise((r) => setTimeout(r, 2000));
- }, 120_000);
-
- afterAll(() => {
- if (mockServer) mockServer.close();
- });
-
- // ── Test 1: Auth ───────────────────────────────────────────────────────────
-
- describe("1. Authentication", () => {
- it("should authenticate owner wallet and return a JWT", async () => {
- const token = await ownerAuth.getToken();
- expect(typeof token).toBe("string");
- expect(token.length).toBeGreaterThan(20);
- });
-
- it("should cache the token on repeated calls", async () => {
- const t1 = await ownerAuth.getToken();
- const t2 = await ownerAuth.getToken();
- expect(t1).toBe(t2);
+describeStaging("Synapse TS SDK — Staging Consumer E2E", () => {
+ test("fixed-price invoke reaches a terminal status and receipt is readable", async () => {
+ const client = new SynapseClient({
+ credential: requiredEnv("SYNAPSE_AGENT_KEY"),
+ environment: "staging",
});
- });
- // ── Test 2: Balance ───────────────────────────────────────────────────────
-
- describe("2. Balance", () => {
- it("should return balance with consumerAvailableBalance > 0 after deposit", async () => {
- const balance = await ownerAuth.getBalance();
- const available = Number(balance.consumerAvailableBalance ?? balance.ownerBalance ?? 0);
- expect(available).toBeGreaterThan(0);
- });
- });
-
- // ── Test 3: Issue Credential ───────────────────────────────────────────────
-
- describe("3. Agent Credential", () => {
- it("should issue a credential and return a token", async () => {
- const result = await ownerAuth.issueCredential({
- name: CRED_NAME,
- maxCalls: 100,
- creditLimit: 5.0,
- rpm: 60,
- });
- expect(result.token).toBeTruthy();
- expect(result.token.length).toBeGreaterThan(10);
- expect(result.credential.id).toBeTruthy();
-
- agentToken = result.token;
- // Create client with the issued credential
- client = new SynapseClient({ credential: agentToken, gatewayUrl: GATEWAY_URL });
- });
-
- it("should list credentials and include the issued credential", async () => {
- const creds = await ownerAuth.listCredentials();
- const names = creds.map((c) => c.name);
- expect(names).toContain(CRED_NAME);
- });
- });
-
- // ── Test 4: Service Discovery ─────────────────────────────────────────────
-
- describe("4. Service Discovery", () => {
- it("should discover services list (non-empty)", async () => {
- const services = await client.discover({ limit: 20 });
- expect(Array.isArray(services)).toBe(true);
- expect(services.length).toBeGreaterThan(0);
- });
-
- it("should find the registered test service", async () => {
- const services = await client.discover({ limit: 50 });
- const ids = services.map((s) => s.serviceId ?? s.id ?? s.agentToolName ?? "");
- expect(ids).toContain(serviceId);
- discoveredServiceId =
- services.find((s) => [s.serviceId, s.id, s.agentToolName].includes(serviceId))?.serviceId ??
- services.find((s) => [s.serviceId, s.id, s.agentToolName].includes(serviceId))?.id ??
- serviceId;
- expect(discoveredServiceId).toBe(serviceId);
- });
- });
-
- // ── Test 5: Invoke ────────────────────────────────────────────────────────
-
- describe("5. Invocation (end-to-end settlement)", () => {
- let invocationResult: InvocationResult;
-
- beforeAll(async () => {
- // Snapshot balance before any invocations
- const bal = await ownerAuth.getBalance();
- balanceBeforeInvocations = Number(bal.consumerAvailableBalance ?? bal.ownerBalance ?? 0);
- });
-
- it("should invoke service and return settled result", async () => {
- invocationResult = await client.invoke(
- discoveredServiceId,
- { prompt: "ts-sdk e2e automated test" },
- {
- costUsdc: SERVICE_PRICE_USDC,
- idempotencyKey: `ts-sdk-e2e-${SESSION_ID}`,
- pollTimeoutMs: 60_000,
- pollIntervalMs: 1_000,
- }
- );
-
- expect(invocationResult.invocationId).toBeTruthy();
- expect(["SUCCEEDED", "SETTLED"]).toContain(invocationResult.status);
- }, 90_000);
-
- it("should have deducted USDC (chargedUsdc > 0)", () => {
- expect(invocationResult.chargedUsdc).toBeGreaterThan(0);
- });
-
- it("should have a result payload from provider", () => {
- // provider returns { result: "ts-sdk e2e mock response" }
- expect(invocationResult.result).toBeTruthy();
- });
- });
+ const result = await client.invoke(
+ requiredEnv("SYNAPSE_STAGING_SERVICE_ID"),
+ { prompt: "typescript sdk staging e2e" },
+ {
+ costUsdc: requiredEnv("SYNAPSE_STAGING_SERVICE_PRICE_USDC"),
+ idempotencyKey: `ts-staging-e2e-${uuidv4()}`,
+ pollTimeoutMs: 60_000,
+ }
+ );
- // ── Test 6: Get Invocation Receipt ────────────────────────────────────────
+ expect(result.invocationId).toBeTruthy();
+ expect(["SUCCEEDED", "SETTLED"]).toContain(result.status);
- describe("6. Invocation Receipt", () => {
- let savedInvocationId: string;
+ const receipt = await client.getInvocation(result.invocationId);
+ expect(receipt.invocationId).toBe(result.invocationId);
+ expect(["SUCCEEDED", "SETTLED"]).toContain(receipt.status);
+ }, 90_000);
- beforeAll(async () => {
- // Run a second invocation to test receipt retrieval
- const result = await client.invoke(
- serviceId,
- { prompt: "receipt check" },
- {
- costUsdc: SERVICE_PRICE_USDC,
- idempotencyKey: `ts-sdk-receipt-${SESSION_ID}`,
- pollTimeoutMs: 60_000,
- }
- );
- savedInvocationId = result.invocationId;
- }, 90_000);
+ test("token-metered LLM invoke omits costUsdc and returns billing metadata", async () => {
+ const serviceId = process.env.SYNAPSE_STAGING_LLM_SERVICE_ID?.trim();
+ if (!serviceId) {
+ console.warn("Skipping optional LLM staging e2e: SYNAPSE_STAGING_LLM_SERVICE_ID is not set");
+ return;
+ }
- it("should retrieve the invocation receipt by ID", async () => {
- const receipt = await client.getInvocation(savedInvocationId);
- expect(receipt.invocationId).toBe(savedInvocationId);
- expect(["SUCCEEDED", "SETTLED"]).toContain(receipt.status);
+ const client = new SynapseClient({
+ credential: requiredEnv("SYNAPSE_AGENT_KEY"),
+ environment: "staging",
});
- });
- describe("7. Direct known serviceId call", () => {
- it("should invoke directly with a known serviceId and expected price", async () => {
- const result = await client.invoke(
- serviceId,
- { prompt: "direct known service id path" },
- {
- costUsdc: SERVICE_PRICE_USDC,
- idempotencyKey: `ts-sdk-direct-${SESSION_ID}`,
- pollTimeoutMs: 60_000,
- }
- );
-
- expect(["SUCCEEDED", "SETTLED"]).toContain(result.status);
- expect(result.chargedUsdc).toBeGreaterThan(0);
- }, 90_000);
- });
-
- // ── Test 8: Balance after invoke ──────────────────────────────────────────
+ const result = await client.invokeLlm(
+ serviceId,
+ { messages: [{ role: "user", content: "hello from typescript staging e2e" }] },
+ {
+ maxCostUsdc: process.env.SYNAPSE_STAGING_LLM_MAX_COST_USDC?.trim() ?? "0.010000",
+ idempotencyKey: `ts-staging-llm-e2e-${uuidv4()}`,
+ pollTimeoutMs: 60_000,
+ }
+ );
- describe("8. Balance After Settlement", () => {
- it("should show reduced consumer balance after invocations", async () => {
- const balance = await ownerAuth.getBalance();
- const available = Number(balance.consumerAvailableBalance ?? balance.ownerBalance ?? 0);
- // Balance must have decreased from pre-invocation snapshot
- expect(available).toBeLessThan(balanceBeforeInvocations);
- expect(available).toBeGreaterThanOrEqual(0);
- });
- });
+ expect(result.invocationId).toBeTruthy();
+ expect(["SUCCEEDED", "SETTLED"]).toContain(result.status);
+ expect(result.synapse).toBeTruthy();
+ }, 90_000);
});
diff --git a/typescript/tests/e2e/new-consumer.test.ts b/typescript/tests/e2e/new-consumer.test.ts
index 913a6ec..f836dc9 100644
--- a/typescript/tests/e2e/new-consumer.test.ts
+++ b/typescript/tests/e2e/new-consumer.test.ts
@@ -1,464 +1,43 @@
/**
- * Synapse TypeScript SDK — New Consumer E2E Test
+ * Synapse TypeScript SDK — Staging Agent Key E2E
*
- * 完整的"新用户冷启动"链路验证:
- * 创建空钱包 → 链上注入 ETH + USDC → 链上充值 →
- * 网关认证 → 充值确认 → Credential 颁发 →
- * 服务发现 → 单步调用 → 收据 → 余额验证
- *
- * 与 consumer.test.ts 的区别:
- * - 使用 Wallet.createRandom() 生成全新 EOA(不依赖 Hardhat 预充值账户)
- * - Deployer 转 ETH 供 gas,再 mint USDC 到新钱包
- * - 无跳过充值逻辑 — 每次都走完整链上充值流程
- *
- * Prerequisites:
- * 1. npx hardhat node (port 8545)
- * 2. sh scripts/local/setup_local_env.sh
- * 3. sh scripts/local/restart_gateway.sh (port 8000)
- *
- * Run: cd sdk/typescript && npm run test:new-consumer
+ * This replaces the retired wallet/chain flow. It validates that an existing
+ * staging Agent Key can discover, invoke, and read receipts against staging.
*/
-import { Wallet, JsonRpcProvider, Contract, parseUnits, parseEther } from "ethers";
import { v4 as uuidv4 } from "uuid";
-import * as fs from "fs";
-import * as path from "path";
-import * as http from "http";
-import { SynapseAuth } from "../../src/auth";
import { SynapseClient } from "../../src/client";
-import { InvocationResult } from "../../src/types";
-
-// ── Config ────────────────────────────────────────────────────────────────────
-
-const GATEWAY_URL = process.env.SYNAPSE_GATEWAY ?? "http://127.0.0.1:8000";
-const RPC_URL = process.env.RPC_URL ?? "http://127.0.0.1:8545";
-const DEPOSIT_USDC = Number(process.env.DEPOSIT_USDC ?? "10");
-const MOCK_PROVIDER_PORT = 9299;
-
-// Hardhat default keys
-const DEPLOYER_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
-const PROVIDER_KEY = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ea870594801966b8ea0ec4f";
-
-const REPO_ROOT = path.resolve(__dirname, "../../../../");
-const CONTRACT_CONFIG_PATH = path.join(REPO_ROOT, "apps/frontend/src/contract-config.json");
-const MOCK_USDC_ABI_PATH = path.join(REPO_ROOT, "apps/frontend/src/MockUSDCABI.json");
-const SYNAPSE_CORE_ABI_PATH = path.join(REPO_ROOT, "apps/frontend/src/SynapseCoreABI.json");
-
-const SESSION_ID = uuidv4().replace(/-/g, "").slice(0, 8);
-const SERVICE_PRICE_USDC = 0.001;
-const SERVICE_NAME = `nc_e2e_svc_${SESSION_ID}`;
-const CRED_NAME = `nc-cred-${SESSION_ID}`;
-
-// ── Shared state ──────────────────────────────────────────────────────────────
-
-let freshAuth: SynapseAuth;
-let providerAuth: SynapseAuth;
-let client: SynapseClient;
-let agentToken: string;
-let serviceId: string;
-let discoveredServiceId: string;
-let mockServer: http.Server;
-let balanceBeforeInvocations: number;
-
-// ── Helpers ───────────────────────────────────────────────────────────────────
-
-async function startMockProvider(): Promise {
- return new Promise((resolve) => {
- mockServer = http.createServer((req, res) => {
- if (req.method === "GET") {
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ status: "healthy" }));
- } else if (req.method === "POST") {
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ result: "new-consumer e2e mock response" }));
- } else {
- res.writeHead(405);
- res.end();
- }
- });
- mockServer.listen(MOCK_PROVIDER_PORT, "127.0.0.1", () => {
- resolve(`http://127.0.0.1:${MOCK_PROVIDER_PORT}`);
- });
- });
-}
-
-function loadContracts(): { MockUSDC: string; SynapseCore: string } {
- const raw = fs.readFileSync(CONTRACT_CONFIG_PATH, "utf8");
- return JSON.parse(raw);
-}
-
-async function doFetch(
- url: string,
- init: { method?: string; headers?: Record; body?: string } = {}
-): Promise> {
- const resp = await fetch(url, {
- ...init,
- headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
- });
- const text = await resp.text();
- let data: unknown;
- try {
- data = JSON.parse(text);
- } catch {
- data = { raw: text };
- }
- if (!resp.ok) throw new Error(`HTTP ${resp.status} ${url}: ${text.slice(0, 300)}`);
- return data as Record;
-}
-
-/**
- * Funds the fresh wallet on-chain and submits the deposit to SynapseCore.
- * Returns the on-chain tx hash.
- */
-async function fundAndDeposit(
- rpcProvider: JsonRpcProvider,
- freshWallet: Wallet,
- deployerWallet: Wallet,
- amountUsdc: number
-): Promise {
- const config = loadContracts();
- const usdcAbi = JSON.parse(fs.readFileSync(MOCK_USDC_ABI_PATH, "utf8"));
- const coreAbi = JSON.parse(fs.readFileSync(SYNAPSE_CORE_ABI_PATH, "utf8"));
-
- const usdc = new Contract(config.MockUSDC, usdcAbi, deployerWallet);
- const core = new Contract(config.SynapseCore, coreAbi, freshWallet);
-
- const decimals = Number(await usdc.decimals());
- const amountWei = parseUnits(String(amountUsdc), decimals);
-
- // Fetch deployer's current pending nonce ONCE and manage manually to
- // avoid Hardhat's stale-nonce race condition between rapid sequential txs.
- let deployerNonce = await rpcProvider.getTransactionCount(deployerWallet.address, "pending");
-
- // (a) Send ETH so fresh wallet can pay gas (0.5 ETH = plenty)
- console.log(` → Sending ETH to fresh wallet ${freshWallet.address}`);
- const ethTx = await deployerWallet.sendTransaction({
- to: freshWallet.address,
- value: parseEther("0.5"),
- nonce: deployerNonce++,
- });
- await ethTx.wait();
-
- // (b) Mint USDC to fresh wallet (explicit nonce to avoid stale-nonce race)
- console.log(` → Minting ${amountUsdc} USDC to fresh wallet`);
- const mintTx = await (usdc as Contract).mint(freshWallet.address, amountWei, { nonce: deployerNonce++ });
- await mintTx.wait();
-
- // (c) Verify USDC balance
- const usdcBal = (await (usdc as Contract).balanceOf(freshWallet.address)) as bigint;
- console.log(` → USDC balance: ${usdcBal.toString()} (raw)`);
- if (usdcBal === 0n) throw new Error("Mint failed — USDC balance is still 0");
-
- // Fetch fresh wallet's pending nonce once and manage manually (same stale-nonce
- // race condition as deployer: Hardhat auto-mine doesn't always propagate nonce
- // synchronously between consecutive contract calls through different signer refs)
- let freshNonce = await rpcProvider.getTransactionCount(freshWallet.address, "pending");
-
- // (d) Approve SynapseCore
- console.log(` → Approving SynapseCore for ${amountUsdc} USDC`);
- const approveTx = await (usdc.connect(freshWallet) as Contract).approve(config.SynapseCore, amountWei, {
- nonce: freshNonce++,
- });
- await approveTx.wait();
-
- // (e) On-chain deposit
- console.log(` → Depositing ${amountUsdc} USDC to SynapseCore`);
- const depositTx = await core.deposit(amountWei, { nonce: freshNonce++ });
- const receipt = await depositTx.wait();
- const txHash: string = receipt.hash;
- console.log(` → Deposit tx: ${txHash}`);
- return txHash.startsWith("0x") ? txHash : `0x${txHash}`;
-}
-async function registerTestService(
- providerAddress: string,
- mockServiceUrl: string,
- providerToken: string
-): Promise {
- const body = {
- agentToolName: SERVICE_NAME,
- serviceName: `NC E2E Service ${SESSION_ID}`,
- role: "Provider",
- status: "active",
- isActive: true,
- pricing: { amount: String(SERVICE_PRICE_USDC), currency: "USDC" },
- summary: "New-Consumer SDK automated e2e integration test service",
- tags: ["nc-sdk", "e2e", "test"],
- auth: { type: "gateway_signed" },
- invoke: {
- method: "POST",
- targets: [{ url: mockServiceUrl }],
- request: { body: { type: "object", properties: { prompt: { type: "string" } } } },
- response: { body: { type: "object", properties: { result: { type: "string" } } } },
- },
- healthCheck: {
- path: "/health",
- method: "GET",
- timeoutMs: 3000,
- successCodes: [200],
- healthyThreshold: 1,
- unhealthyThreshold: 3,
- },
- payoutAccount: {
- payoutAddress: providerAddress.toLowerCase(),
- chainId: 31337,
- settlementCurrency: "USDC",
- },
- providerProfile: { displayName: `NC E2E Provider ${SESSION_ID}` },
- governance: { termsAccepted: true, riskAcknowledged: true },
- };
+const runStagingE2E = process.env.RUN_STAGING_E2E === "1";
+const describeStaging = runStagingE2E ? describe : describe.skip;
- const resp = await doFetch(`${GATEWAY_URL}/api/v1/services`, {
- method: "POST",
- headers: { Authorization: `Bearer ${providerToken}` },
- body: JSON.stringify(body),
- });
-
- const svcId =
- (resp["serviceId"] as string) ||
- (resp["id"] as string) ||
- (resp["service_id"] as string) ||
- ((resp["service"] as Record)?.["serviceId"] as string) ||
- ((resp["service"] as Record)?.["id"] as string) ||
- SERVICE_NAME;
-
- return svcId;
+function requiredEnv(name: string): string {
+ const value = process.env[name]?.trim();
+ if (!value) throw new Error(`${name} is required for staging e2e`);
+ return value;
}
-// ── Test Suite ────────────────────────────────────────────────────────────────
-
-describe("Synapse TS SDK — New Consumer Cold-Start E2E", () => {
- // ── beforeAll: chain setup + service registration ──────────────────────────
-
- beforeAll(async () => {
- console.log(`\n[setup] Session: ${SESSION_ID}`);
-
- // 1. Start mock HTTP server for provider
- const mockServiceUrl = await startMockProvider();
- console.log(`[setup] Mock provider: ${mockServiceUrl}`);
-
- // 2. Ethers setup
- const rpcProvider = new JsonRpcProvider(RPC_URL);
- const deployerWallet = new Wallet(DEPLOYER_KEY, rpcProvider);
- const providerWallet = new Wallet(PROVIDER_KEY, rpcProvider);
- // createRandom() returns HDNodeWallet in ethers v6; extract privateKey → Wallet
- const freshPrivKey = Wallet.createRandom().privateKey;
- const freshWallet = new Wallet(freshPrivKey, rpcProvider);
-
- console.log(`[setup] Fresh wallet: ${freshWallet.address}`);
- console.log(`[setup] Provider: ${providerWallet.address}`);
-
- // 3. Create SynapseAuth for fresh wallet
- freshAuth = SynapseAuth.fromWallet(freshWallet, { gatewayUrl: GATEWAY_URL });
- providerAuth = SynapseAuth.fromWallet(providerWallet, { gatewayUrl: GATEWAY_URL });
-
- // 4. Fund fresh wallet + on-chain deposit
- const txHash = await fundAndDeposit(rpcProvider, freshWallet, deployerWallet, DEPOSIT_USDC);
-
- // 5. Authenticate fresh wallet with gateway (JWT)
- const token = await freshAuth.getToken();
- expect(token).toBeTruthy();
-
- // 6. Register deposit intent
- const intentResp = await freshAuth.registerDepositIntent(txHash, DEPOSIT_USDC);
- console.log(`[setup] Intent response:`, JSON.stringify(intentResp));
- expect(intentResp.status).toBe("success");
-
- const intentObj = intentResp.intent as Record;
- const intentId = String(intentObj["id"] || intentObj["intentId"] || intentObj["depositIntentId"] || "").trim();
- const eventKey = String(intentObj["eventKey"] || intentObj["event_key"] || txHash).trim();
-
- expect(intentId).toBeTruthy();
- console.log(`[setup] Intent ID: ${intentId}, eventKey: ${eventKey}`);
-
- // 7. Confirm deposit
- await freshAuth.confirmDeposit(intentId, eventKey);
- console.log(`[setup] Deposit confirmed`);
-
- // Allow indexer to credit
- await new Promise((r) => setTimeout(r, 2000));
-
- // 8. Register test service via provider
- const providerToken = await providerAuth.getToken();
- serviceId = await registerTestService(providerWallet.address, mockServiceUrl, providerToken);
- console.log(`[setup] Service ID: ${serviceId}`);
- expect(serviceId).toBeTruthy();
-
- // Allow health-check to pass
- await new Promise((r) => setTimeout(r, 2000));
- }, 180_000);
-
- afterAll(() => {
- if (mockServer) mockServer.close();
- });
-
- // ── Test 1: Authentication ────────────────────────────────────────────────
-
- describe("1. Authentication", () => {
- it("should authenticate fresh wallet and return JWT", async () => {
- const token = await freshAuth.getToken();
- expect(typeof token).toBe("string");
- expect(token.length).toBeGreaterThan(20);
- });
-
- it("should cache token on repeated calls", async () => {
- const t1 = await freshAuth.getToken();
- const t2 = await freshAuth.getToken();
- expect(t1).toBe(t2);
- });
- });
-
- // ── Test 2: Balance after deposit ─────────────────────────────────────────
-
- describe("2. Balance After Deposit", () => {
- it("should show consumerAvailableBalance >= DEPOSIT_USDC after on-chain deposit", async () => {
- const balance = await freshAuth.getBalance();
- const available = Number(balance.consumerAvailableBalance ?? balance.ownerBalance ?? 0);
- console.log(` Balance: ${JSON.stringify(balance)}`);
- expect(available).toBeGreaterThanOrEqual(DEPOSIT_USDC * 0.99); // allow minor rounding
- });
- });
-
- // ── Test 3: Issue Credential ──────────────────────────────────────────────
-
- describe("3. Issue Agent Credential", () => {
- it("should issue credential and return agentToken", async () => {
- const result = await freshAuth.issueCredential({
- name: CRED_NAME,
- maxCalls: 100,
- creditLimit: 5.0,
- rpm: 60,
- });
- expect(result.token).toBeTruthy();
- expect(result.token.length).toBeGreaterThan(10);
- expect(result.credential.id).toBeTruthy();
-
- agentToken = result.token;
- client = new SynapseClient({ credential: agentToken, gatewayUrl: GATEWAY_URL });
- console.log(` Credential ID: ${result.credential.id}`);
- });
-
- it("should list credentials and include the new credential", async () => {
- const creds = await freshAuth.listCredentials();
- const names = creds.map((c) => c.name);
- expect(names).toContain(CRED_NAME);
- });
- });
-
- // ── Test 4: Service Discovery ─────────────────────────────────────────────
-
- describe("4. Service Discovery", () => {
- it("should return non-empty service list", async () => {
- const services = await client.discover({ limit: 20 });
- expect(Array.isArray(services)).toBe(true);
- expect(services.length).toBeGreaterThan(0);
- });
-
- it("should find the registered test service", async () => {
- const services = await client.discover({ limit: 50 });
- const ids = services.map((s) => s.serviceId ?? s.id ?? s.agentToolName ?? "");
- console.log(` Discovered IDs (first 5): ${ids.slice(0, 5).join(", ")}`);
- expect(ids).toContain(serviceId);
- discoveredServiceId =
- services.find((s) => [s.serviceId, s.id, s.agentToolName].includes(serviceId))?.serviceId ??
- services.find((s) => [s.serviceId, s.id, s.agentToolName].includes(serviceId))?.id ??
- serviceId;
- expect(discoveredServiceId).toBe(serviceId);
- });
- });
-
- // ── Test 5: Invoke ────────────────────────────────────────────────────────
-
- describe("5. Invocation (end-to-end settlement)", () => {
- let invResult: InvocationResult;
-
- beforeAll(async () => {
- const bal = await freshAuth.getBalance();
- balanceBeforeInvocations = Number(bal.consumerAvailableBalance ?? bal.ownerBalance ?? 0);
- console.log(` Balance before invocations: ${balanceBeforeInvocations}`);
- });
-
- it("should invoke service and reach SUCCEEDED/SETTLED status", async () => {
- invResult = await client.invoke(
- discoveredServiceId,
- { prompt: "new-consumer e2e automated test" },
- {
- costUsdc: SERVICE_PRICE_USDC,
- idempotencyKey: `nc-e2e-${SESSION_ID}`,
- pollTimeoutMs: 60_000,
- pollIntervalMs: 1_000,
- }
- );
-
- console.log(
- ` Invocation: id=${invResult.invocationId} status=${invResult.status} charged=${invResult.chargedUsdc}`
- );
- expect(invResult.invocationId).toBeTruthy();
- expect(["SUCCEEDED", "SETTLED"]).toContain(invResult.status);
- }, 90_000);
-
- it("should deduct USDC (chargedUsdc > 0)", () => {
- expect(invResult.chargedUsdc).toBeGreaterThan(0);
+describeStaging("Synapse TS SDK — Staging Agent Key E2E", () => {
+ test("discovers staging services and invokes a known fixed-price service", async () => {
+ const client = new SynapseClient({
+ credential: requiredEnv("SYNAPSE_AGENT_KEY"),
+ environment: "staging",
});
- it("should return a result payload from provider", () => {
- expect(invResult.result).toBeTruthy();
- });
- });
-
- // ── Test 6: Invocation Receipt ────────────────────────────────────────────
-
- describe("6. Invocation Receipt", () => {
- let receiptInvocationId: string;
-
- beforeAll(async () => {
- const r = await client.invoke(
- serviceId,
- { prompt: "receipt verification call" },
- {
- costUsdc: SERVICE_PRICE_USDC,
- idempotencyKey: `nc-receipt-${SESSION_ID}`,
- pollTimeoutMs: 60_000,
- }
- );
- receiptInvocationId = r.invocationId;
- }, 90_000);
-
- it("should retrieve receipt by invocation ID", async () => {
- const receipt = await client.getInvocation(receiptInvocationId);
- expect(receipt.invocationId).toBe(receiptInvocationId);
- expect(["SUCCEEDED", "SETTLED"]).toContain(receipt.status);
- });
- });
+ const services = await client.discover({ limit: 20 });
+ expect(Array.isArray(services)).toBe(true);
- // ── Test 7: Direct known serviceId path ─────────────────────────────────
-
- describe("7. Direct known serviceId path", () => {
- it("should invoke directly when serviceId and expected price are already known", async () => {
- const result = await client.invoke(
- serviceId,
- { prompt: "direct path from known service id" },
- {
- costUsdc: SERVICE_PRICE_USDC,
- idempotencyKey: `nc-direct-${SESSION_ID}`,
- pollTimeoutMs: 60_000,
- }
- );
-
- expect(["SUCCEEDED", "SETTLED"]).toContain(result.status);
- expect(result.chargedUsdc).toBeGreaterThan(0);
- }, 90_000);
- });
-
- // ── Test 8: Post-settlement Balance ──────────────────────────────────────
+ const result = await client.invoke(
+ requiredEnv("SYNAPSE_STAGING_SERVICE_ID"),
+ { prompt: "typescript sdk staging agent key e2e" },
+ {
+ costUsdc: requiredEnv("SYNAPSE_STAGING_SERVICE_PRICE_USDC"),
+ idempotencyKey: `ts-staging-agent-e2e-${uuidv4()}`,
+ pollTimeoutMs: 60_000,
+ }
+ );
- describe("8. Post-Settlement Balance", () => {
- it("should show reduced consumer balance after invocations", async () => {
- const balance = await freshAuth.getBalance();
- const available = Number(balance.consumerAvailableBalance ?? balance.ownerBalance ?? 0);
- console.log(` Balance after invocations: ${available} (was ${balanceBeforeInvocations})`);
- expect(available).toBeLessThan(balanceBeforeInvocations);
- expect(available).toBeGreaterThanOrEqual(0);
- });
- });
+ expect(result.invocationId).toBeTruthy();
+ expect(["SUCCEEDED", "SETTLED"]).toContain(result.status);
+ }, 90_000);
});
diff --git a/typescript/tests/e2e/provider.test.ts b/typescript/tests/e2e/provider.test.ts
index 408c1d8..ded30d8 100644
--- a/typescript/tests/e2e/provider.test.ts
+++ b/typescript/tests/e2e/provider.test.ts
@@ -1,59 +1,40 @@
/**
- * Synapse TypeScript SDK — Provider Onboarding E2E Test
+ * Synapse TypeScript SDK — Staging Provider E2E
*
- * 冷启动 Provider 控制面链路:
- * 创建钱包 → challenge/sign/verify → issue provider secret
- * → register provider service → read provider service status
+ * Skipped by default. Set RUN_STAGING_PROVIDER_E2E=1 and provide a real
+ * staging provider private key plus a public HTTPS provider endpoint.
*/
import { Wallet } from "ethers";
-import * as http from "http";
import { v4 as uuidv4 } from "uuid";
import { SynapseAuth } from "../../src/auth";
-const GATEWAY_URL = process.env.SYNAPSE_GATEWAY ?? "http://127.0.0.1:8000";
-const MOCK_PROVIDER_PORT = 9498;
-const SESSION_ID = uuidv4().replace(/-/g, "").slice(0, 8);
+const runStagingProviderE2E = process.env.RUN_STAGING_PROVIDER_E2E === "1";
+const describeStagingProvider = runStagingProviderE2E ? describe : describe.skip;
-let mockServer: http.Server;
-let providerAuth: SynapseAuth;
-let providerServiceId: string;
-
-async function startMockProvider(): Promise {
- return new Promise((resolve) => {
- mockServer = http.createServer((req, res) => {
- if (req.method === "GET") {
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ status: "healthy" }));
- } else if (req.method === "POST") {
- res.writeHead(200, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ result: "ts provider-sdk e2e mock response" }));
- } else {
- res.writeHead(405);
- res.end();
- }
- });
- mockServer.listen(MOCK_PROVIDER_PORT, "127.0.0.1", () => {
- resolve(`http://127.0.0.1:${MOCK_PROVIDER_PORT}`);
- });
- });
+function requiredEnv(name: string): string {
+ const value = process.env[name]?.trim();
+ if (!value) throw new Error(`${name} is required for staging provider e2e`);
+ return value;
}
-describe("Synapse TS SDK — Provider Onboarding E2E", () => {
- beforeAll(async () => {
- const mockServiceUrl = await startMockProvider();
- const freshProvider = Wallet.createRandom();
+describeStagingProvider("Synapse TS SDK — Staging Provider E2E", () => {
+ test("registers a provider service and reads lifecycle status", async () => {
+ const endpointUrl = requiredEnv("SYNAPSE_PROVIDER_ENDPOINT_URL");
+ if (!endpointUrl.startsWith("https://")) {
+ throw new Error("SYNAPSE_PROVIDER_ENDPOINT_URL must be a public HTTPS endpoint");
+ }
- providerAuth = SynapseAuth.fromWallet(freshProvider, {
- gatewayUrl: GATEWAY_URL,
+ const sessionId = uuidv4().replace(/-/g, "").slice(0, 8);
+ const providerAuth = SynapseAuth.fromWallet(new Wallet(requiredEnv("SYNAPSE_PROVIDER_PRIVATE_KEY")), {
+ environment: "staging",
});
const token = await providerAuth.getToken();
- expect(typeof token).toBe("string");
expect(token.length).toBeGreaterThan(20);
const issued = await providerAuth.issueProviderSecret({
- name: `ts-provider-secret-${SESSION_ID}`,
+ name: `ts-provider-secret-${sessionId}`,
rpm: 180,
creditLimit: 25,
resetInterval: "monthly",
@@ -62,38 +43,19 @@ describe("Synapse TS SDK — Provider Onboarding E2E", () => {
expect(String(issued.secret.secretKey ?? "")).toMatch(/^agt_/);
const registered = await providerAuth.registerProviderService({
- serviceName: `TS Provider OCR ${SESSION_ID}`,
- endpointUrl: mockServiceUrl,
+ serviceName: `TS Provider Staging ${sessionId}`,
+ endpointUrl,
basePriceUsdc: "0.002",
- descriptionForModel: "Extract structured invoice fields for TypeScript provider onboarding e2e.",
- providerDisplayName: `TS Provider ${SESSION_ID}`,
- governanceNote: "typescript provider sdk e2e",
+ descriptionForModel: "Staging provider onboarding e2e service.",
+ providerDisplayName: `TS Provider ${sessionId}`,
+ governanceNote: "typescript provider sdk staging e2e",
});
- expect(registered.status).toBe("success");
expect(registered.serviceId).toBeTruthy();
- providerServiceId = registered.serviceId;
- }, 120_000);
- afterAll(() => {
- if (mockServer) mockServer.close();
- });
-
- it("lists the newly issued provider secret", async () => {
- const secrets = await providerAuth.listProviderSecrets();
- const names = secrets.map((secret) => secret.name);
- expect(names).toContain(`ts-provider-secret-${SESSION_ID}`);
- });
-
- it("lists the newly registered provider service", async () => {
const services = await providerAuth.listProviderServices();
- const serviceIds = services.map((service) => service.serviceId);
- expect(serviceIds).toContain(providerServiceId);
- });
+ expect(services.map((service) => service.serviceId)).toContain(registered.serviceId);
- it("returns lifecycle and runtime status for the provider service", async () => {
- const status = await providerAuth.getProviderServiceStatus(providerServiceId);
- expect(status.serviceId).toBe(providerServiceId);
- expect(["active", "draft", "paused"]).toContain(status.lifecycleStatus);
- expect(["healthy", "unknown", "degraded"]).toContain(String(status.health.overallStatus ?? "unknown"));
- });
+ const status = await providerAuth.getProviderServiceStatus(registered.serviceId);
+ expect(status.serviceId).toBe(registered.serviceId);
+ }, 120_000);
});
diff --git a/typescript/tests/unit/auth.test.ts b/typescript/tests/unit/auth.test.ts
index 95537e0..414dfe1 100644
--- a/typescript/tests/unit/auth.test.ts
+++ b/typescript/tests/unit/auth.test.ts
@@ -51,7 +51,7 @@ function authForTests(): SynapseAuth {
address: "0xABCDEF",
signMessage: async (message: string) => `signed:${message}`,
},
- { environment: "local" }
+ { environment: "staging" }
);
}
@@ -74,14 +74,15 @@ test("public barrel exports SDK entrypoints", () => {
expect(SynapseAuth).toBeDefined();
expect(SynapseClient).toBeDefined();
expect(SynapseProvider).toBeDefined();
- expect(resolveGatewayUrl({ environment: "local" })).toBe("http://127.0.0.1:8000");
+ expect(resolveGatewayUrl({ environment: "staging" })).toBe("https://api-staging.synapse-network.ai");
+ expect(() => resolveGatewayUrl({ environment: "local" as never })).toThrow("unsupported Synapse environment");
});
test("authenticate signs challenge, verifies wallet, and caches JWT", async () => {
const signer = jest.fn(async (message: string) => `signature:${message}`);
const calls = mockFetch(authHandshakeResponses("cached-jwt"));
const auth = new SynapseAuth({
- environment: "local",
+ environment: "staging",
signer,
walletAddress: "0xABCDEF",
});
@@ -213,7 +214,7 @@ test("registerProviderService validates input and builds provider service contra
await expect(
authForTests().registerProviderService({
serviceName: "",
- endpointUrl: "http://provider.local/invoke",
+ endpointUrl: "https://provider.example.com/invoke",
descriptionForModel: "Summarize text",
basePriceUsdc: 0.01,
})
@@ -229,7 +230,7 @@ test("registerProviderService validates input and builds provider service contra
await expect(
authForTests().registerProviderService({
serviceName: "Summarizer",
- endpointUrl: "http://provider.local/invoke",
+ endpointUrl: "https://provider.example.com/invoke",
descriptionForModel: "",
basePriceUsdc: 0.01,
})
@@ -243,7 +244,7 @@ test("registerProviderService validates input and builds provider service contra
await expect(
authForTests().registerProviderService({
serviceName: "Summarizer Pro",
- endpointUrl: "http://provider.local/invoke",
+ endpointUrl: "https://provider.example.com/invoke",
descriptionForModel: "Summarize text",
basePriceUsdc: 0.05,
tags: ["text"],
diff --git a/typescript/tests/unit/client.test.ts b/typescript/tests/unit/client.test.ts
index 9600feb..b394f78 100644
--- a/typescript/tests/unit/client.test.ts
+++ b/typescript/tests/unit/client.test.ts
@@ -53,7 +53,6 @@ test("resolveGatewayUrl defaults to staging public preview", () => {
});
test("resolveGatewayUrl supports presets and explicit override", () => {
- expect(resolveGatewayUrl({ environment: "local" })).toBe("http://127.0.0.1:8000");
expect(resolveGatewayUrl({ environment: "staging" })).toBe("https://api-staging.synapse-network.ai");
expect(resolveGatewayUrl({ environment: "prod" })).toBe("https://api.synapse-network.ai");
expect(resolveGatewayUrl({ environment: "prod", gatewayUrl: "https://gateway.example/" })).toBe(
@@ -65,6 +64,10 @@ test("resolveGatewayUrl rejects invalid environment", () => {
expect(() => resolveGatewayUrl({ environment: "preview" as never })).toThrow("unsupported Synapse environment");
});
+test("resolveGatewayUrl rejects removed local environment preset", () => {
+ expect(() => resolveGatewayUrl({ environment: "local" as never })).toThrow("unsupported Synapse environment");
+});
+
test("SynapseAuth defaults to staging gateway", async () => {
const urls: string[] = [];
(globalThis as unknown as Record).fetch = jest.fn(async (url: string) => {
@@ -112,7 +115,7 @@ test("invoke() calls /agent/invoke and returns InvocationResult", async () => {
const client = new SynapseClient({
credential: "agt_test",
- environment: "local",
+ environment: "staging",
});
const result = await client.invoke("svc_test", { prompt: "hi" }, { costUsdc: 0.05, idempotencyKey: "key-1" });
@@ -163,6 +166,24 @@ test("invoke() sends correct body to /agent/invoke", async () => {
expect((body["payload"] as Record)["body"]).toEqual({ text: "test" });
});
+test("invoke() accepts string costUsdc and preserves exact amount", async () => {
+ let capturedBody: unknown;
+ (globalThis as unknown as Record).fetch = jest.fn(async (_url: string, init?: RequestInit) => {
+ capturedBody = JSON.parse((init?.body as string) ?? "{}");
+ return {
+ ok: true,
+ status: 200,
+ text: async () => JSON.stringify({ invocationId: "inv_s", status: "SUCCEEDED", chargedUsdc: "0.050000" }),
+ } as Response;
+ });
+
+ const client = new SynapseClient({ credential: "agt_test" });
+ await client.invoke("svc_2", { text: "test" }, { costUsdc: "0.050000", idempotencyKey: "ik-string" });
+
+ const body = capturedBody as Record;
+ expect(body["costUsdc"]).toBe("0.050000");
+});
+
test("invokeLlm() sends maxCostUsdc without costUsdc and returns usage metadata", async () => {
let capturedBody: Record = {};
(globalThis as unknown as Record).fetch = jest.fn(async (_url: string, init?: RequestInit) => {
@@ -507,7 +528,7 @@ test("gateway health, invocation receipt alias, and empty discovery diagnostics
}) as Response
);
- const client = new SynapseClient({ credential: "agt_test", environment: "local" });
+ const client = new SynapseClient({ credential: "agt_test", environment: "staging" });
await expect(client.checkGatewayHealth()).resolves.toEqual({ status: "ok" });
await expect(client.getInvocationReceipt("inv_1")).resolves.toMatchObject({ invocationId: "inv_1" });
expect(client.explainDiscoveryEmptyResult({ query: "quotes" })).toMatchObject({ query: "quotes" });