{"skill":{"slug":"sparkbtcbot-proxy","displayName":"Spark Bitcoin L2 Proxy for AI Agents","summary":"Use a Spark Bitcoin L2 wallet proxy for AI agents via HTTP API. Check balances, send payments, create invoices, pay L402 paywalls — all without holding the m...","description":"---\nname: sparkbtcbot-proxy\ndescription: Use a Spark Bitcoin L2 wallet proxy for AI agents via HTTP API. Check balances, send payments, create invoices, pay L402 paywalls — all without holding the mnemonic. Use when user mentions \"Spark proxy,\" \"wallet API,\" \"L402,\" \"proxy payment,\" \"bearer token auth,\" or wants secured Bitcoin capabilities for an agent.\nargument-hint: \"[Optional: specify operation - balance, pay, invoice, l402, transfer, or setup]\"\nhomepage: https://github.com/echennells/sparkbtcbot-proxy\nsource: https://github.com/echennells/sparkbtcbot-proxy\nrequires:\n  env:\n    - name: PROXY_URL\n      description: HTTPS URL of your deployed sparkbtcbot-proxy instance (e.g., https://your-app.vercel.app)\n      sensitive: false\n    - name: PROXY_TOKEN\n      description: Bearer token for proxy authentication. Create via POST /api/tokens with an admin token. Use least-privilege roles — prefer 'read-only', 'invoice', or 'pay-only' over 'admin' for agents.\n      sensitive: true\nmodel-invocation: autonomous\nmodel-invocation-reason: This skill enables agents to call a wallet proxy API. Autonomous invocation is intentional for payment workflows, but the proxy enforces spending limits and role-based access. Always use least-privilege tokens and set per-tx/daily caps on the proxy side.\n---\n\n# Spark Bitcoin L2 Proxy for AI Agents\n\nYou are an expert in using the sparkbtcbot-proxy — a serverless HTTP API that gives AI agents scoped access to a Spark Bitcoin L2 wallet without exposing the private key.\n\n## Why Use the Proxy Instead of Direct SDK\n\n| Concern | Direct SDK (sparkbtcbot-skill) | Proxy (this skill) |\n|---------|-------------------------------|-------------------|\n| Mnemonic location | Agent holds it | Server holds it |\n| Spending limits | None (agent decides) | Per-tx and daily caps |\n| Access revocation | Move funds to new wallet | Revoke bearer token |\n| Role-based access | No | Yes (admin, invoice, pay-only, read-only) |\n| Setup complexity | npm install + mnemonic | HTTP calls + bearer token |\n\n**Use the proxy when:**\n- You don't trust the agent with full wallet control\n- You need spending limits or audit logs\n- You want to revoke access without moving funds\n- Multiple agents share one wallet with different permissions\n\n**Use direct SDK when:**\n- Testing or development\n- Agent needs offline signing\n- You're building the proxy itself\n\n## Before You Start\n\n1. **Deploy your own proxy** — see [sparkbtcbot-proxy](https://github.com/echennells/sparkbtcbot-proxy) for setup instructions. The proxy runs on Vercel (free tier works) with Upstash Redis.\n\n2. **Use HTTPS only** — never connect to a proxy over plain HTTP. All Vercel deployments use HTTPS by default.\n\n3. **Create least-privilege tokens** — don't give agents admin tokens. Use the most restrictive role that works:\n   - `read-only` for monitoring/dashboard agents\n   - `invoice` for agents that receive payments but don't spend\n   - `pay-only` for agents that pay L402 paywalls but don't create invoices\n   - `admin` only for your own management scripts\n\n4. **Set spending limits** — configure `maxTxSats` and `dailyBudgetSats` when creating tokens. The proxy enforces these server-side.\n\n5. **Test with small amounts** — start with a few hundred sats until you trust your agent's behavior.\n\n6. **Have a revocation plan** — know how to revoke tokens via `DELETE /api/tokens` if an agent is compromised.\n\n## Token Roles\n\n| Role | Permissions |\n|------|-------------|\n| `admin` | Full access: read, create invoices, pay, transfer, manage tokens |\n| `invoice` | Read + create invoices. Cannot pay or transfer. |\n| `pay-only` | Read + pay invoices and L402. Cannot create invoices or transfer. |\n| `read-only` | Read only (balance, info, transactions, logs). Cannot pay or create invoices. |\n\n## Base URL\n\nThe proxy runs on Vercel. Your base URL will look like:\n```\nhttps://your-deployment.vercel.app\n```\n\nAll requests require authentication:\n```\nAuthorization: Bearer <your-token>\n```\n\n## API Reference\n\n### Read Operations (any role)\n\n#### Get Balance\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/balance\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"balance\": \"50000\",\n    \"tokenBalances\": {\n      \"btkn1...\": {\n        \"balance\": \"1000\",\n        \"tokenMetadata\": {\n          \"tokenName\": \"Example Token\",\n          \"tokenTicker\": \"EXT\",\n          \"decimals\": 0\n        }\n      }\n    }\n  }\n}\n```\n\n#### Get Wallet Info\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/info\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"sparkAddress\": \"sp1p...\",\n    \"identityPublicKey\": \"02abc...\"\n  }\n}\n```\n\n#### Get Deposit Address (L1 Bitcoin)\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/deposit-address\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"address\": \"bc1p...\"\n  }\n}\n```\n\n#### Get Transaction History\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/transactions?limit=10&offset=0\"\n```\n\n#### Get Fee Estimate\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/fee-estimate?invoice=lnbc...\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"feeSats\": 5\n  }\n}\n```\n\n#### Get Activity Logs\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/logs?limit=20\"\n```\n\n### Invoice Operations (admin or invoice role)\n\n#### Create Lightning Invoice (BOLT11)\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"amountSats\": 1000, \"memo\": \"Payment for service\", \"expirySeconds\": 3600}' \\\n  \"$PROXY_URL/api/invoice/create\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"encodedInvoice\": \"lnbc10u1p...\"\n  }\n}\n```\n\n#### Create Spark Invoice\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"amount\": 1000, \"memo\": \"Spark payment\"}' \\\n  \"$PROXY_URL/api/invoice/spark\"\n```\n\n### Payment Operations (admin or pay-only role)\n\n#### Pay Lightning Invoice\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"invoice\": \"lnbc10u1p...\", \"maxFeeSats\": 10}' \\\n  \"$PROXY_URL/api/pay\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"payment-id-123\",\n    \"status\": \"LIGHTNING_PAYMENT_SUCCEEDED\",\n    \"paymentPreimage\": \"abc123...\"\n  }\n}\n```\n\n#### Transfer to Spark Address\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"receiverSparkAddress\": \"sp1p...\", \"amountSats\": 1000}' \\\n  \"$PROXY_URL/api/transfer\"\n```\n\n### L402 Paywall Operations (admin or pay-only role)\n\nL402 lets you pay for API access with Lightning. The proxy handles the full flow automatically.\n\n#### Pay L402 and Fetch Content\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://lightningfaucet.com/api/l402/joke\", \"maxFeeSats\": 50}' \\\n  \"$PROXY_URL/api/l402\"\n```\n\nResponse (immediate success):\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"status\": 200,\n    \"paid\": true,\n    \"priceSats\": 21,\n    \"preimage\": \"be2ebe7c...\",\n    \"data\": {\"setup\": \"Why do programmers...\", \"punchline\": \"...\"}\n  }\n}\n```\n\nResponse (cached token reused):\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"status\": 200,\n    \"paid\": false,\n    \"cached\": true,\n    \"data\": {\"setup\": \"...\", \"punchline\": \"...\"}\n  }\n}\n```\n\n#### Preview L402 Cost (any role)\n\nCheck what an L402 resource costs without paying:\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://lightningfaucet.com/api/l402/joke\"}' \\\n  \"$PROXY_URL/api/l402/preview\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"requires_payment\": true,\n    \"invoice_amount_sats\": 21,\n    \"invoice\": \"lnbc210n1p...\",\n    \"macaroon\": \"AgELbGlnaHRuaW5n...\"\n  }\n}\n```\n\n#### Handling Pending L402 Payments (IMPORTANT)\n\nLightning payments are asynchronous. If the preimage isn't available within ~7.5 seconds, the proxy returns a pending status:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"status\": \"pending\",\n    \"pendingId\": \"a1b2c3d4...\",\n    \"message\": \"Payment sent but preimage not yet available. Poll GET /api/l402/status?id=<pendingId> to complete.\",\n    \"priceSats\": 21\n  }\n}\n```\n\n**You MUST handle this case.** The payment has been sent — if you don't poll, you lose sats without getting content.\n\nPoll for completion:\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/l402/status?id=a1b2c3d4...\"\n```\n\n**Recommended retry logic:**\n\n```javascript\nasync function fetchL402(proxyUrl, token, targetUrl, maxFeeSats = 50) {\n  const response = await fetch(`${proxyUrl}/api/l402`, {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${token}`,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ url: targetUrl, maxFeeSats }),\n  });\n\n  const result = await response.json();\n\n  if (result.data?.status === 'pending') {\n    const pendingId = result.data.pendingId;\n    for (let i = 0; i < 10; i++) {\n      await new Promise(r => setTimeout(r, 3000));\n      const statusResponse = await fetch(\n        `${proxyUrl}/api/l402/status?id=${pendingId}`,\n        { headers: { 'Authorization': `Bearer ${token}` } }\n      );\n      const statusResult = await statusResponse.json();\n      if (statusResult.data?.status !== 'pending') {\n        return statusResult;\n      }\n    }\n    throw new Error('L402 payment timed out');\n  }\n\n  return result;\n}\n```\n\n### Token Management (admin role only)\n\n#### List Tokens\n\n```bash\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/tokens\"\n```\n\n#### Create Token\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"role\": \"invoice\", \"label\": \"merchant-bot\", \"maxTxSats\": 5000, \"dailyBudgetSats\": 50000}' \\\n  \"$PROXY_URL/api/tokens\"\n```\n\nResponse includes the full token string — save it, shown only once:\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"token\": \"sbp_abc123...\",\n    \"role\": \"invoice\",\n    \"label\": \"merchant-bot\"\n  }\n}\n```\n\n#### Revoke Token\n\n```bash\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\": \"sbp_abc123...\"}' \\\n  \"$PROXY_URL/api/tokens\"\n```\n\n## Complete Agent Class (JavaScript)\n\n```javascript\nexport class SparkProxyAgent {\n  #baseUrl;\n  #token;\n\n  constructor(baseUrl, token) {\n    this.#baseUrl = baseUrl.replace(/\\/$/, '');\n    this.#token = token;\n  }\n\n  async #request(method, path, body = null) {\n    const options = {\n      method,\n      headers: {\n        'Authorization': `Bearer ${this.#token}`,\n        'Content-Type': 'application/json',\n      },\n    };\n    if (body) {\n      options.body = JSON.stringify(body);\n    }\n\n    const response = await fetch(`${this.#baseUrl}${path}`, options);\n    const result = await response.json();\n\n    if (!result.success) {\n      throw new Error(result.error || 'Request failed');\n    }\n    return result.data;\n  }\n\n  async getBalance() {\n    return this.#request('GET', '/api/balance');\n  }\n\n  async getInfo() {\n    return this.#request('GET', '/api/info');\n  }\n\n  async getDepositAddress() {\n    return this.#request('GET', '/api/deposit-address');\n  }\n\n  async getTransactions(limit = 10, offset = 0) {\n    return this.#request('GET', `/api/transactions?limit=${limit}&offset=${offset}`);\n  }\n\n  async getFeeEstimate(invoice) {\n    return this.#request('GET', `/api/fee-estimate?invoice=${encodeURIComponent(invoice)}`);\n  }\n\n  async createLightningInvoice(amountSats, memo = '', expirySeconds = 3600) {\n    return this.#request('POST', '/api/invoice/create', {\n      amountSats,\n      memo,\n      expirySeconds,\n    });\n  }\n\n  async createSparkInvoice(amount, memo = '') {\n    return this.#request('POST', '/api/invoice/spark', { amount, memo });\n  }\n\n  async payLightningInvoice(invoice, maxFeeSats = 10) {\n    return this.#request('POST', '/api/pay', { invoice, maxFeeSats });\n  }\n\n  async transfer(receiverSparkAddress, amountSats) {\n    return this.#request('POST', '/api/transfer', {\n      receiverSparkAddress,\n      amountSats,\n    });\n  }\n\n  async previewL402(url) {\n    return this.#request('POST', '/api/l402/preview', { url });\n  }\n\n  async fetchL402(url, options = {}) {\n    const { method = 'GET', headers = {}, body, maxFeeSats = 50 } = options;\n\n    const result = await this.#request('POST', '/api/l402', {\n      url,\n      method,\n      headers,\n      body,\n      maxFeeSats,\n    });\n\n    // Handle pending status with polling\n    if (result.status === 'pending') {\n      const pendingId = result.pendingId;\n      for (let i = 0; i < 10; i++) {\n        await new Promise(r => setTimeout(r, 3000));\n        const status = await this.#request('GET', `/api/l402/status?id=${pendingId}`);\n        if (status.status !== 'pending') {\n          return status;\n        }\n      }\n      throw new Error('L402 payment timed out');\n    }\n\n    return result;\n  }\n}\n\n// Usage\nconst agent = new SparkProxyAgent(\n  process.env.PROXY_URL,\n  process.env.PROXY_TOKEN\n);\n\nconst balance = await agent.getBalance();\nconsole.log('Balance:', balance.balance, 'sats');\n\nconst invoice = await agent.createLightningInvoice(1000, 'Test payment');\nconsole.log('Invoice:', invoice.encodedInvoice);\n\nconst l402Result = await agent.fetchL402('https://lightningfaucet.com/api/l402/joke');\nconsole.log('Joke:', l402Result.data);\n```\n\n## Environment Variables for Agent\n\n```\nPROXY_URL=https://your-deployment.vercel.app\nPROXY_TOKEN=sbp_your_token_here\n```\n\n## Error Handling\n\nAll errors return:\n```json\n{\n  \"success\": false,\n  \"error\": \"Error message here\"\n}\n```\n\nCommon errors:\n- **401 Unauthorized** — Invalid or missing bearer token\n- **403 Forbidden** — Token role doesn't permit this operation\n- **400 Bad Request** — Missing required parameters\n- **429 Too Many Requests** — Daily budget exceeded\n- **500 Internal Server Error** — Spark SDK or server error\n\n## Spending Limits\n\nThe proxy enforces two types of limits:\n\n1. **Global limits** (from env vars):\n   - `MAX_TRANSACTION_SATS` — per-transaction cap\n   - `DAILY_BUDGET_SATS` — daily total cap (resets midnight UTC)\n\n2. **Per-token limits** (set when creating token):\n   - `maxTxSats` — per-transaction cap for this token\n   - `dailyBudgetSats` — daily cap for this token\n\nThe lower of global and per-token limits applies.\n\n## Security Notes\n\n1. **Treat bearer tokens like passwords** — they grant wallet access up to their role\n2. **Use the most restrictive role possible** — if an agent only creates invoices, use `invoice` role\n3. **Set per-token spending limits** — don't rely solely on global limits\n4. **Monitor logs** — check `/api/logs` for unexpected activity\n5. **Revoke compromised tokens immediately** — no need to move funds\n\n## Resources\n\n- Proxy repo: https://github.com/echennells/sparkbtcbot-proxy\n- Direct SDK skill: https://github.com/echennells/sparkbtcbot-skill\n- Spark docs: https://docs.spark.money\n- L402 spec: https://docs.lightning.engineering/the-lightning-network/l402\n","tags":{"latest":"1.1.0"},"stats":{"comments":0,"downloads":1102,"installsAllTime":0,"installsCurrent":0,"stars":0,"versions":2},"createdAt":1771263998836,"updatedAt":1779077012617},"latestVersion":{"version":"1.1.0","createdAt":1771268037736,"changelog":"Declare env vars with sensitive flags, add model-invocation reason, add homepage/source URLs, add security guidance for HTTPS and least-privilege tokens","license":null},"metadata":null,"owner":{"handle":"echennells","userId":"s17c0k375bsxdtsd34rkcnxard885aqr","displayName":"echennells","image":"https://avatars.githubusercontent.com/u/83478409?v=4"},"moderation":null}