Skill: botmechanic/nexwave-gateway (version 1.0.2)

--- name: nexwave-gateway description: Unified crosschain USDC balance via Circle Gateway + Circle Programmable Wallets. Deposit USDC on any supported chain, check your unified balance, and instantly mint USDC on any destination chain in <500ms — no bridging, no raw private keys. version: 1.1.0 tags: - usdc - circle - gateway - crosschain - defi - payments - agent-commerce - circle-wallet - arc author: nexwave requiredEnv: - CIRCLE_API_KEY - CIRCLE_ENTITY_SECRET - CIRCLE_WALLET_SET_ID dependencies: - eltontay/circle-wallet --- # Nexwave Gateway — Unified Crosschain USDC for OpenClaw Agents ## What This Skill Does Circle Gateway gives you a **single unified USDC balance** that is instantly accessible on any supported chain in under 500 milliseconds. Instead of holding separate USDC balances on Ethereum, Base, Avalanche, etc., you deposit USDC into Gateway on any chain and can mint it out on any other chain — instantly. This is fundamentally different from bridging. There are no liquidity pools, no bridge operators, no 15-minute waits. Gateway uses a deposit → sign burn intent → receive attestation → mint flow that executes in <500ms. ## Supported Chains (Testnet) | Chain | Domain ID | USDC Address | |---|---|---| | Ethereum Sepolia | 0 | [REDACTED_SECRET] | | Base Sepolia | 6 | [REDACTED_SECRET] | | Arc Testnet | 26 | [REDACTED_SECRET] | **Note:** Arc is Circle's purpose-built L1 blockchain where USDC is the native gas token. No separate gas token needed — USDC covers everything. Arc has the fastest Gateway finality at ~0.5 seconds. **Gateway Contracts (same address on all EVM chains):** - Gateway Wallet: [REDACTED_SECRET] - Gateway Minter: [REDACTED_SECRET] **Gateway API (testnet):** `https://gateway-api-testnet.circle.com/v1` ## Prerequisites 1. Circle Developer Account with API key and entity secret (https://console.circle.com) 2. The `circle-wallet` skill installed (`clawhub install eltontay/circle-wallet`) with a wallet set containing wallets on ETH-SEPOLIA, BASE-SEPOLIA, and ARC-TESTNET 3. Testnet USDC from https://faucet.circle.com (20 USDC per address per chain, every 2 hours) 4. Testnet ETH for gas on Sepolia/Base Sepolia (use Google's Sepolia faucet). On Arc, USDC is the native gas token — same faucet covers everything. 5. Node.js installed with the `viem`, `dotenv`, and `@circle-fin/developer-controlled-wallets` packages ## How To Use This Skill ### Step 1: Setup the Project Run the setup script to initialize the project with all dependencies: ```bash cd /path/to/nexwave-gateway && bash setup.sh ``` This creates a `gateway-app/` directory with all necessary files pre-configured. ### Step 2: Check Gateway Info and Your Balance ```bash cd gateway-app && node check-balance.js ``` This queries the Gateway API for supported chains and shows your unified USDC balance across all chains. ### Step 3: Deposit USDC into Gateway ```bash node deposit.js ``` This deposits USDC into the Gateway Wallet contract on Ethereum Sepolia and Arc Testnet. After deposit and chain finality, your unified balance is credited. Arc finalizes in ~0.5 seconds; Ethereum may take up to 20 minutes. ### Step 4: Transfer USDC Crosschain Instantly ```bash node transfer.js ``` This creates burn intents, signs them via Circle Programmable Wallets (MPC — no raw private keys), submits them to the Gateway API for attestation, and mints USDC on Base Sepolia. The attestation response typically arrives in <500ms. ## Key Concepts **Unified Balance:** After depositing USDC into Gateway on any chain, the Gateway system credits your address with a unified balance. This balance is not locked to any specific chain — it can be accessed on any supported chain. **Burn Intent:** To withdraw from your unified balance to a specific chain, you sign a "burn intent" — an EIP-712 typed data structure specifying the source chain, destination chain, amount, and recipient. Signing is done securely via Circle's MPC Programmable Wallets (no raw private keys). Gateway verifies your balance is sufficient and returns a signed attestation. **Circle Programmable Wallets:** Instead of raw private keys in `.env` files, this skill uses Circle's developer-controlled wallets backed by multi-party computation (MPC). The private key is never exposed — signing happens server-side via Circle's API. This makes the skill safe for agent use without risk of key exfiltration. **Attestation:** The Gateway API's signed proof that authorizes minting on the destination chain. You submit this attestation to the Gateway Minter contract on the destination chain to receive USDC. **Fees:** 0.5 basis points (0.005%) during the early access period (through June 30, 2026). Plus base gas fees for on-chain transactions. ## Flow Diagram ``` Agent deposits USDC on Chain A │ ▼ Gateway Wallet Contract (approve + deposit) │ ▼ Wait for chain finality → Unified balance credited │ ▼ Agent signs burn intent (EIP-712 via Circle MPC Wallets) │ ▼ Submit to Gateway API ──► Attestation returned (<500ms) │ ▼ Submit attestation to Gateway Minter on Chain B │ ▼ USDC minted on Chain B for recipient ``` ## Agent Use Cases - **Multi-chain arbitrage:** Access USDC on any chain instantly to capture price differences - **Cross-chain payments:** Pay for services on any chain from a single balance - **Treasury management:** Consolidate USDC from multiple chains into one balance - **Agent-to-agent commerce:** Accept payment on one chain, spend on another without delays - **Capital efficiency:** No need to pre-position USDC across chains ## Troubleshooting - **"Insufficient balance"**: Wait for chain finality after depositing. Ethereum takes ~20 min, Arc is ~0.5 seconds. - **"Gateway deposit not yet picked up"**: The Gateway API waits for block confirmations. Be patient on Ethereum. - **Gas errors**: On Ethereum/Base you need testnet ETH for gas. On Arc, USDC is the gas token — same faucet covers everything. - **Faucet limits**: You can get 20 USDC per address per chain every 2 hours from faucet.circle.com. ## References - Circle Gateway Docs: https://developers.circle.com/gateway - Circle Programmable Wallets: https://developers.circle.com/wallets - Circle Wallet Skill (ClawHub): https://clawhub.ai/eltontay/circle-wallet - Arc Testnet Docs: https://docs.arc.network - Gateway Quickstart: https://developers.circle.com/gateway/quickstarts/unified-balance-evm - Full Quickstart Code: https://github.com/circlefin/evm-gateway-contracts/tree/master/quickstart - Circle Faucet: https://faucet.circle.com - Gateway API Reference: https://gateway-api-testnet.circle.com/v1/info

path: setup.sh
content: #!/usr/bin/env bash
# Nexwave Gateway — One-command setup
# Initializes a Node.js project with Circle Gateway + Circle Programmable Wallets dependencies.
# No raw private keys needed — uses Circle's MPC-secured developer-controlled wallets.

set -e

echo "═══════════════════════════════════════════════"
echo "  Nexwave Gateway — Setup"
echo "═══════════════════════════════════════════════"

# Create project directory
mkdir -p gateway-app && cd gateway-app

# Initialize Node.js project (if needed)
if [ ! -f package.json ]; then
  echo '{ "type": "module" }' > package.json
fi

# Install dependencies
echo "📦 Installing dependencies..."
npm install viem dotenv @circle-fin/developer-controlled-wallets

# Copy skill files into the project
echo "📂 Copying skill files..."
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
for f in abis.js gateway-client.js circle-wallet-client.js typed-data.js setup-gateway.js check-balance.js deposit.js transfer.js; do
  cp "$SKILL_DIR/$f" ./ 2>/dev/null || true
done

# Create .env template (if not already configured)
if [ ! -f .env ]; then
  echo "🔧 Creating .env template..."
  cat > .env << 'EOF'
# Circle Programmable Wallets (from circle-wallet skill)
# Get these from https://console.circle.com → Developer Services
CIRCLE_API_KEY=
CIRCLE_ENTITY_SECRET=
CIRCLE_WALLET_SET_ID=
EOF
  echo "   ⚠️  Fill in .env with your Circle Developer Console credentials"
  echo "   💡 If you have the circle-wallet skill installed, these should"
  echo "      already be in your agent's environment."
else
  echo "   .env already exists — keeping your existing configuration"
fi

echo ""
echo "═══════════════════════════════════════════════"
echo "✅ Setup complete!"
echo ""
echo "Prerequisites:"
echo "  1. Install the circle-wallet skill: clawhub install eltontay/circle-wallet"
echo "  2. Set up a Circle Developer Account: https://console.circle.com"
echo "  3. Create a wallet set with wallets on ETH-SEPOLIA, BASE-SEPOLIA, ARC-TESTNET"
echo "  4. Get testnet USDC from https://faucet.circle.com"
echo ""
echo "Usage:"
echo "  node check-balance.js   # Check unified USDC balance"
echo "  node deposit.js         # Deposit USDC into Gateway"
echo "  node transfer.js        # Transfer USDC crosschain"
echo "═══════════════════════════════════════════════"


path: circle-wallet-client.js
content: import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";

///////////////////////////////////////////////////////////////////////////////
// Circle Programmable Wallets client for secure agent-native key management.
// Uses MPC-secured developer-controlled wallets — no raw private keys needed.
// This client wraps the Circle SDK to provide Gateway-specific operations:
// - EIP-712 signing for burn intents
// - Contract execution for approve, deposit, mint
// - Transaction polling for confirmation

// Maps our internal chain names to Circle blockchain identifiers
const BLOCKCHAIN_MAP = {
  sepolia: "ETH-SEPOLIA",
  baseSepolia: "BASE-SEPOLIA",
  arcTestnet: "ARC-TESTNET",
};

/**
 * Custom JSON serializer that handles BigInt values.
 * Circle's signTypedData API accepts EIP-712 typed data as a JSON string,
 * but JSON.stringify chokes on BigInt. We convert to string representation.
 */
function bigIntSerializer(key, value) {
  if (typeof value === "bigint") return value.toString();
  return value;
}

export class CircleWalletClient {
  constructor() {
    this.client = initiateDeveloperControlledWalletsClient({
      [REDACTED_SECRET],
      entitySecret: process.env.CIRCLE_ENTITY_SECRET,
    });
    this.walletSetId = process.env.CIRCLE_WALLET_SET_ID;
    this.wallets = {}; // chainName -> { walletId, address, blockchain }
    this.address = null;
  }

  /**
   * Initialize: discover wallets in the wallet set for each needed chain.
   * If a wallet doesn't exist for a chain, create one automatically.
   */
  async init() {
    console.log("🔐 Initializing Circle Programmable Wallets...");

    // List existing wallets in the set
    const response = await this.client.listWallets({
      walletSetId: this.walletSetId,
    });
    const existingWallets = response.data?.wallets || [];

    // Map existing wallets by blockchain
    for (const w of existingWallets) {
      for (const [chainName, blockchain] of Object.entries(BLOCKCHAIN_MAP)) {
        if (w.blockchain === blockchain && w.state === "LIVE") {
          this.wallets[chainName] = {
            walletId: w.id,
            address: w.address,
            blockchain,
          };
        }
      }
    }

    // Create wallets for any missing chains
    const missingChains = Object.entries(BLOCKCHAIN_MAP).filter(
      ([name]) => !this.wallets[name]
    );

    if (missingChains.length > 0) {
      console.log(
        `   Creating wallets for: ${missingChains.map(([, b]) => b).join(", ")}`
      );
      const createResponse = await this.client.createWallets({
        blockchains: missingChains.map(([, blockchain]) => blockchain),
        count: 1,
        walletSetId: this.walletSetId,
      });
      const created = createResponse.data?.wallets || [];
      for (const w of created) {
        for (const [chainName, blockchain] of missingChains) {
          if (w.blockchain === blockchain) {
            this.wallets[chainName] = {
              walletId: w.id,
              address: w.address,
              blockchain,
            };
          }
        }
      }
    }

    // All wallets in a set share the same address
    const firstWallet = Object.values(this.wallets)[0];
    if (!firstWallet) {
      throw new Error("No wallets found or created. Check your CIRCLE_WALLET_SET_ID.");
    }
    this.address = firstWallet.address;

    console.log(`   ✅ Wallet address: ${this.address}`);
    for (const [name, w] of Object.entries(this.wallets)) {
      console.log(`   • ${w.blockchain}: ${w.walletId}`);
    }

    return this;
  }

  /**
   * Get wallet ID for a given chain name.
   */
  walletIdForChain(chainName) {
    const w = this.wallets[chainName];
    if (!w) throw new Error(`No wallet found for chain: ${chainName}`);
    return w.walletId;
  }

  /**
   * Sign EIP-712 typed data using Circle's MPC signing.
   * Returns the signature as a hex string (0x-prefixed).
   *
   * @param {string} chainName - e.g. "sepolia", "arcTestnet"
   * @param {Object} typedData - Full EIP-712 typed data { types, domain, primaryType, message }
   * @param {string} [memo] - Human-readable description
   * @returns {Promise<string>} Hex-encoded signature
   */
  async signTypedData(chainName, typedData, memo) {
    const walletId = this.walletIdForChain(chainName);
    const response = await this.client.signTypedData({
      walletId,
      data: JSON.stringify(typedData, bigIntSerializer),
      memo: memo || "Gateway burn intent",
    });

    let sig = response.data?.signature;
    if (!sig) throw new Error("signTypedData returned no signature");

    // Ensure hex format (Circle may return base64 for some chains)
    if (!sig.startsWith("0x")) {
      sig = "0x" + Buffer.from(sig, "base64").toString("hex");
    }
    return sig;
  }

  /**
   * Execute a smart contract function via Circle's transaction API.
   * Submits the transaction and polls until it completes.
   *
   * @param {string} chainName - e.g. "sepolia", "baseSepolia", "arcTestnet"
   * @param {Object} opts
   * @param {string} opts.contractAddress - Target contract
   * @param {string} opts.functionSignature - e.g. "approve(address,uint256)"
   * @param {Array} opts.params - ABI parameters
   * @param {string} [opts.feeLevel] - "LOW", "MEDIUM", or "HIGH"
   * @returns {Promise<Object>} Completed transaction object
   */
  async executeContract(
    chainName,
    { contractAddress, functionSignature, params, feeLevel = "MEDIUM" }
  ) {
    const walletId = this.walletIdForChain(chainName);
    const response = await this.client.createContractExecutionTransaction({
      walletId,
      contractAddress,
      abiFunctionSignature: functionSignature,
      abiParameters: params,
      fee: { type: "level", config: { feeLevel } },
    });

    const txId = response.data?.id;
    if (!txId) throw new Error("createContractExecutionTransaction returned no id");

    // Poll for completion
    return this.waitForTransaction(txId);
  }

  /**
   * Poll a transaction until it reaches a terminal state.
   */
  async waitForTransaction(txId, timeoutMs = 120_000) {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
      const response = await this.client.getTransaction({ id: txId });
      const tx = response.data?.transaction;

      if (tx?.state === "COMPLETE") return tx;
      if (tx?.state === "FAILED" || tx?.state === "CANCELLED" || tx?.state === "DENIED") {
        throw new Error(
          `Transaction ${txId} ${tx.state}: ${tx.errorReason || "unknown error"}`
        );
      }

      await new Promise((r) => setTimeout(r, 2000)); // Poll every 2s
    }
    throw new Error(`Transaction ${txId} timed out after ${timeoutMs}ms`);
  }
}


path: check-balance.js
content: import { walletAddress } from "./setup-gateway.js";
import { GatewayClient } from "./gateway-client.js";

///////////////////////////////////////////////////////////////////////////////
// Check Gateway info and unified USDC balance
// This script queries the Gateway API to show:
// 1. Which chains are supported (wallet + minter contracts)
// 2. Your unified USDC balance across all chains

const gatewayClient = new GatewayClient();

console.log("═══════════════════════════════════════════════");
console.log("  Nexwave Gateway — Balance Check");
console.log("═══════════════════════════════════════════════");
console.log(`Account: ${walletAddress}\n`);

// Fetch supported chain info
console.log("📡 Fetching Gateway API info...");
const info = await gatewayClient.info();
console.log(`   API Version: ${info.version}`);
console.log("   Supported chains:");
for (const domain of info.domains) {
  const hasWallet = "walletContract" in domain;
  const hasMinter = "minterContract" in domain;
  console.log(
    `   • ${domain.chain} ${domain.network} — wallet: ${hasWallet ? "✅" : "❌"}, minter: ${hasMinter ? "✅" : "❌"}`
  );
}

// Check unified balance
console.log("\n💰 Checking unified USDC balance...");
const { balances } = await gatewayClient.balances("USDC", walletAddress);

let totalBalance = 0;
for (const balance of balances) {
  const chainName =
    GatewayClient.CHAINS[balance.domain] || `Domain ${balance.domain}`;
  const amount = parseFloat(balance.balance);
  totalBalance += amount;
  console.log(`   • ${chainName}: ${balance.balance} USDC`);
}

console.log(`\n   📊 Total unified balance: ${totalBalance.toFixed(6)} USDC`);
console.log("\n═══════════════════════════════════════════════");

process.exit(0);


path: deposit.js
content: import {
  circleWallet,
  walletAddress,
  ethereum,
  arc,
} from "./setup-gateway.js";

///////////////////////////////////////////////////////////////////////////////
// Deposit USDC into Circle Gateway
// This script deposits USDC into the Gateway Wallet contract on each chain,
// creating a unified crosschain USDC balance.
//
// Uses Circle Programmable Wallets for transaction signing — no raw private keys.
// IMPORTANT: Do NOT use ERC-20 transfer() directly — that will lose funds.
// The correct flow is: approve() → deposit() on the Gateway Wallet contract.

const DEPOSIT_AMOUNT = "10000000"; // 10 USDC (6 decimals, as string for Circle SDK)
const DEPOSIT_DISPLAY = "10";

console.log("═══════════════════════════════════════════════");
console.log("  Nexwave Gateway — Deposit USDC");
console.log("═══════════════════════════════════════════════");
console.log(`Account: ${walletAddress}`);
console.log(`Deposit amount: ${DEPOSIT_DISPLAY} USDC per chain\n`);

for (const chain of [ethereum, arc]) {
  console.log(`\n🔗 ${chain.name} (Domain ${chain.domain})`);
  console.log("─".repeat(40));

  // Check USDC balance (read-only via viem)
  console.log("   Checking USDC balance...");
  const balance = await chain.usdc.read.balanceOf([walletAddress]);
  console.log(`   Balance: ${Number(balance) / 1e6} USDC`);

  if (balance < BigInt(DEPOSIT_AMOUNT)) {
    console.error(`   ❌ Insufficient USDC on ${chain.name}!`);
    console.error("   Please top up at https://faucet.circle.com");
    process.exit(1);
  }

  try {
    // Step 1: Approve Gateway Wallet to spend USDC
    console.log("   Approving Gateway Wallet for USDC...");
    const approveTx = await circleWallet.executeContract(chain.chainName, {
      contractAddress: chain.usdcAddress,
      functionSignature: "approve(address,uint256)",
      params: [chain.gatewayWalletAddress, DEPOSIT_AMOUNT],
    });
    console.log(`   ✅ Approved — tx: ${approveTx.txHash}`);

    // Step 2: Deposit USDC into Gateway Wallet
    console.log("   Depositing USDC into Gateway Wallet...");
    const depositTx = await circleWallet.executeContract(chain.chainName, {
      contractAddress: chain.gatewayWalletAddress,
      functionSignature: "deposit(address,uint256)",
      params: [chain.usdcAddress, DEPOSIT_AMOUNT],
    });
    console.log(`   ✅ Deposited — tx: ${depositTx.txHash}`);
  } catch (error) {
    console.error("   ❌ Error:", error.message || error);
    process.exit(1);
  }
}

console.log("\n═══════════════════════════════════════════════");
console.log("✅ Deposits complete!");
console.log("   Arc: finality is ~0.5 seconds (1 block)");
console.log("   Ethereum: wait ~20 min for finality");
console.log("   Then run: node check-balance.js");
console.log("═══════════════════════════════════════════════");

process.exit(0);


path: gateway-client.js
content: ///////////////////////////////////////////////////////////////////////////////
// Lightweight API client for Circle Gateway
// Docs: https://developers.circle.com/gateway

export class GatewayClient {
  static GATEWAY_API_BASE_URL = "https://gateway-api-testnet.circle.com/v1";

  // Domain identifiers for supported blockchains (same as CCTP domains)
  // See: https://developers.circle.com/cctp/cctp-supported-blockchains#cctp-supported-domains
  static DOMAINS = {
    ethereum: 0,
    mainnet: 0,
    sepolia: 0,
    avalanche: 1,
    avalancheFuji: 1,
    base: 6,
    baseSepolia: 6,
    arc: 26,
    arcTestnet: 26,
  };

  // Human-readable chain names by domain
  static CHAINS = {
    0: "Ethereum",
    1: "Avalanche",
    6: "Base",
    26: "Arc",
  };

  // Get info about supported chains and contracts
  async info() {
    return this.#get("/info");
  }

  // Check unified USDC balance for a given depositor across specified domains
  // If no domains are specified, checks all supported domains
  async balances(token, depositor, domains) {
    if (!domains) {
      domains = Object.keys(GatewayClient.CHAINS).map((d) => parseInt(d));
    }
    return this.#post("/balances", {
      token,
      sources: domains.map((domain) => ({ depositor, domain })),
    });
  }

  // Submit burn intents to receive attestation for crosschain mint
  async transfer(body) {
    return this.#post("/transfer", body);
  }

  async #get(path) {
    const url = GatewayClient.GATEWAY_API_BASE_URL + path;
    const response = await fetch(url);
    return response.json();
  }

  async #post(path, body) {
    const url = GatewayClient.GATEWAY_API_BASE_URL + path;
    const headers = { "Content-Type": "application/json" };
    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(body, (_key, value) =>
        typeof value === "bigint" ? value.toString() : value,
      ),
    });
    return response.json();
  }
}


path: README.md
content: # nexwave-gateway 🌐

**Unified Crosschain USDC for OpenClaw Agents — Powered by Circle Gateway**

> The only OpenClaw skill that gives agents a single USDC balance accessible on any chain in <500ms. No bridging. No liquidity pools. No waiting.

## What It Does

`nexwave-gateway` enables OpenClaw agents to use [Circle Gateway](https://www.circle.com/gateway) for instant crosschain USDC operations. Instead of managing separate USDC balances on Ethereum, Base, Avalanche, etc., your agent deposits USDC into Gateway on any chain and can instantly access it on any other supported chain.

### The Flow

```
1. Deposit USDC on Chain A → Gateway credits unified balance
2. Sign burn intent → Gateway returns attestation (<500ms)
3. Submit attestation on Chain B → USDC minted for you
```

### Why This Matters for Agents

- **Capital efficiency**: No pre-positioning USDC across chains
- **Speed**: <500ms crosschain transfers vs. 15-20 minute bridge waits
- **Simplicity**: One balance, accessible everywhere
- **Non-custodial**: Funds only move with your signature

## Quick Start

### 1. Install

```bash
clawhub install nexwave-gateway
# or manually:
git clone <repo-url> ~/.openclaw/skills/nexwave-gateway
```

### 2. Configure

Install the Circle Wallet skill and set your credentials:

```bash
clawhub install eltontay/circle-wallet

# Set Circle Wallet credentials (from https://console.circle.com)
export CIRCLE_API_KEY=your_api_key
export CIRCLE_ENTITY_SECRET=your_entity_secret
export CIRCLE_WALLET_SET_ID=your_wallet_set_id
```

No raw private keys needed — uses Circle's MPC-secured developer-controlled wallets.

### 3. Setup & Run

```bash
cd ~/.openclaw/skills/nexwave-gateway
bash setup.sh
cd gateway-app
node check-balance.js   # See your unified balance
node deposit.js         # Deposit USDC into Gateway
node transfer.js        # Transfer crosschain to Base
```

### 4. Get Testnet USDC

Visit [faucet.circle.com](https://faucet.circle.com) — 20 USDC per address per chain, every 2 hours.

## Supported Chains (Testnet)

| Chain | Network | Domain ID | Notes |
|---|---|---|---|
| Ethereum | Sepolia | 0 | ~20 min finality |
| Base | Sepolia | 6 | ~13-19 min finality |
| Arc | Testnet | 26 | ~0.5s finality, USDC-native gas |

## Files

| File | Description |
|---|---|
| `SKILL.md` | OpenClaw skill definition with instructions |
| `setup.sh` | Project initialization script |
| `abis.js` | Gateway Wallet & Minter contract ABIs |
| `gateway-client.js` | Lightweight Circle Gateway API client |
| `circle-wallet-client.js` | Circle Programmable Wallets SDK wrapper (MPC signing + transactions) |
| `typed-data.js` | EIP-712 typed data for burn intents |
| `setup-gateway.js` | Chain clients and contract initialization |
| `check-balance.js` | Query unified USDC balance |
| `deposit.js` | Deposit USDC into Gateway |
| `transfer.js` | Full crosschain transfer demo |

## Circle Products Used

- **[Circle Gateway](https://developers.circle.com/gateway)** — Unified crosschain USDC balance
- **[Circle Programmable Wallets](https://developers.circle.com/wallets)** — MPC-secured developer-controlled wallets (no raw private keys)
- **[Arc L1](https://docs.arc.network)** — Circle's purpose-built blockchain where USDC is the native gas token
- **[USDC](https://www.circle.com/usdc)** — Native stablecoin on all supported chains
- **[Circle Faucet](https://faucet.circle.com)** — Testnet USDC

## Architecture

```
┌─────────────────────────────────────────────┐
│           OpenClaw Agent (NexBot)            │
│  ┌───────────────────────────────────────┐  │
│  │       nexwave-gateway Skill           │  │
│  │  check-balance │ deposit │ transfer   │  │
│  └───────────┬───────────────────────────┘  │
└──────────────┼──────────────────────────────┘
               │
    ┌──────────┴──────────┐
    ▼                     ▼
┌────────────┐    ┌──────────────────┐
│  Gateway   │    │   Gateway API    │
│  Wallet    │    │  (attestations)  │
│  Contract  │    │  <500ms response │
│ (on-chain) │    │                  │
└────────────┘    └──────────────────┘
    │                     │
    ▼                     ▼
┌──────────────────────────────────┐
│       Gateway Minter Contract    │
│    (mints USDC on dest chain)    │
└──────────────────────────────────┘
```

## Why Gateway > Bridging

| | Traditional Bridge | Circle Gateway |
|---|---|---|
| **Speed** | 13-19 minutes (Ethereum) | <500ms |
| **Trust** | Third-party bridge operators | Circle (USDC issuer) |
| **Liquidity** | Fragmented pools | Unified balance |
| **Capital** | Pre-positioned per chain | On-demand, any chain |
| **USDC type** | Often "wrapped" | Always native USDC |

## Built By

**[Nexwave](https://nexwave.dev)** — AI Trading Agent Factory

Built for the [USDC Agentic Hackathon](https://www.moltbook.com/post/b021cdea-de86-4460-8c4b-8539842423fe) on Moltbook.

## References

- [Circle Gateway Docs](https://developers.circle.com/gateway)
- [Gateway Quickstart](https://developers.circle.com/gateway/quickstarts/unified-balance-evm)
- [Full Quickstart Code](https://github.com/circlefin/evm-gateway-contracts/tree/master/quickstart)
- [Gateway Blog Post](https://www.circle.com/blog/circle-gateway-redefining-crosschain-ux)
- [Machine-to-Machine Micropayments](https://www.circle.com/blog/enabling-machine-to-machine-micropayments-with-gateway-and-usdc)

## License

MIT


path: typed-data.js
content: import { randomBytes } from "node:crypto";
import { pad, zeroAddress, maxUint256 } from "viem";

///////////////////////////////////////////////////////////////////////////////
// EIP-712 typed data utilities for Gateway burn intents
// Burn intents are the signed messages that authorize Gateway to burn USDC
// from your unified balance on a source chain and mint on a destination chain.

const domain = { name: "GatewayWallet", version: "1" };

const EIP712Domain = [
  { name: "name", type: "string" },
  { name: "version", type: "string" },
];

const TransferSpec = [
  { name: "version", type: "uint32" },
  { name: "sourceDomain", type: "uint32" },
  { name: "destinationDomain", type: "uint32" },
  { name: "sourceContract", type: "bytes32" },
  { name: "destinationContract", type: "bytes32" },
  { name: "sourceToken", type: "bytes32" },
  { name: "destinationToken", type: "bytes32" },
  { name: "sourceDepositor", type: "bytes32" },
  { name: "destinationRecipient", type: "bytes32" },
  { name: "sourceSigner", type: "bytes32" },
  { name: "destinationCaller", type: "bytes32" },
  { name: "value", type: "uint256" },
  { name: "salt", type: "bytes32" },
  { name: "hookData", type: "bytes" },
];

const BurnIntent = [
  { name: "maxBlockHeight", type: "uint256" },
  { name: "maxFee", type: "uint256" },
  { name: "spec", type: "TransferSpec" },
];

const BurnIntentSet = [{ name: "intents", type: "BurnIntent[]" }];

function addressToBytes32(address) {
  return pad(address.toLowerCase(), { size: 32 });
}

/**
 * Construct a burn intent for transferring USDC from one chain to another.
 *
 * @param {Object} params
 * @param {string} params.walletAddress - The wallet address (hex string)
 * @param {Object} params.from - Source chain config (from setup-gateway.js)
 * @param {Object} params.to - Destination chain config (from setup-gateway.js)
 * @param {number} params.amount - Amount of USDC to transfer (in human units, e.g. 2 = 2 USDC)
 * @param {string} [params.recipient] - Destination address (defaults to walletAddress)
 */
export function burnIntent({ walletAddress, from, to, amount, recipient }) {
  return {
    maxBlockHeight: maxUint256, // Needs to be at least 7 days in the future
    maxFee: 2_010000n, // 2.01 USDC covers fees on any chain
    spec: {
      version: 1,
      sourceDomain: from.domain,
      destinationDomain: to.domain,
      sourceContract: from.gatewayWalletAddress,
      destinationContract: from.gatewayMinterAddress,
      sourceToken: from.usdcAddress,
      destinationToken: to.usdcAddress,
      sourceDepositor: walletAddress,
      destinationRecipient: recipient || walletAddress,
      sourceSigner: walletAddress,
      destinationCaller: zeroAddress, // Anyone can submit the attestation
      value: BigInt(Math.floor(amount * 1e6)), // Convert to USDC atomic units (6 decimals)
      salt: "0x" + randomBytes(32).toString("hex"),
      hookData: "0x", // No hook data
    },
  };
}

/**
 * Create EIP-712 typed data for a single burn intent (for signing).
 */
export function burnIntentTypedData(intent) {
  return {
    types: { EIP712Domain, TransferSpec, BurnIntent },
    domain,
    primaryType: "BurnIntent",
    message: {
      ...intent,
      spec: {
        ...intent.spec,
        sourceContract: addressToBytes32(intent.spec.sourceContract),
        destinationContract: addressToBytes32(intent.spec.destinationContract),
        sourceToken: addressToBytes32(intent.spec.sourceToken),
        destinationToken: addressToBytes32(intent.spec.destinationToken),
        sourceDepositor: addressToBytes32(intent.spec.sourceDepositor),
        destinationRecipient: addressToBytes32(intent.spec.destinationRecipient),
        sourceSigner: addressToBytes32(intent.spec.sourceSigner),
        destinationCaller: addressToBytes32(
          intent.spec.destinationCaller ?? zeroAddress
        ),
      },
    },
  };
}

/**
 * Create EIP-712 typed data for a set of burn intents (batch transfer).
 */
export function burnIntentSetTypedData({ intents }) {
  return {
    types: { EIP712Domain, TransferSpec, BurnIntent, BurnIntentSet },
    domain,
    primaryType: "BurnIntentSet",
    message: {
      intents: intents.map((intent) => burnIntentTypedData(intent).message),
    },
  };
}


path: abis.js
content: ///////////////////////////////////////////////////////////////////////////////
// ABIs for Circle Gateway contracts
// Only includes functions needed for deposit, balance check, and mint operations.
// Full ABI reference: https://developers.circle.com/gateway/references/contract-interfaces-and-events

export const gatewayWalletAbi = [
  {
    type: "function",
    name: "deposit",
    inputs: [
      { name: "token", type: "address", internalType: "address" },
      { name: "value", type: "uint256", internalType: "uint256" },
    ],
    outputs: [],
    stateMutability: "nonpayable",
  },
];

export const gatewayMinterAbi = [
  {
    type: "function",
    name: "gatewayMint",
    inputs: [
      { name: "attestationPayload", type: "bytes", internalType: "bytes" },
      { name: "signature", type: "bytes", internalType: "bytes" },
    ],
    outputs: [],
    stateMutability: "nonpayable",
  },
];


path: setup-gateway.js
content: import "dotenv/config";
import {
  createPublicClient,
  getContract,
  http,
  erc20Abi,
  defineChain,
} from "viem";
import * as chains from "viem/chains";
import { GatewayClient } from "./gateway-client.js";
import { CircleWalletClient } from "./circle-wallet-client.js";
import { gatewayWalletAbi, gatewayMinterAbi } from "./abis.js";

///////////////////////////////////////////////////////////////////////////////
// Chain configuration for Circle Gateway testnet
// Uses Circle Programmable Wallets for secure MPC-based key management.
// No raw private keys — the Circle SDK handles signing via API.

const gatewayWalletAddress = [REDACTED_SECRET];
const gatewayMinterAddress = [REDACTED_SECRET];

export const usdcAddresses = {
  sepolia: [REDACTED_SECRET],
  baseSepolia: [REDACTED_SECRET],
  arcTestnet: "0x3600000000000000000000000000000000000000",
};

export { gatewayWalletAddress, gatewayMinterAddress };

// Arc Testnet is not in viem's built-in chains, so we define it manually.
// Arc is Circle's purpose-built L1 where USDC is the native gas token.
const arcTestnet = defineChain({
  id: 5042002,
  name: "Arc Testnet",
  nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://rpc.testnet.arc.network"] },
  },
  blockExplorers: {
    default: { name: "ArcScan", url: "https://testnet.arcscan.app" },
  },
  testnet: true,
});

function setup(chainName) {
  const chain = chainName === "arcTestnet" ? arcTestnet : chains[chainName];
  const client = createPublicClient({
    chain,
    transport:
      chainName === "baseSepolia"
        ? http("https://sepolia-preconf.base.org")
        : chainName === "arcTestnet"
          ? http("https://rpc.testnet.arc.network")
          : http(),
  });

  return {
    client,
    chainName, // Internal key used by CircleWalletClient
    name: chain.name,
    domain: GatewayClient.DOMAINS[chainName],
    currency: chain.nativeCurrency.symbol,
    usdcAddress: usdcAddresses[chainName],
    gatewayWalletAddress,
    gatewayMinterAddress,
    usdc: getContract({
      address: usdcAddresses[chainName],
      abi: erc20Abi,
      client,
    }),
    gatewayWallet: getContract({
      address: gatewayWalletAddress,
      abi: gatewayWalletAbi,
      client,
    }),
    gatewayMinter: getContract({
      address: gatewayMinterAddress,
      abi: gatewayMinterAbi,
      client,
    }),
  };
}

// Initialize Circle Programmable Wallet (MPC-secured, no raw private keys)
export const circleWallet = await new CircleWalletClient().init();
export const walletAddress = circleWallet.address;

// Initialize read-only clients for each testnet chain
export const ethereum = setup("sepolia");
export const base = setup("baseSepolia");
export const arc = setup("arcTestnet");


path: transfer.js
content: import {
  circleWallet,
  walletAddress,
  ethereum,
  base,
  arc,
} from "./setup-gateway.js";
import { GatewayClient } from "./gateway-client.js";
import { burnIntent, burnIntentTypedData } from "./typed-data.js";

///////////////////////////////////////////////////////////////////////////////
// Transfer USDC from Unified Balance to Base Sepolia
// This script demonstrates the full Gateway crosschain transfer flow:
// 1. Check unified balance across chains
// 2. Construct burn intents (source chain + amount)
// 3. Sign burn intents with EIP-712 via Circle Programmable Wallets (MPC)
// 4. Submit to Gateway API → receive attestation (<500ms)
// 5. Mint USDC on destination chain (Base Sepolia)

const gatewayClient = new GatewayClient();

console.log("═══════════════════════════════════════════════");
console.log("  Nexwave Gateway — Crosschain Transfer");
console.log("═══════════════════════════════════════════════");
console.log(`Account: ${walletAddress}\n`);

// Step 1: Check supported chains
console.log("📡 Fetching Gateway API info...");
const info = await gatewayClient.info();
for (const domain of info.domains) {
  console.log(
    `   • ${domain.chain} ${domain.network}`,
    `(wallet: ${"walletContract" in domain}, minter: ${"minterContract" in domain})`
  );
}

// Step 2: Check balances
console.log("\n💰 Checking unified USDC balance...");
const { balances } = await gatewayClient.balances("USDC", walletAddress);
for (const balance of balances) {
  const chainName =
    GatewayClient.CHAINS[balance.domain] || `Domain ${balance.domain}`;
  console.log(`   • ${chainName}: ${balance.balance} USDC`);
}

// Amounts to transfer from each source chain
const fromEthereumAmount = 2;
const fromArcAmount = 3;

// Verify balances are sufficient
const arcBalance = balances.find(
  (b) => b.domain === GatewayClient.DOMAINS.arc
)?.balance;
if (!arcBalance || parseFloat(arcBalance) < fromArcAmount) {
  console.error(
    "\n❌ Arc balance insufficient. Wait for deposit finality (~0.5s)."
  );
  process.exit(1);
}
console.log("\n   ✅ Arc deposit confirmed");

const ethereumBalance = balances.find(
  (b) => b.domain === GatewayClient.DOMAINS.ethereum
)?.balance;
if (!ethereumBalance || parseFloat(ethereumBalance) < fromEthereumAmount) {
  console.error(
    "\n❌ Ethereum balance insufficient. Ethereum takes ~20 min to finalize."
  );
  process.exit(1);
}
console.log("   ✅ Ethereum deposit confirmed");

// Step 3: Construct burn intents
console.log("\n🔥 Constructing burn intents...");
console.log(`   • ${fromEthereumAmount} USDC from Ethereum → Base`);
console.log(`   • ${fromArcAmount} USDC from Arc → Base`);

const burnIntents = [
  burnIntent({
    walletAddress,
    from: ethereum,
    to: base,
    amount: fromEthereumAmount,
    recipient: walletAddress,
  }),
  burnIntent({
    walletAddress,
    from: arc,
    to: base,
    amount: fromArcAmount,
    recipient: walletAddress,
  }),
];

// Step 4: Sign burn intents via Circle Programmable Wallets (MPC-secured EIP-712)
console.log("\n🔐 Signing burn intents via Circle MPC Wallets...");
const request = await Promise.all(
  burnIntents.map(async (intent, i) => {
    const typedData = burnIntentTypedData(intent);
    // Sign using the source chain's wallet (Circle routes to correct MPC key shard)
    const sourceChain = i === 0 ? "sepolia" : "arcTestnet";
    const signature = await circleWallet.signTypedData(
      sourceChain,
      typedData,
      `Gateway burn intent: ${i === 0 ? fromEthereumAmount : fromArcAmount} USDC from ${i === 0 ? "Ethereum" : "Arc"} to Base`
    );
    return { burnIntent: typedData.message, signature };
  })
);
console.log("   ✅ Burn intents signed (MPC — no raw private keys used)");

// Step 5: Submit to Gateway API for attestation
console.log("\n📤 Requesting attestation from Gateway API...");
const start = performance.now();
const response = await gatewayClient.transfer(request);
const elapsed = performance.now() - start;

if (response.success === false) {
  console.error(`\n❌ Gateway API error: ${response.message}`);
  process.exit(1);
}

console.log(`   ✅ Attestation received in ${elapsed.toFixed(2)}ms`);
console.log(`   ⚡ That's ${elapsed < 500 ? "under" : "over"} 500ms!`);

// Step 6: Mint USDC on Base Sepolia via Circle Wallet
console.log("\n🪙 Minting USDC on Base Sepolia...");
const { attestation, signature } = response;
const mintTx = await circleWallet.executeContract("baseSepolia", {
  contractAddress: base.gatewayMinterAddress,
  functionSignature: "gatewayMint(bytes,bytes)",
  params: [attestation, signature],
});

console.log(`   ✅ Minted! Transaction: ${mintTx.txHash}`);
console.log(`   🔗 Explorer: https://sepolia.basescan.org/tx/${mintTx.txHash}`);

console.log("\n═══════════════════════════════════════════════");
console.log(
  `✅ Successfully transferred ${fromEthereumAmount + fromArcAmount} USDC to Base Sepolia!`
);
console.log("   Source: Ethereum (2 USDC) + Arc (3 USDC)");
console.log("   Destination: Base Sepolia");
console.log(`   Attestation latency: ${elapsed.toFixed(2)}ms`);
console.log("   Signing: Circle MPC Wallets (no raw keys)");
console.log("═══════════════════════════════════════════════");

process.exit(0);


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)