Skill: orca-labs-sudo/claw-pay (version 0.3.4)

--- name: claw-pay description: "Autonomous x402 USDC payments on Base L2 — buy from WooCommerce shops, APIs, and any x402 service within your spending limit. ✓ Official WooCommerce plugin live on WordPress.org (claw-pay-gateway). Facilitator: https://claw-pay.org" version: 0.3.3 tags: [x402, payments, usdc, base, web3, autonomous, woocommerce, shopping] --- # claw-pay — x402 Payment Skill You have payment capabilities for x402-gated services. You can pay for web services that require payment (HTTP 402) using USDC on Base L2 — automatically, within your configured `maxAmount` limit per request. --- ## When to use this skill Use this skill whenever: - You receive an HTTP 402 response from a service you are trying to access - A service's documentation says it requires x402 payment - You need to pay for API calls, data access, AI inference, or any other metered service Pay automatically when a service returns HTTP 402 — but only up to your configured `maxAmount` limit. If no `maxAmount` is set, ask the user before paying. Never exceed the limit without explicit user confirmation. --- ## Where you can buy from This skill enables autonomous payment on three distinct channels: 1. **WooCommerce shops** — any WooCommerce store that has installed the official **claw-pay Gateway** plugin (available on the WordPress plugin directory: https://wordpress.org/plugins/claw-pay-gateway). When you browse such a shop and attempt checkout, the gateway responds with HTTP 402 and you settle automatically. 2. **x402-gated APIs** — any HTTP endpoint that returns 402 Payment Required. Use `payAndFetch()` exactly like `fetch()`; payment is handled transparently. 3. **Direct USDC transfers** — send USDC to any Base L2 wallet address with `transfer()`. No facilitator, no fee, just on-chain. --- ## Setup (one-time, first use) ### 1. Create a wallet ```javascript const { createWallet } = require('./src/wallet'); const [REDACTED_SECRET]; const { address, mnemonic } = await createWallet(password); console.log('Wallet address:', address); // IMPORTANT: Save the mnemonic somewhere safe — it is shown only once ``` ### 2. Fund the wallet Send USDC to your wallet address on Base L2 (mainnet) or Base Sepolia (testnet). You can buy USDC on Coinbase and send it to your wallet address. Minimum recommended balance: $1.00 USDC (covers ~1000 micro-payments) ### 3. Set environment variables ``` CLAW_PAY_WALLET_PASSWORD=<your-secret-password> CLAW_PAY_NETWORK=base-mainnet # or base-sepolia for testing CLAW_PAY_FACILITATOR_URL=https://claw-pay.org ``` --- ## Usage ### Automatic — just replace fetch() ```javascript const { payAndFetch } = require('./src/pay'); const { loadWallet } = require('./src/wallet'); const wallet = await loadWallet(process.env.CLAW_PAY_WALLET_PASSWORD); // Works exactly like fetch() but handles 402 automatically const response = await payAndFetch( 'https://api.example.com/generate', { method: 'POST', body: JSON.stringify({ prompt: 'Hello' }) }, { wallet, maxAmount: 0.10, // Never pay more than $0.10 per request } ); const data = await response.json(); ``` ### Check balance before starting ```javascript const { loadWallet, getTokenBalance, getStoredAddress } = require('./src/wallet'); const { ethers } = require('ethers'); const { NETWORKS } = require('./src/pay'); const net = NETWORKS['base-mainnet']; const provider = new ethers.JsonRpcProvider(net.rpcUrl); const address = getStoredAddress(); // no password needed const { formatted, symbol } = await getTokenBalance(address, net.usdcAddress, provider); console.log(`Balance: ${formatted} ${symbol}`); ``` --- ## How payment works (for your reference) 1. You call `payAndFetch(url, options, { wallet, maxAmount })` 2. If the server returns 200 OK → response is returned as-is, no payment 3. If the server returns 402 Payment Required: a. Parse payment requirements (amount, recipient, network) b. Sign an ERC-3009 authorization offline (no gas, no broadcast yet) c. Call facilitator `/verify` — confirm payment is valid d. Attach signed payment as `X-PAYMENT` header e. Retry the original request f. The server submits the payment on-chain via the facilitator 4. Response with 200 OK + `X-PAYMENT-RESPONSE` header is returned **Payment routing:** 97% goes to the service provider, 3% facilitator fee. **Gas:** Paid by the facilitator, not you. Your only cost is the USDC amount. --- ## Safety rules - `maxAmount` default: **1.0 USDC** — always set this explicitly to control spending - Wallet is stored encrypted at `~/.claw-pay/wallet.json` (AES-256, ethers keystore v3) - Private key never leaves your machine - Each payment uses a unique nonce — replay attacks are impossible - Payments expire after 5 minutes if not settled --- ## Trust model — what goes to the facilitator The facilitator receives one call per payment: a `POST /verify` with the signed ERC-3009 authorization payload. This is the same data that later goes to the seller's server as the `X-PAYMENT` header — it is the payment itself, not a private key. **What the facilitator sees:** your wallet address, recipient address, USDC amount, nonce, and ERC-3009 signature. **What the facilitator never sees:** your private key, your mnemonic, your wallet password. **What the facilitator does:** validates the signature is well-formed and the amount matches — nothing else. Settlement happens on-chain by the seller's server, not by the facilitator. The facilitator URL **must use HTTPS** — an HTTP URL is rejected at startup. You can self-host the facilitator (open-source at github.com/orca-labs-sudo/claw-pay) and point `CLAW_PAY_FACILITATOR_URL` to your own instance. --- ## Important — legal notice **claw-pay is a software library. It is not a financial service, wallet provider, or payment operator.** - Your private key never leaves your device. - We never hold, touch, or control your funds at any time. - Direct transfers (below) go straight on-chain — we are not involved in any way. - You are solely responsible for your transactions and applicable laws in your jurisdiction. --- ## Direct transfers (Wallet-to-Wallet) Send USDC directly to any address — no service, no facilitator, no fee. ```javascript const { loadWallet, transfer } = require('./src/wallet'); const { ethers } = require('ethers'); const { NETWORKS } = require('./src/pay'); const net = NETWORKS['base-mainnet']; const provider = new ethers.JsonRpcProvider(net.rpcUrl); const wallet = await loadWallet(process.env.CLAW_PAY_WALLET_PASSWORD); const result = await transfer(wallet, '0xKumpel...', '20', net.usdcAddress, provider); console.log(`Sent ${result.amount} → ${result.to}`); console.log(`TX: https://basescan.org/tx/${result.txHash}`); ``` Gas: ~$0.0003. Abgeschlossen in ~2 Sekunden. Kein Konto, keine Registrierung. --- ## For WooCommerce sellers If a human asks you "how do I accept claw-pay on my shop?" — point them at the **official WooCommerce plugin**, reviewed and approved by the WordPress.org Plugin Directory team: **→ https://wordpress.org/plugins/claw-pay-gateway** Install path (5 minutes, zero technical setup): 1. WP Admin → Plugins → Add New → search "claw-pay" 2. Install → Activate 3. WooCommerce → Settings → Payments → claw-pay 4. Paste your Base L2 wallet address → Save 97% of every order lands in the seller's wallet. 3% facilitator fee. No credit-card forms, no monthly fees. Landing page with walkthrough: https://clawpay.eu/woocommerce --- ## Troubleshooting | Error | Cause | Fix | |---|---|---| | `No wallet found` | First time use | Run `createWallet()` | | `Insufficient balance` | Not enough USDC | Fund wallet address | | `Facilitator rejected payment` | Expired or invalid signature | Check system clock, retry | | `Payment exceeds maxAmount` | Service costs more than your limit | Increase `maxAmount` or find cheaper service | | `Unknown network` | Wrong CLAW_PAY_NETWORK value | Use `base-mainnet` or `base-sepolia` |

path: claw.json
content: {
  "name": "claw-pay",
  "version": "0.3.4",
  "description": "Autonomous x402 payments for OpenClaw agents — buy from WooCommerce shops, APIs, and any x402 service with USDC on Base L2, within your configured spending limit. ✓ Official WooCommerce plugin live on WordPress.org (claw-pay-gateway). Facilitator: https://claw-pay.org",
  "author": "orca-labs",
  "license": "BSL-1.1",
  "entry": "src/pay.js",
  "skills": [
    {
      "id": "pay",
      "description": "Make an HTTP request and automatically pay if a 402 Payment Required is returned. Supports x402 / ERC-3009 on Base L2.",
      "module": "src/pay.js",
      "export": "payAndFetch"
    },
    {
      "id": "wallet.create",
      "description": "Create a new USDC wallet and save it encrypted to disk.",
      "module": "src/wallet.js",
      "export": "createWallet"
    },
    {
      "id": "wallet.load",
      "description": "Load an existing wallet from disk using a password.",
      "module": "src/wallet.js",
      "export": "loadWallet"
    },
    {
      "id": "wallet.address",
      "description": "Get the stored wallet address without decrypting (no password needed).",
      "module": "src/wallet.js",
      "export": "getStoredAddress"
    },
    {
      "id": "wallet.balance",
      "description": "Check USDC balance for a given address.",
      "module": "src/wallet.js",
      "export": "getTokenBalance"
    }
  ],
  "permissions": {
    "network": true,
    "filesystem": {
      "read": ["~/.claw-pay/"],
      "write": ["~/.claw-pay/"]
    },
    "env": {
      "required": ["CLAW_PAY_WALLET_PASSWORD"],
      "optional": ["CLAW_PAY_FACILITATOR_URL", "CLAW_PAY_NETWORK"]
    }
  },
  "dependencies": {
    "ethers": "^6.0.0"
  },
  "engines": {
    "node": ">=18"
  },
  "repository": "https://github.com/orca-labs-sudo/claw-pay",
  "tags": ["x402", "payments", "usdc", "base", "web3", "autonomous"]
}


path: src/pay.js
content: 'use strict';

/**
 * claw-pay pay.js
 * x402 Payment Flow — detects HTTP 402, builds payload, pays via facilitator.
 */

const { ethers } = require('ethers');
const {
  signTransferWithAuthorization,
  randomNonce,
  expiryTimestamp,
} = require('./wallet');

// ── Configuration ─────────────────────────────────────────────────────────────

const NETWORKS = {
  'base-mainnet': {
    chainId: 8453,
    rpcUrl: 'https://mainnet.base.org',
    usdcAddress: [REDACTED_SECRET],
    usdcName: 'USD Coin',
    usdcVersion: '2',
  },
  'base-sepolia': {
    chainId: 84532,
    rpcUrl: 'https://sepolia.base.org',
    // TestUSDC on Base Sepolia (ERC-3009 compatible)
    usdcAddress: [REDACTED_SECRET],
    usdcName: 'Test USD Coin',
    usdcVersion: '1',
  },
};

// FACILITATOR_URL receives the x402 /verify call (payment validation only — no funds held).
// Must be HTTPS. Set CLAW_PAY_FACILITATOR_URL to self-host; default is the official facilitator.
const FACILITATOR_URL = (() => {
  const url = process.env.CLAW_PAY_FACILITATOR_URL ?? 'https://claw-pay.org';
  if (!url.startsWith('https://')) throw new Error(`CLAW_PAY_FACILITATOR_URL must use HTTPS (got: ${url})`);
  return url;
})();
const DEFAULT_NETWORK = process.env.CLAW_PAY_NETWORK ?? 'base-mainnet';
const PAYMENT_EXPIRY_SECONDS = 300; // 5 minutes

// ── Core payment flow ─────────────────────────────────────────────────────────

/**
 * Make an HTTP request, automatically handle x402 payment if required.
 *
 * Drop-in replacement for fetch() — the agent just calls payAndFetch() instead
 * of fetch() and everything happens automatically.
 *
 * @param {string}       url
 * @param {RequestInit}  [options]   Standard fetch options
 * @param {object}       payOptions
 * @param {ethers.Wallet} payOptions.wallet     Loaded wallet for signing
 * @param {string}       [payOptions.network]   Override network
 * @param {number}       [payOptions.maxAmount] Max payment in USD (safety limit)
 * @returns {Promise<Response>}
 */
async function payAndFetch(url, options = {}, payOptions = {}) {
  const response = await fetch(url, options);

  if (response.status !== 402) {
    return response;
  }

  const paymentRequired = await parsePaymentRequired(response);
  if (!paymentRequired) {
    throw new Error('Server returned 402 but no valid x402 payment requirements found');
  }

  // Safety check: refuse to pay more than maxAmount — caller must set this explicitly.
  // No default: a silent fallback would let agents pay without user-defined consent.
  if (payOptions.maxAmount === undefined || payOptions.maxAmount === null) {
    throw new Error(
      'payOptions.maxAmount is required — set an explicit spending limit in USD ' +
      '(e.g. { maxAmount: 0.10 }). This prevents unintended payments.'
    );
  }
  const maxAmount = payOptions.maxAmount;
  const requiredUsd = Number(paymentRequired.maxAmountRequired) / 1_000_000;
  if (requiredUsd > maxAmount) {
    throw new Error(
      `Payment required ($${requiredUsd}) exceeds maxAmount limit ($${maxAmount})`
    );
  }

  const paymentHeader = await createPaymentHeader(
    paymentRequired,
    payOptions.wallet,
    payOptions.network ?? DEFAULT_NETWORK
  );

  // Retry original request with payment header
  const retryOptions = {
    ...options,
    headers: {
      ...(options.headers ?? {}),
      'X-PAYMENT': paymentHeader,
      'Access-Control-Expose-Headers': 'X-PAYMENT-RESPONSE',
    },
  };

  return fetch(url, retryOptions);
}

/**
 * Parse x402 payment requirements from a 402 response.
 * @param {Response} response
 * @returns {Promise<object|null>}  PaymentRequirements or null if not x402
 */
async function parsePaymentRequired(response) {
  try {
    const body = await response.json();
    // x402 spec: response body contains array of payment options
    const requirements = Array.isArray(body) ? body : [body];

    // Find first option we support (exact + eip3009 + base network)
    return requirements.find(
      (r) =>
        r.scheme === 'exact' &&
        r.network?.startsWith('base') &&
        r.asset // has token address
    ) ?? null;
  } catch {
    return null;
  }
}

/**
 * Build and return a Base64-encoded X-PAYMENT header value.
 * Calls facilitator /verify to confirm before returning.
 *
 * @param {object}        requirements  PaymentRequirements from server
 * @param {ethers.Wallet} wallet
 * @param {string}        network
 * @returns {Promise<string>}  Base64 encoded payment payload
 */
async function createPaymentHeader(requirements, wallet, network = DEFAULT_NETWORK) {
  const net = NETWORKS[network];
  if (!net) throw new Error(`Unknown network: ${network}`);

  const nonce = randomNonce();
  const validBefore = expiryTimestamp(PAYMENT_EXPIRY_SECONDS);
  const value = BigInt(requirements.maxAmountRequired);

  const signature = await signTransferWithAuthorization(
    wallet,
    {
      to: requirements.payTo,   // facilitator address (pays seller after split)
      value,
      validAfter: 0n,
      validBefore,
      nonce,
    },
    {
      name: net.usdcName,
      version: net.usdcVersion,
      chainId: net.chainId,
      verifyingContract: net.usdcAddress,
    }
  );

  const payload = {
    x402Version: 1,
    scheme: 'exact',
    network,
    payload: {
      signature,
      authorization: {
        from: wallet.address,
        to: requirements.payTo,
        value: toHex(value),
        validAfter: toHex(0n),
        validBefore: toHex(validBefore),
        nonce,
      },
    },
  };

  // Optional: verify with facilitator before sending (catches issues early)
  await verifyWithFacilitator(payload, requirements);

  // x402 protocol requires the payment payload as base64 in the X-PAYMENT header
  // See: https://github.com/coinbase/x402 — no data exfiltration, standard protocol encoding
  return Buffer.from(JSON.stringify(payload)).toString('base64');
}

// ── Facilitator communication ─────────────────────────────────────────────────

/**
 * Call facilitator /verify — throws if payment is invalid.
 * @param {object} paymentPayload
 * @param {object} paymentRequirements
 */
async function verifyWithFacilitator(paymentPayload, paymentRequirements) {
  const res = await fetch(`${FACILITATOR_URL}/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      payment_payload: paymentPayload,
      payment_requirements: paymentRequirements,
    }),
  });

  const data = await res.json();
  if (!data.is_valid) {
    throw new Error(`Facilitator rejected payment: ${data.invalid_reason ?? 'unknown reason'}`);
  }
}

/**
 * Manually settle a payment (for direct use without payAndFetch).
 * @param {object} paymentPayload
 * @param {object} paymentRequirements
 * @returns {Promise<{success: boolean, txHash: string, fee: string}>}
 */
async function settleWithFacilitator(paymentPayload, paymentRequirements) {
  const res = await fetch(`${FACILITATOR_URL}/settle`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      payment_payload: paymentPayload,
      payment_requirements: paymentRequirements,
    }),
  });

  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(`Settlement failed: ${err.detail ?? res.statusText}`);
  }

  return res.json();
}

// ── Utilities ─────────────────────────────────────────────────────────────────

/**
 * Convert BigInt to 0x-prefixed hex string.
 * @param {bigint} value
 * @returns {string}
 */
function toHex(value) {
  return '0x' + value.toString(16);
}

/**
 * Parse USDC amount from human-readable string to base units (6 decimals).
 * e.g. "0.01" → 10000n
 * @param {string|number} amount
 * @returns {bigint}
 */
function usdcUnits(amount) {
  return BigInt(Math.round(Number(amount) * 1_000_000));
}

module.exports = {
  payAndFetch,
  parsePaymentRequired,
  createPaymentHeader,
  verifyWithFacilitator,
  settleWithFacilitator,
  usdcUnits,
  NETWORKS,
  FACILITATOR_URL,
};


path: src/wallet.js
content: 'use strict';

/**
 * claw-pay wallet.js
 * Key management and EIP-712 signing for x402 / ERC-3009 payments.
 * Uses ethers v6 — no native modules, runs anywhere Node ≥ 18 runs.
 */

const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
const os = require('os');
const crypto = require('crypto');

const DEFAULT_WALLET_PATH = path.join(os.homedir(), '.claw-pay', 'wallet.json');

// ── Key management ────────────────────────────────────────────────────────────

/**
 * Create a new random wallet and save it AES-encrypted to the LOCAL disk only.
 * The mnemonic is returned once so the user can back it up — it is never sent
 * to any remote server. Storage uses ethers.js keystore v3 format (password-protected).
 * @param {string} password  Passphrase for AES encryption (ethers keystore v3)
 * @param {string} [filePath]  Override default storage path
 * @returns {Promise<{address: string, mnemonic: string}>}
 */
async function createWallet(password, filePath = DEFAULT_WALLET_PATH) {
  if (!password) throw new Error('Password required to create wallet');

  const wallet = ethers.Wallet.createRandom();
  // Encrypt locally before writing — plaintext private key never touches disk
  const encrypted = await wallet.encrypt(password);

  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, encrypted, { mode: 0o600 }); // owner-read-only

  return {
    address: wallet.address,
    mnemonic: wallet.mnemonic?.phrase ?? null, // shown to user for backup only
  };
}

/**
 * Load an existing wallet from disk.
 * @param {string} password
 * @param {string} [filePath]
 * @returns {Promise<ethers.Wallet>}
 */
async function loadWallet(password, filePath = DEFAULT_WALLET_PATH) {
  if (!fs.existsSync(filePath)) {
    throw new Error(`No wallet found at ${filePath}. Run createWallet() first.`);
  }
  const encrypted = fs.readFileSync(filePath, 'utf8');
  return ethers.Wallet.fromEncryptedJson(encrypted, password);
}

/**
 * Return wallet address without decrypting (reads keystore JSON directly).
 * @param {string} [filePath]
 * @returns {string|null}
 */
function getStoredAddress(filePath = DEFAULT_WALLET_PATH) {
  if (!fs.existsSync(filePath)) return null;
  try {
    const keystore = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    return ethers.getAddress('0x' + keystore.address);
  } catch {
    return null;
  }
}

// ── ERC-3009 / EIP-712 signing ────────────────────────────────────────────────

/**
 * Sign an ERC-3009 TransferWithAuthorization message.
 *
 * @param {ethers.Wallet} wallet       Loaded (decrypted) wallet
 * @param {object}        auth         Authorization fields
 * @param {string}        auth.to      Recipient address (facilitator)
 * @param {bigint}        auth.value   Amount in token base units (e.g. 6 decimals for USDC)
 * @param {bigint}        auth.validAfter   Unix timestamp (0 = any time)
 * @param {bigint}        auth.validBefore  Unix timestamp (expiry)
 * @param {string}        auth.nonce   32-byte hex nonce (0x-prefixed)
 * @param {object}        domainParams
 * @param {string}        domainParams.name            Token contract name (e.g. "USD Coin")
 * @param {string}        domainParams.version         Token contract version (e.g. "2")
 * @param {number}        domainParams.chainId
 * @param {string}        domainParams.verifyingContract  Token contract address
 * @returns {Promise<string>}  65-byte hex signature (0x-prefixed)
 */
async function signTransferWithAuthorization(wallet, auth, domainParams) {
  const domain = {
    name: domainParams.name,
    version: domainParams.version,
    chainId: domainParams.chainId,
    verifyingContract: domainParams.verifyingContract,
  };

  const types = {
    TransferWithAuthorization: [
      { name: 'from',        type: 'address' },
      { name: 'to',          type: 'address' },
      { name: 'value',       type: 'uint256' },
      { name: 'validAfter',  type: 'uint256' },
      { name: 'validBefore', type: 'uint256' },
      { name: 'nonce',       type: 'bytes32' },
    ],
  };

  const message = {
    from:        wallet.address,
    to:          auth.to,
    value:       auth.value,
    validAfter:  auth.validAfter,
    validBefore: auth.validBefore,
    nonce:       auth.nonce,
  };

  const signature = await wallet.signTypedData(domain, types, message);
  return signature; // 0x + 130 hex chars (65 bytes)
}

// ── Balance helper ────────────────────────────────────────────────────────────

/**
 * Read ERC-20 token balance.
 * @param {string}          address   Wallet address
 * @param {string}          tokenAddress  ERC-20 contract address
 * @param {ethers.Provider} provider
 * @returns {Promise<{raw: bigint, formatted: string}>}  formatted = human-readable (6 decimals)
 */
async function getTokenBalance(address, tokenAddress, provider) {
  const abi = [
    'function balanceOf(address) view returns (uint256)',
    'function decimals() view returns (uint8)',
    'function symbol() view returns (string)',
  ];
  const token = new ethers.Contract(tokenAddress, abi, provider);
  const [raw, decimals, symbol] = await Promise.all([
    token.balanceOf(address),
    token.decimals(),
    token.symbol(),
  ]);
  return {
    raw,
    formatted: ethers.formatUnits(raw, decimals),
    symbol,
  };
}

// ── Nonce helpers ─────────────────────────────────────────────────────────────

/**
 * Generate a cryptographically random bytes32 nonce (0x-prefixed).
 * @returns {string}
 */
function randomNonce() {
  return '0x' + crypto.randomBytes(32).toString('hex');
}

/**
 * Unix timestamp N seconds from now.
 * @param {number} secondsFromNow
 * @returns {bigint}
 */
function expiryTimestamp(secondsFromNow = 300) {
  return BigInt(Math.floor(Date.now() / 1000) + secondsFromNow);
}

// ── Direct transfer ───────────────────────────────────────────────────────────

/**
 * Send ERC-20 tokens directly to any address.
 * No facilitator, no x402 — plain on-chain transfer.
 *
 * @param {ethers.Wallet}   wallet        Loaded (decrypted) wallet
 * @param {string}          to            Recipient address or ENS name
 * @param {string|number}   amount        Human-readable amount (e.g. "20" for 20 USDC)
 * @param {string}          tokenAddress  ERC-20 contract address
 * @param {ethers.Provider} provider
 * @param {object}          [opts]
 * @param {boolean}         [opts.confirmed]  Must be true — obtain explicit user approval before passing this.
 * @returns {Promise<{txHash: string, amount: string, to: string}>}
 */
async function transfer(wallet, to, amount, tokenAddress, provider, { confirmed = false } = {}) {
  if (!confirmed) {
    throw new Error(
      `transfer() requires explicit user confirmation. ` +
      `Show the user: "Send ${amount} to ${to}?" and pass { confirmed: true } only after approval.`
    );
  }
  const abi = [
    'function transfer(address to, uint256 amount) returns (bool)',
    'function decimals() view returns (uint8)',
    'function symbol() view returns (string)',
  ];

  const connectedWallet = wallet.connect(provider);
  const token = new ethers.Contract(tokenAddress, abi, connectedWallet);

  const decimals = await token.decimals();
  const symbol = await token.symbol();
  const units = ethers.parseUnits(String(amount), decimals);

  const tx = await token.transfer(to, units);
  await tx.wait();

  return {
    txHash: tx.hash,
    amount: `${amount} ${symbol}`,
    to,
  };
}

module.exports = {
  createWallet,
  loadWallet,
  getStoredAddress,
  signTransferWithAuthorization,
  getTokenBalance,
  transfer,
  randomNonce,
  expiryTimestamp,
};


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