{
  "schemaVersion": "1.0",
  "item": {
    "slug": "sparkbtcbot-proxy",
    "name": "Spark Bitcoin L2 Proxy for AI Agents",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/echennells/sparkbtcbot-proxy",
    "canonicalUrl": "https://clawhub.ai/echennells/sparkbtcbot-proxy",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/sparkbtcbot-proxy",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=sparkbtcbot-proxy",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "slug": "sparkbtcbot-proxy",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-05-09T07:50:31.622Z",
      "expiresAt": "2026-05-16T07:50:31.622Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=sparkbtcbot-proxy",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=sparkbtcbot-proxy",
        "contentDisposition": "attachment; filename=\"sparkbtcbot-proxy-1.1.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null,
        "slug": "sparkbtcbot-proxy"
      },
      "scope": "item",
      "summary": "Item download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this item.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/sparkbtcbot-proxy"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/sparkbtcbot-proxy",
    "agentPageUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy/agent",
    "manifestUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Spark Bitcoin L2 Proxy for AI Agents",
        "body": "You 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."
      },
      {
        "title": "Why Use the Proxy Instead of Direct SDK",
        "body": "ConcernDirect SDK (sparkbtcbot-skill)Proxy (this skill)Mnemonic locationAgent holds itServer holds itSpending limitsNone (agent decides)Per-tx and daily capsAccess revocationMove funds to new walletRevoke bearer tokenRole-based accessNoYes (admin, invoice, pay-only, read-only)Setup complexitynpm install + mnemonicHTTP calls + bearer token\n\nUse the proxy when:\n\nYou don't trust the agent with full wallet control\nYou need spending limits or audit logs\nYou want to revoke access without moving funds\nMultiple agents share one wallet with different permissions\n\nUse direct SDK when:\n\nTesting or development\nAgent needs offline signing\nYou're building the proxy itself"
      },
      {
        "title": "Before You Start",
        "body": "Deploy your own proxy — see sparkbtcbot-proxy for setup instructions. The proxy runs on Vercel (free tier works) with Upstash Redis.\n\n\nUse HTTPS only — never connect to a proxy over plain HTTP. All Vercel deployments use HTTPS by default.\n\n\nCreate least-privilege tokens — don't give agents admin tokens. Use the most restrictive role that works:\n\nread-only for monitoring/dashboard agents\ninvoice for agents that receive payments but don't spend\npay-only for agents that pay L402 paywalls but don't create invoices\nadmin only for your own management scripts\n\n\n\nSet spending limits — configure maxTxSats and dailyBudgetSats when creating tokens. The proxy enforces these server-side.\n\n\nTest with small amounts — start with a few hundred sats until you trust your agent's behavior.\n\n\nHave a revocation plan — know how to revoke tokens via DELETE /api/tokens if an agent is compromised."
      },
      {
        "title": "Token Roles",
        "body": "RolePermissionsadminFull access: read, create invoices, pay, transfer, manage tokensinvoiceRead + create invoices. Cannot pay or transfer.pay-onlyRead + pay invoices and L402. Cannot create invoices or transfer.read-onlyRead only (balance, info, transactions, logs). Cannot pay or create invoices."
      },
      {
        "title": "Base URL",
        "body": "The proxy runs on Vercel. Your base URL will look like:\n\nhttps://your-deployment.vercel.app\n\nAll requests require authentication:\n\nAuthorization: Bearer <your-token>"
      },
      {
        "title": "Read Operations (any role)",
        "body": "Get Balance\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/balance\"\n\nResponse:\n\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\nGet Wallet Info\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/info\"\n\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"sparkAddress\": \"sp1p...\",\n    \"identityPublicKey\": \"02abc...\"\n  }\n}\n\nGet Deposit Address (L1 Bitcoin)\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/deposit-address\"\n\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"address\": \"bc1p...\"\n  }\n}\n\nGet Transaction History\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/transactions?limit=10&offset=0\"\n\nGet Fee Estimate\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/fee-estimate?invoice=lnbc...\"\n\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"feeSats\": 5\n  }\n}\n\nGet Activity Logs\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/logs?limit=20\""
      },
      {
        "title": "Invoice Operations (admin or invoice role)",
        "body": "Create Lightning Invoice (BOLT11)\n\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\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"encodedInvoice\": \"lnbc10u1p...\"\n  }\n}\n\nCreate Spark Invoice\n\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\""
      },
      {
        "title": "Payment Operations (admin or pay-only role)",
        "body": "Pay Lightning Invoice\n\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\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"payment-id-123\",\n    \"status\": \"LIGHTNING_PAYMENT_SUCCEEDED\",\n    \"paymentPreimage\": \"abc123...\"\n  }\n}\n\nTransfer to Spark Address\n\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"receiverSparkAddress\": \"sp1p...\", \"amountSats\": 1000}' \\\n  \"$PROXY_URL/api/transfer\""
      },
      {
        "title": "L402 Paywall Operations (admin or pay-only role)",
        "body": "L402 lets you pay for API access with Lightning. The proxy handles the full flow automatically.\n\nPay L402 and Fetch Content\n\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\nResponse (immediate success):\n\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\nResponse (cached token reused):\n\n{\n  \"success\": true,\n  \"data\": {\n    \"status\": 200,\n    \"paid\": false,\n    \"cached\": true,\n    \"data\": {\"setup\": \"...\", \"punchline\": \"...\"}\n  }\n}\n\nPreview L402 Cost (any role)\n\nCheck what an L402 resource costs without paying:\n\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\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"requires_payment\": true,\n    \"invoice_amount_sats\": 21,\n    \"invoice\": \"lnbc210n1p...\",\n    \"macaroon\": \"AgELbGlnaHRuaW5n...\"\n  }\n}\n\nHandling 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{\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\nYou 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\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/l402/status?id=a1b2c3d4...\"\n\nRecommended retry logic:\n\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}"
      },
      {
        "title": "Token Management (admin role only)",
        "body": "List Tokens\n\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/tokens\"\n\nCreate Token\n\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\nResponse includes the full token string — save it, shown only once:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"token\": \"sbp_abc123...\",\n    \"role\": \"invoice\",\n    \"label\": \"merchant-bot\"\n  }\n}\n\nRevoke Token\n\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\": \"sbp_abc123...\"}' \\\n  \"$PROXY_URL/api/tokens\""
      },
      {
        "title": "Complete Agent Class (JavaScript)",
        "body": "export 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);"
      },
      {
        "title": "Environment Variables for Agent",
        "body": "PROXY_URL=https://your-deployment.vercel.app\nPROXY_TOKEN=sbp_your_token_here"
      },
      {
        "title": "Error Handling",
        "body": "All errors return:\n\n{\n  \"success\": false,\n  \"error\": \"Error message here\"\n}\n\nCommon errors:\n\n401 Unauthorized — Invalid or missing bearer token\n403 Forbidden — Token role doesn't permit this operation\n400 Bad Request — Missing required parameters\n429 Too Many Requests — Daily budget exceeded\n500 Internal Server Error — Spark SDK or server error"
      },
      {
        "title": "Spending Limits",
        "body": "The proxy enforces two types of limits:\n\nGlobal limits (from env vars):\n\nMAX_TRANSACTION_SATS — per-transaction cap\nDAILY_BUDGET_SATS — daily total cap (resets midnight UTC)\n\n\n\nPer-token limits (set when creating token):\n\nmaxTxSats — per-transaction cap for this token\ndailyBudgetSats — daily cap for this token\n\nThe lower of global and per-token limits applies."
      },
      {
        "title": "Security Notes",
        "body": "Treat bearer tokens like passwords — they grant wallet access up to their role\nUse the most restrictive role possible — if an agent only creates invoices, use invoice role\nSet per-token spending limits — don't rely solely on global limits\nMonitor logs — check /api/logs for unexpected activity\nRevoke compromised tokens immediately — no need to move funds"
      },
      {
        "title": "Resources",
        "body": "Proxy repo: https://github.com/echennells/sparkbtcbot-proxy\nDirect SDK skill: https://github.com/echennells/sparkbtcbot-skill\nSpark docs: https://docs.spark.money\nL402 spec: https://docs.lightning.engineering/the-lightning-network/l402"
      }
    ],
    "body": "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\nWhy Use the Proxy Instead of Direct SDK\nConcern\tDirect SDK (sparkbtcbot-skill)\tProxy (this skill)\nMnemonic location\tAgent holds it\tServer holds it\nSpending limits\tNone (agent decides)\tPer-tx and daily caps\nAccess revocation\tMove funds to new wallet\tRevoke bearer token\nRole-based access\tNo\tYes (admin, invoice, pay-only, read-only)\nSetup complexity\tnpm install + mnemonic\tHTTP calls + bearer token\n\nUse the proxy when:\n\nYou don't trust the agent with full wallet control\nYou need spending limits or audit logs\nYou want to revoke access without moving funds\nMultiple agents share one wallet with different permissions\n\nUse direct SDK when:\n\nTesting or development\nAgent needs offline signing\nYou're building the proxy itself\nBefore You Start\n\nDeploy your own proxy — see sparkbtcbot-proxy for setup instructions. The proxy runs on Vercel (free tier works) with Upstash Redis.\n\nUse HTTPS only — never connect to a proxy over plain HTTP. All Vercel deployments use HTTPS by default.\n\nCreate least-privilege tokens — don't give agents admin tokens. Use the most restrictive role that works:\n\nread-only for monitoring/dashboard agents\ninvoice for agents that receive payments but don't spend\npay-only for agents that pay L402 paywalls but don't create invoices\nadmin only for your own management scripts\n\nSet spending limits — configure maxTxSats and dailyBudgetSats when creating tokens. The proxy enforces these server-side.\n\nTest with small amounts — start with a few hundred sats until you trust your agent's behavior.\n\nHave a revocation plan — know how to revoke tokens via DELETE /api/tokens if an agent is compromised.\n\nToken Roles\nRole\tPermissions\nadmin\tFull access: read, create invoices, pay, transfer, manage tokens\ninvoice\tRead + create invoices. Cannot pay or transfer.\npay-only\tRead + pay invoices and L402. Cannot create invoices or transfer.\nread-only\tRead only (balance, info, transactions, logs). Cannot pay or create invoices.\nBase 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\nAPI Reference\nRead Operations (any role)\nGet Balance\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/balance\"\n\n\nResponse:\n\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\nGet Wallet Info\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/info\"\n\n\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"sparkAddress\": \"sp1p...\",\n    \"identityPublicKey\": \"02abc...\"\n  }\n}\n\nGet Deposit Address (L1 Bitcoin)\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/deposit-address\"\n\n\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"address\": \"bc1p...\"\n  }\n}\n\nGet Transaction History\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/transactions?limit=10&offset=0\"\n\nGet Fee Estimate\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/fee-estimate?invoice=lnbc...\"\n\n\nResponse:\n\n{\n  \"success\": true,\n  \"data\": {\n    \"feeSats\": 5\n  }\n}\n\nGet Activity Logs\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/logs?limit=20\"\n\nInvoice Operations (admin or invoice role)\nCreate Lightning Invoice (BOLT11)\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\n{\n  \"success\": true,\n  \"data\": {\n    \"encodedInvoice\": \"lnbc10u1p...\"\n  }\n}\n\nCreate Spark Invoice\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\nPayment Operations (admin or pay-only role)\nPay Lightning Invoice\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\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"payment-id-123\",\n    \"status\": \"LIGHTNING_PAYMENT_SUCCEEDED\",\n    \"paymentPreimage\": \"abc123...\"\n  }\n}\n\nTransfer to Spark Address\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\nL402 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\nPay L402 and Fetch Content\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\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\n{\n  \"success\": true,\n  \"data\": {\n    \"status\": 200,\n    \"paid\": false,\n    \"cached\": true,\n    \"data\": {\"setup\": \"...\", \"punchline\": \"...\"}\n  }\n}\n\nPreview L402 Cost (any role)\n\nCheck what an L402 resource costs without paying:\n\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\n{\n  \"success\": true,\n  \"data\": {\n    \"requires_payment\": true,\n    \"invoice_amount_sats\": 21,\n    \"invoice\": \"lnbc210n1p...\",\n    \"macaroon\": \"AgELbGlnaHRuaW5n...\"\n  }\n}\n\nHandling 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{\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\nYou 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\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/l402/status?id=a1b2c3d4...\"\n\n\nRecommended retry logic:\n\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\nToken Management (admin role only)\nList Tokens\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  \"$PROXY_URL/api/tokens\"\n\nCreate Token\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\n{\n  \"success\": true,\n  \"data\": {\n    \"token\": \"sbp_abc123...\",\n    \"role\": \"invoice\",\n    \"label\": \"merchant-bot\"\n  }\n}\n\nRevoke Token\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\": \"sbp_abc123...\"}' \\\n  \"$PROXY_URL/api/tokens\"\n\nComplete Agent Class (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\nEnvironment Variables for Agent\nPROXY_URL=https://your-deployment.vercel.app\nPROXY_TOKEN=sbp_your_token_here\n\nError Handling\n\nAll errors return:\n\n{\n  \"success\": false,\n  \"error\": \"Error message here\"\n}\n\n\nCommon errors:\n\n401 Unauthorized — Invalid or missing bearer token\n403 Forbidden — Token role doesn't permit this operation\n400 Bad Request — Missing required parameters\n429 Too Many Requests — Daily budget exceeded\n500 Internal Server Error — Spark SDK or server error\nSpending Limits\n\nThe proxy enforces two types of limits:\n\nGlobal limits (from env vars):\n\nMAX_TRANSACTION_SATS — per-transaction cap\nDAILY_BUDGET_SATS — daily total cap (resets midnight UTC)\n\nPer-token limits (set when creating token):\n\nmaxTxSats — per-transaction cap for this token\ndailyBudgetSats — daily cap for this token\n\nThe lower of global and per-token limits applies.\n\nSecurity Notes\nTreat bearer tokens like passwords — they grant wallet access up to their role\nUse the most restrictive role possible — if an agent only creates invoices, use invoice role\nSet per-token spending limits — don't rely solely on global limits\nMonitor logs — check /api/logs for unexpected activity\nRevoke compromised tokens immediately — no need to move funds\nResources\nProxy repo: https://github.com/echennells/sparkbtcbot-proxy\nDirect SDK skill: https://github.com/echennells/sparkbtcbot-skill\nSpark docs: https://docs.spark.money\nL402 spec: https://docs.lightning.engineering/the-lightning-network/l402"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/echennells/sparkbtcbot-proxy",
    "publisherUrl": "https://clawhub.ai/echennells/sparkbtcbot-proxy",
    "owner": "echennells",
    "version": "1.1.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy",
    "downloadUrl": "https://openagent3.xyz/downloads/sparkbtcbot-proxy",
    "agentUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy/agent",
    "manifestUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/sparkbtcbot-proxy/agent.md"
  }
}