Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Privacy Router provider configuration.
#
# The vendor-neutral privacy-router skill enables providers independently.
# Missing credentials disable only that provider; `list-providers` will still
# show the disabled reason.
#
# Disable the bundled HoudiniSwap provider explicitly:
# PRIVACY_ROUTER_PROVIDER_HOUDINISWAP_ENABLED=0

# HoudiniSwap Partner API v2 credentials.
#
# This file is auto-loaded by skills/houdiniswap/scripts/_client.mjs whenever
Expand Down
79 changes: 62 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![node: >=22](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](.nvmrc)

A privacy-preserving cross-chain crypto swap **Agent Skill**, powered by the
A privacy-preserving cross-chain crypto transfer/swap **Agent Skill** suite.
The primary entrypoint is now a vendor-neutral `privacy-router` that can choose
among Web3 products based on the user's chain, asset, amount, privacy, cost and
speed requirements. The first bundled provider adapter is powered by the
official [HoudiniSwap Partner API v2](https://docs.houdiniswap.com/).

This skill lets AI agents (Claude Code, OpenClaw, Hermes, Pi,
This project lets AI agents (Claude Code, OpenClaw, Hermes, Pi,
Cursor, and any other tool that supports the open [`SKILL.md`](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
specification) understand and execute privacy-preserving cross-chain swaps:
get quotes, create exchanges, poll for completion, and submit batches —
all in natural language.
specification) understand and execute privacy-preserving cross-chain intents:
plan routes, compare provider quotes, create orders, poll for completion, and
submit provider-specific batches — all in natural language.

## Highlights

Expand All @@ -24,9 +27,12 @@ all in natural language.
No `npm install` required at runtime.
- **Tested.** 65 offline tests + 2 opt-in live tests, run on a CI matrix
of Node 22/24 × Ubuntu/macOS/Windows.
- **Privacy-first by design.** Maps natural-language privacy intent to
HoudiniSwap's `--anonymous` Private Swap routing, not the cheaper but
on-chain-traceable default path.
- **Provider-neutral routing.** `skills/privacy-router/` chooses a provider by
privacy requirement first, then output amount, then speed. HoudiniSwap remains
available as the first adapter and as a backward-compatible direct skill.
- **Privacy-first by design.** Strict privacy requirements are never silently
downgraded; if no provider can satisfy a route, the router returns an unmet
result instead of creating an order.

## Prerequisites

Expand Down Expand Up @@ -72,7 +78,7 @@ The installer:
hook is there for future extensions).
3. Copies `.env.example` to `.env` (preserves an existing `.env`).
4. Runs the offline smoke test suite (`npm run smoke`).
5. Symlinks `skills/houdiniswap/` into every agent's standard skills
5. Symlinks `skills/privacy-router/` and `skills/houdiniswap/` into every agent's standard skills
directory that exists on this machine (`~/.claude/skills/`,
`~/.openclaw/skills/`, `~/.hermes/skills/`, `~/.pi/agent/skills/`,
`~/.cursor/skills/`), then verifies the deployment is readable
Expand All @@ -82,6 +88,38 @@ The installer:
Pass `--no-link` if you only want steps 1–4 (CI, container builds, etc.),
or `--copy` to fall back from symlinks to plain copies.

## Privacy Router

Use the router when the user wants the agent to decide which Web3 product or
blockchain route should satisfy a private transfer/swap:

```bash
node scripts/router/list-providers.mjs
node scripts/router/plan-transfer.mjs \
--from-chain solana --to-chain ethereum \
--from-asset SOL --to-asset ETH --amount 1.5 \
--privacy unlinkability
node scripts/router/get-quotes.mjs \
--from-chain solana --to-chain ethereum \
--from-asset SOL --to-asset ETH --amount 1.5 \
--privacy unlinkability
```

`get-quotes` returns all eligible quotes plus `selectedQuote`. Routed quote and
order IDs are formatted as `<provider>:<providerObjectId>`, for example
`houdiniswap:q-...`, so follow-up commands can dispatch to the right adapter:

```bash
node scripts/router/create-order.mjs \
--quote-id houdiniswap:<quoteId> \
--address-to 0xRecipient...
node scripts/router/wait-order.mjs --id houdiniswap:<orderId>
```

The router currently bundles one provider adapter, HoudiniSwap. Future
providers can be added by registering another adapter with the same capability,
quote and order interfaces.

## Configure auth

Open `.env` and set **one** of:
Expand All @@ -100,13 +138,13 @@ Mode A. `search-tokens`, `list-chains`, and `get-quote` work in either
mode. Full details in
[`skills/houdiniswap/references/auth.md`](skills/houdiniswap/references/auth.md).

## Install the skill into your agent
## Install the skills into your agent

> **Why this matters.** Every SKILL.md-compatible agent (Claude Code,
> OpenClaw, Hermes, Pi, Cursor, …) expects a directory layout where `SKILL.md` is
> at the **top level** of `<agent-skills-dir>/<skill-name>/`. This repo,
> on the other hand, is a multi-skill monorepo with `SKILL.md` nested
> at `skills/houdiniswap/SKILL.md` so future skills can live as siblings.
> on the other hand, is a multi-skill monorepo with `SKILL.md` files nested
> at `skills/privacy-router/SKILL.md` and `skills/houdiniswap/SKILL.md`.
> The two structures are reconciled by symlinking the inner skill
> directory into the agent's expected location:

Expand All @@ -117,12 +155,15 @@ graph LR
B["package.json"]
C["scripts/install.sh"]
D["skills/houdiniswap/<br/>SKILL.md<br/>scripts/<br/>references/<br/>tests/"]
G["skills/privacy-router/<br/>SKILL.md<br/>scripts/<br/>lib/"]
end
subgraph agent["~/.claude/skills/ (agent view)"]
E["houdiniswap/<br/>SKILL.md<br/>scripts/<br/>references/<br/>tests/"]
E["privacy-router/<br/>SKILL.md<br/>scripts/<br/>lib/"]
F["houdiniswap/<br/>SKILL.md<br/>scripts/<br/>references/<br/>tests/"]
end
D -. "ln -s" .-> E
A -. "auto-loaded by<br/>_client.mjs<br/>via parent search" .-> E
D -. "ln -s" .-> F
G -. "ln -s" .-> E
A -. "auto-loaded by<br/>_client.mjs<br/>via parent search" .-> F
```

`scripts/install.sh` does the symlink for you and verifies it works
Expand Down Expand Up @@ -155,8 +196,8 @@ bash scripts/verify-deployment.sh

### Manual install (if you don't want to use install.sh)

The skill source dir is `skills/houdiniswap/`. Symlink it into whichever
of these your agent uses:
The skill source dirs are `skills/privacy-router/` and `skills/houdiniswap/`.
Symlink them into whichever agent directory you use:

| Agent | Global path | Project-scoped path |
| -------------- | ------------------------------------------------ | ---------------------------------- |
Expand All @@ -171,6 +212,7 @@ of these your agent uses:
# macOS / Linux
mkdir -p ~/.claude/skills
ln -snf "$(pwd)/skills/houdiniswap" ~/.claude/skills/houdiniswap
ln -snf "$(pwd)/skills/privacy-router" ~/.claude/skills/privacy-router
```

```powershell
Expand All @@ -179,6 +221,9 @@ New-Item -ItemType Directory -Force $env:USERPROFILE\.claude\skills | Out-Null
New-Item -ItemType Junction `
-Path "$env:USERPROFILE\.claude\skills\houdiniswap" `
-Target "$(Get-Location)\skills\houdiniswap"
New-Item -ItemType Junction `
-Path "$env:USERPROFILE\.claude\skills\privacy-router" `
-Target "$(Get-Location)\skills\privacy-router"
```

Restart your agent so it re-scans the skills directory. The skill is
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"scripts": {
"test": "node scripts/run-tests.mjs",
"test:coverage": "node scripts/run-tests.mjs --coverage",
"smoke": "node --test --test-reporter=spec skills/houdiniswap/tests/_client.test.mjs skills/houdiniswap/tests/list-chains.test.mjs",
"lint:syntax": "node --check skills/houdiniswap/scripts/_client.mjs"
"smoke": "node --test --test-reporter=spec skills/houdiniswap/tests/_client.test.mjs skills/houdiniswap/tests/list-chains.test.mjs skills/privacy-router/tests/registry.test.mjs skills/privacy-router/tests/router.test.mjs",
"lint:syntax": "node --check skills/houdiniswap/scripts/_client.mjs && node --check skills/privacy-router/lib/router.mjs && node --check scripts/router/list-providers.mjs"
},
"repository": {
"type": "git",
Expand Down
37 changes: 20 additions & 17 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- Verifies Node.js >= 22 is installed
- Copies .env.example to .env (preserving existing .env)
- Runs the offline smoke test suite
- Symlinks/junctions skills/houdiniswap into the standard agent skill paths
- Symlinks/junctions skills/houdiniswap and skills/privacy-router into the standard agent skill paths
(Claude Code / OpenClaw / Hermes / Pi / Cursor) when they exist on this machine
- Verifies the deployment is readable end-to-end

Expand Down Expand Up @@ -44,8 +44,7 @@ $RequiredMajor = 22
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..')
Set-Location $RepoRoot

$SkillName = 'houdiniswap'
$SkillSrc = Join-Path $RepoRoot "skills\$SkillName"
$SkillNames = @('houdiniswap', 'privacy-router')

$AgentTargets = @(
@{ Label = 'Claude Code (global)'; Parent = (Join-Path $env:USERPROFILE '.claude\skills') },
Expand All @@ -66,7 +65,7 @@ function Write-Fail($msg) { Write-Host " $msg" -ForegroundColor Red }

Write-Bold "==> HoudiniSkill installer"
Write-Info "Repo root: $RepoRoot"
Write-Info "Skill src: $SkillSrc"
Write-Info "Skills: $($SkillNames -join ', ')"
""

if (-not $LinkOnly) {
Expand Down Expand Up @@ -150,8 +149,9 @@ if ($NoLink) {

Write-Bold "==> Step 5/5: Installing the skill into agent skills directories"

function Link-One($label, $parentDir) {
$dest = Join-Path $parentDir $SkillName
function Link-One($label, $parentDir, $skillName) {
$skillSrc = Join-Path $RepoRoot "skills\$skillName"
$dest = Join-Path $parentDir $skillName

if (-not (Test-Path $parentDir)) {
Write-Info "[$label] $parentDir does not exist (agent not installed?). Skipping."
Expand All @@ -161,12 +161,12 @@ function Link-One($label, $parentDir) {
if (Test-Path $dest) {
$item = Get-Item $dest -Force
if ($item.LinkType -eq 'Junction' -or $item.LinkType -eq 'SymbolicLink') {
if ($item.Target -eq $SkillSrc -or ($item.Target -is [System.Array] -and $item.Target -contains $SkillSrc)) {
if ($item.Target -eq $skillSrc -or ($item.Target -is [System.Array] -and $item.Target -contains $skillSrc)) {
Write-Info "[$label] $dest already points to skill src. OK."
return $true
}
Write-Warn "[$label] $dest is a $($item.LinkType) to $($item.Target)"
Write-Warn " Replacing with $SkillSrc"
Write-Warn " Replacing with $skillSrc"
Remove-Item $dest -Force -Recurse
} else {
Write-Warn "[$label] $dest exists and is NOT a junction/symlink."
Expand All @@ -177,11 +177,11 @@ function Link-One($label, $parentDir) {

try {
if ($Copy) {
Copy-Item -Recurse $SkillSrc $dest
Write-Info "[$label] Copy-Item -Recurse $SkillSrc -> $dest"
Copy-Item -Recurse $skillSrc $dest
Write-Info "[$label] Copy-Item -Recurse $skillSrc -> $dest"
} else {
New-Item -ItemType Junction -Path $dest -Target $SkillSrc | Out-Null
Write-Info "[$label] Junction $dest -> $SkillSrc"
New-Item -ItemType Junction -Path $dest -Target $skillSrc | Out-Null
Write-Info "[$label] Junction $dest -> $skillSrc"
}
return $true
} catch {
Expand All @@ -193,8 +193,10 @@ function Link-One($label, $parentDir) {

$linkFailures = 0
foreach ($entry in $AgentTargets) {
if (-not (Link-One -label $entry.Label -parentDir $entry.Parent)) {
$linkFailures++
foreach ($skillName in $SkillNames) {
if (-not (Link-One -label $entry.Label -parentDir $entry.Parent -skillName $skillName)) {
$linkFailures++
}
}
}

Expand All @@ -205,7 +207,8 @@ Write-Bold "==> Project-scoped install (optional)"

cd <your-project>
New-Item -ItemType Directory -Force .claude\skills | Out-Null
New-Item -ItemType Junction -Path ".claude\skills\$SkillName" -Target "$SkillSrc"
New-Item -ItemType Junction -Path ".claude\skills\houdiniswap" -Target "$(Join-Path $RepoRoot 'skills\houdiniswap')"
New-Item -ItemType Junction -Path ".claude\skills\privacy-router" -Target "$(Join-Path $RepoRoot 'skills\privacy-router')"

"@ | Write-Host

Expand All @@ -231,7 +234,7 @@ if ($linkFailures -gt 0) {

What this did:

- Linked skills/houdiniswap/ into every agent's global skills directory
- Linked skills/houdiniswap/ and skills/privacy-router/ into every agent's global skills directory
that already exists on this machine (Claude Code / OpenClaw / Hermes /
Pi / Cursor).
- The skill auto-loads .env from the cloned repo root (via _client.mjs),
Expand All @@ -242,7 +245,7 @@ if ($linkFailures -gt 0) {
- Restart your agent (Claude Desktop / Code CLI / OpenClaw / Hermes /
Pi / Cursor) so it re-scans its skills directory.
- Try a prompt:
"Use the houdiniswap skill to quote 26 USDT (eth) -> USDT (bsc)."
"Use the privacy-router skill to find a private cross-chain route for 26 USDT (eth) -> USDT (bsc)."

Verify connectivity to the real API (optional, no funds spent):

Expand Down
Loading
Loading