Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ ClawRouter is built for the agent-first world:

This is the stack that lets agents operate autonomously: **x402 + USDC + local routing**.

### Pre-spend trust (TWZRD AutoGate)

By default ClawRouter installs [`twzrd-x402-gate`](https://www.npmjs.com/package/twzrd-x402-gate) on its internal x402 client **before any payment is signed**:

- **Solana** pays: free TWZRD preflight (wash / allow|warn|block). `block` aborts the sign.
- **Base / EVM** pays: observe mode (no fabricated Solana reputation; payment proceeds).
- **Seat identity:** every gate call stamps `X-Twzrd-Caller: twzrd-x402-gate/<ver>` for measurable installs.

**Kill switch (default remains ON):**

```bash
export TWZRD_AUTO_GATE=0
# or
export TWZRD_GATE_ENABLED=false
```

TWZRD is a pre-spend check, not a facilitator and not a replacement for ClawRouter routing.


---

## How it compares
Expand Down
127 changes: 87 additions & 40 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@
"undici": "^8.5.0",
"viem": "^2.39.3"
},
"optionalDependencies": {
"twzrd-x402-gate": "^0.8.6"
},
"peerDependencies": {
"openclaw": ">=2025.1.0"
},
Expand Down
49 changes: 49 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1993,13 +1993,62 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> {
};
}


/**
* Default-on TWZRD AutoGate for the internal x402 client (Fork-1 seat).
* Missing optional package twzrd-x402-gate = soft-skip so forks can omit it.
* Soft-skip only when the resolution error names twzrd-x402-gate itself —
* missing transitive deps (or any other failure) fail closed so the proxy
* never boots with unguarded payments.
* Kill: TWZRD_AUTO_GATE=0 or TWZRD_GATE_ENABLED=false (handled inside the gate).
*/
async function installTwzrdAutoGateOnClient(client: x402Client): Promise<void> {
let gate: typeof import("twzrd-x402-gate");
try {
gate = await import("twzrd-x402-gate");
} catch (err) {
const code = (err as { code?: string } | null)?.code;
const msg = err instanceof Error ? err.message : String(err);
const missingModule =
code === "ERR_MODULE_NOT_FOUND" ||
code === "MODULE_NOT_FOUND" ||
/Cannot find module/i.test(msg) ||
/Cannot find package/i.test(msg);
// Only soft-skip when the absent package is the gate itself.
if (missingModule && /['"]twzrd-x402-gate['"]/i.test(msg)) {
console.warn(
"[ClawRouter] twzrd-x402-gate not available — payments unguarded. npm i twzrd-x402-gate@^0.8.6",
);
return;
}
throw err;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Registration failures must not fall through to unguarded payments.
gate.installTwzrdAutoGate(client, {
// refuse hard wash on Solana; Base remains observe (network unscored → allow)
refuseWashFlagged: true,
gateOnCanSpend: false,
unsupportedNetworkMode: "observe",
});
console.log(
"[ClawRouter] TWZRD AutoGate ON (pre-spend check). Kill: TWZRD_AUTO_GATE=0",
);
}

// Create x402 payment client with EVM scheme (always available)
const account = privateKeyToAccount(walletKey as `0x${string}`);
const evmPublicClient = createPublicClient({ chain: base, transport: http() });
const evmSigner = toClientEvmSigner(account, evmPublicClient);
const x402 = new x402Client();
registerExactEvmScheme(x402, { signer: evmSigner });

// TWZRD Fork-1 default seat: pre-spend gate on every payment before sign.
// Default ON when twzrd-x402-gate is installed (optionalDependency). Kill switch:
// TWZRD_AUTO_GATE=0 | TWZRD_GATE_ENABLED=false
// Base/EVM payments: observe mode (allow; Solana corpus does not score EVM).
// Solana payments: full wash/preflight. Seat identity stamps X-Twzrd-Caller.
await installTwzrdAutoGateOnClient(x402);

// Register Solana scheme if key is available
// Uses registerExactSvmScheme helper which registers:
// - solana:* wildcard (catches any CAIP-2 Solana network)
Expand Down
76 changes: 76 additions & 0 deletions src/twzrd-autogate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect, vi, afterEach } from "vitest";

/**
* Proxy installs TWZRD AutoGate on the internal x402Client before payFetch.
* We unit-test the env kill + dynamic import contract without booting the proxy.
*/
describe("TWZRD AutoGate seat (Fork-1)", () => {
const prevAuto = process.env.TWZRD_AUTO_GATE;
const prevGate = process.env.TWZRD_GATE_ENABLED;

afterEach(() => {
if (prevAuto === undefined) delete process.env.TWZRD_AUTO_GATE;
else process.env.TWZRD_AUTO_GATE = prevAuto;
if (prevGate === undefined) delete process.env.TWZRD_GATE_ENABLED;
else process.env.TWZRD_GATE_ENABLED = prevGate;
vi.resetModules();
});

it("isTwzrdAutoGateDisabled honors TWZRD_AUTO_GATE=0", async () => {
process.env.TWZRD_AUTO_GATE = "0";
const { isTwzrdAutoGateDisabled } = await import("twzrd-x402-gate");
expect(isTwzrdAutoGateDisabled()).toBe(true);
});

it("isTwzrdAutoGateDisabled default false when env unset", async () => {
delete process.env.TWZRD_AUTO_GATE;
delete process.env.TWZRD_GATE_ENABLED;
const { isTwzrdAutoGateDisabled } = await import("twzrd-x402-gate");
expect(isTwzrdAutoGateDisabled()).toBe(false);
});

it("installTwzrdAutoGate registers on a client with onBeforePaymentCreation", async () => {
delete process.env.TWZRD_AUTO_GATE;
delete process.env.TWZRD_GATE_ENABLED;
const hooks: Array<(ctx: unknown) => Promise<unknown>> = [];
const client = {
onBeforePaymentCreation(hook: (ctx: unknown) => Promise<unknown>) {
hooks.push(hook);
},
};
const { installTwzrdAutoGate } = await import("twzrd-x402-gate");
installTwzrdAutoGate(client as never, {
refuseWashFlagged: true,
gateOnCanSpend: false,
unsupportedNetworkMode: "observe",
});
// Registrar must actually be invoked — typeof alone is true pre-install.
expect(hooks.length).toBeGreaterThan(0);
expect(typeof client.onBeforePaymentCreation).toBe("function");
});

it("soft-skip matcher only accepts missing twzrd-x402-gate itself", () => {
// Mirror installTwzrdAutoGateOnClient catch predicate (proxy.ts).
const isSoftSkip = (code: string | undefined, msg: string): boolean => {
const missingModule =
code === "ERR_MODULE_NOT_FOUND" ||
code === "MODULE_NOT_FOUND" ||
/Cannot find module/i.test(msg) ||
/Cannot find package/i.test(msg);
return missingModule && /['"]twzrd-x402-gate['"]/i.test(msg);
};
expect(
isSoftSkip(
"ERR_MODULE_NOT_FOUND",
"Cannot find package 'twzrd-x402-gate' imported from /app/proxy.js",
),
).toBe(true);
expect(
isSoftSkip(
"ERR_MODULE_NOT_FOUND",
"Cannot find package 'some-transitive-dep' imported from /app/node_modules/twzrd-x402-gate/index.js",
),
).toBe(false);
expect(isSoftSkip(undefined, "boom unrelated")).toBe(false);
});
});