Skill: cuongdcdev/near-getpay (version 1.0.0)

--- name: near-getpay description: Accept crypto payments (NEAR, USDC, USDT) via a beautiful payment page with PingPay or HOT PAY integration. --- # NEAR GetPay Skill Accept crypto payments (NEAR, USDC, USDT) via a beautiful payment page with PingPay or HOT PAY integration. ## 🎯 What It Does Creates a hosted payment page where people can pay you in crypto with just a few clicks: 1. **Beautiful UI** - Mobile-friendly payment page with preset amounts 2. **Multi-token** - Accept NEAR, USDC, or USDT 3. **Dual provider** - Works with PingPay or HOT PAY (or both) 4. **Public URL** - Exposes via localhost.run tunnel 5. **First-time friendly** - Setup wizard guides new users 6. **Smart token selection** - Once a token is selected, others are hidden to avoid confusion (great for HOT PAY where each token has a unique checkout link) ## 🎨 User Flow ### PingPay Flow (default, best flow) 1. User selects token (NEAR, USDC, or USDT) 2. User enters amount or picks preset 3. Clicks "Pay Now" 4. Redirects to PingPay checkout with pre-filled amount ### HOT PAY Flow 1. User selects token (only configured tokens shown) 2. **Other tokens are hidden** to avoid confusion 3. User enters amount or picks preset 4. Clicks "Pay Now" 5. Redirects to HOT PAY checkout link for that specific token and amount ## 🚀 Quick Start ### 1. Installation ```bash cd ~/.openclaw/skills # Clone or extract the skill npm install ``` ### 2. Choose Your Provider You need **either** PingPay **or** HOT PAY (or both): **Option A: PingPay** (recommended for beginners) - Sign up at https://pingpay.io - Set your NEAR wallet in Dashboard → Settings - Get API key from Dashboard → Settings → API Keys - Add to `.env`: `PINGPAY_API_KEY=your_key_here` **Option B: HOT PAY** (for advanced users) - Visit https://pay.hot-labs.org/admin/overview - Create payment links for each token (NEAR, USDC, USDT) - Set your NEAR wallet as recipient when creating each link - Copy each `item_id` and add to `.env` ### 3. Configure Copy `.env.example` to `.env`: ```bash cp .env.example .env ``` Edit `.env`: ```env # Display name (actual recipient is set in PingPay/HOT PAY dashboard!) RECIPIENT_ADDRESS=your-account.near # Choose provider PAYMENT_PROVIDER=pingpay # or 'hotpay' # PingPay config (if using PingPay) PINGPAY_API_KEY=your_api_key_here # HOT PAY config (if using HOT PAY) HOTPAY_NEAR_ITEM_ID=your_near_item_id HOTPAY_USDC_ITEM_ID=your_usdc_item_id HOTPAY_USDT_ITEM_ID=your_usdt_item_id ``` ### 4. Start Server ```bash ./start.sh ``` Or manually: ```bash npx ts-node start-tunnel.ts ``` This will: - Start Express server on port 3000 - Create public URL via localhost.run - Display the shareable payment link ### 5. Share Your Link Share the generated `https://xxxxx.lhr.life` URL to accept payments! ## ⚠️ Important Notes ### Recipient Address Configuration **The recipient address (where payments go) is configured at the provider level, NOT in the .env file:** - **PingPay**: Set in your PingPay Dashboard → Settings → Wallet Address - **HOT PAY**: Set when creating each payment link The `RECIPIENT_ADDRESS` in `.env` is **only for display** on the payment page. To change where payments actually go, update your provider's dashboard settings. ### Tunnel Domains localhost.run generates random URLs that change each restart. For a permanent domain: 1. Sign up at https://admin.localhost.run/ 2. Configure your custom domain 3. Update the tunnel command in `start-tunnel.ts` Or use your own reverse proxy (Cloudflare Tunnel, ngrok paid, etc.) ## 📖 Usage in OpenClaw When a user asks to accept payments: ``` User: "Create a payment page to accept USDT donations" Agent: 1. Check if near-getpay skill is installed 2. Ask which provider they want (PingPay or HOT PAY) 3. Guide them through setup if not configured 4. Run: cd ~/.openclaw/skills/near-getpay && ./start.sh 5. Share the generated public URL ``` ### Example Conversation ``` User: I need to accept crypto payments Agent: I'll set up a payment page for you using the GetPay skill. First, do you have a PingPay or HOT PAY account? User: No, what's easier? Agent: PingPay is simpler. Here's what to do: 1. Sign up at https://pingpay.io 2. Add your NEAR wallet address in settings 3. Get an API key from Dashboard → API Keys 4. Share it with me (or add to .env yourself) Let me know when you have the API key! User: Got it: sk_test_abc123... Agent: Perfect! Starting your payment server... [runs start.sh] ✅ Your payment page is live: https://abc123xyz.lhr.life Share this link to accept NEAR, USDC, or USDT payments! ``` ## 🛠️ File Structure ``` near-getpay/ ├── SKILL.md ← You are here ├── package.json ├── tsconfig.json ├── .env.example ← Config template ├── .env ← Your config (gitignored) ├── start.sh ← Quick start script ├── start-tunnel.ts ← Server + tunnel launcher ├── server-simple.ts ← Main payment server └── scripts/ ├── pingpay-client.ts └── payment-orchestrator.ts ``` ## 🔧 Advanced Configuration ### Custom Port Edit `.env`: ```env PORT=8080 ``` ### Custom Tunnel Replace localhost.run in `start-tunnel.ts`: ```typescript // Option 1: ngrok const tunnel = spawn('ngrok', ['http', PORT.toString()]); // Option 2: Cloudflare Tunnel const tunnel = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${PORT}`]); // Option 3: localtunnel (less reliable) const tunnel = spawn('npx', ['localtunnel', '--port', PORT.toString()]); ``` ### Webhook Integration (HOT PAY only) HOT PAY sends webhooks to `/webhook/hotpay`. To use: 1. Expose your server publicly (not localhost.run - needs stable URL) 2. Configure webhook URL in HOT PAY dashboard 3. Server logs payment confirmations automatically ## 🎨 Customization ### Payment Amounts Edit preset amounts in `server-simple.ts`: ```typescript tokens: [ { symbol: 'NEAR', chain: 'NEAR', decimals: 24, presets: [0.5, 1, 5, 10] // ← Change these }, // ... ] ``` ### Branding Update colors, fonts, text in the HTML template sections of `server-simple.ts`. ### Add More Tokens Add to the `tokens` array (requires provider support): ```typescript { symbol: 'ETH', chain: 'NEAR', decimals: 18, presets: [0.01, 0.05, 0.1, 0.5] } ``` ## 🐛 Troubleshooting ### "No provider configured" Visit `http://localhost:3000/setup` to see setup instructions. ### "Permission denied (publickey)" (localhost.run) Run: `ssh-keygen -t rsa -b 2048 -f ~/.ssh/id_rsa -N ""` ### "Tunnel closed" localhost.run tunnels timeout after inactivity. Restart the server. ### "Token not configured" (HOT PAY) You need to create a payment link for each token. Missing tokens won't appear on the payment page. ### Provider returning errors - **PingPay**: Check API key is valid and account is active - **HOT PAY**: Verify item_ids match your created links ## 🔐 Security - ✅ API keys stored in `.env` (gitignored) - ✅ No private keys needed (payments go directly to provider) - ✅ HTTPS via tunnel - ✅ Webhook signature verification (HOT PAY) **Never commit `.env` to git!** ## 📦 Sharing This Skill ### As a Skill Package ```bash # From the skill directory openclaw skill pack # Share the .skill file # Users install with: openclaw skill install near-getpay.skill ``` ### Via GitHub ```bash git init git add . git commit -m "Initial commit: NEAR GetPay skill" git remote add origin https://github.com/yourusername/near-getpay.git git push -u origin main ``` Users install with: ```bash cd ~/.openclaw/skills git clone https://github.com/yourusername/near-getpay.git cd near-getpay npm install ``` ### Via Clawhub 1. Visit https://clawhub.com 2. Upload the `.skill` package 3. Add description and tags 4. Publish! ## 🤝 Support - **Issues**: GitHub Issues (if published) - **PingPay**: https://pingpay.io/docs - **HOT PAY**: https://pay.hot-labs.org/admin - **OpenClaw**: https://docs.openclaw.ai ## 📝 License MIT --- **Made for OpenClaw** 🐾

path: server-simple.ts
content: import express from 'express';
import { PingPayClient } from './scripts/pingpay-client';
import * as dotenv from 'dotenv';
import * as path from 'path';

dotenv.config({ path: path.join(__dirname, '.env') });

const app = express();
const PORT = process.env.PORT || 3000;

// Configuration
const config = {
  recipient: process.env.RECIPIENT_ADDRESS || 'meteorkent.near',
  provider: process.env.PAYMENT_PROVIDER || 'none',
  
  pingpay: {
    [REDACTED_SECRET] || '',
    configured: false
  },
  
  hotpay: {
    near_item_id: process.env.HOTPAY_NEAR_ITEM_ID || '',
    usdc_item_id: process.env.HOTPAY_USDC_ITEM_ID || '',
    usdt_item_id: process.env.HOTPAY_USDT_ITEM_ID || '',
    configured: false
  },
  
  tokens: [
    {
      symbol: 'NEAR',
      chain: 'NEAR',
      decimals: 24,
      presets: [0.5, 1, 5, 10]
    },
    {
      symbol: 'USDC',
      chain: 'NEAR',
      decimals: 6,
      presets: [5, 10, 25, 50]
    },
    {
      symbol: 'USDT',
      chain: 'NEAR',
      decimals: 6,
      presets: [5, 10, 25, 50]
    }
  ]
};

// Check what's configured
config.pingpay.configured = !!config.pingpay.apiKey;
config.hotpay.configured = !!(config.hotpay.near_item_id || config.hotpay.usdc_item_id || config.hotpay.usdt_item_id);

// Auto-select provider if only one is configured
if (config.provider === 'none') {
  if (config.pingpay.configured && !config.hotpay.configured) {
    config.provider = 'pingpay';
  } else if (config.hotpay.configured && !config.pingpay.configured) {
    config.provider = 'hotpay';
  }
}

const client = config.pingpay.configured ? new PingPayClient(config.pingpay.apiKey) : null;

app.use(express.json());

// Setup wizard page
app.get('/setup', (req, res) => {
  const html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GetPay Setup</title>
  <style>
    :root {
      --bg-body: #0b0c0e;
      --bg-card: rgba(255, 255, 255, 0.05);
      --border-color: rgba(71, 235, 165, 0.2);
      --accent-primary: #5feda9;
      --accent-gradient: linear-gradient(135deg, #a3f9d8 0%, #47eba5 100%);
      --accent-glow: 0 0 20px rgba(71, 235, 165, 0.3);
      --text-main: #ffffff;
      --text-secondary: #a0a0a0;
      --text-on-accent: #000000;
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: var(--bg-body);
      min-height: 100vh;
      padding: 20px;
    }

    .container {
      max-width: 800px;
      margin: 40px auto;
      background: var(--bg-card);
      backdrop-filter: blur(10px);
      border: 1px solid var(--border-color);
      border-radius: 16px;
      padding: 40px;
      box-shadow: var(--accent-glow);
    }

    h1 {
      color: var(--text-main);
      margin-bottom: 8px;
      font-size: 32px;
    }

    .subtitle {
      color: var(--text-secondary);
      margin-bottom: 32px;
      font-size: 16px;
    }

    .status {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 16px;
      border-radius: 8px;
      margin-bottom: 24px;
      font-weight: 600;
      border: 1px solid var(--border-color);
      backdrop-filter: blur(10px);
    }

    .status.success {
      background: rgba(71, 235, 165, 0.1);
      color: var(--accent-primary);
    }

    .status.warning {
      background: rgba(255, 171, 0, 0.1);
      color: #ffab00;
      border-color: rgba(255, 171, 0, 0.2);
    }

    .provider-section {
      border: 2px solid var(--border-color);
      border-radius: 12px;
      padding: 24px;
      margin-bottom: 24px;
      background: var(--bg-card);
      backdrop-filter: blur(10px);
    }

    .provider-section.configured {
      border-color: var(--accent-primary);
      background: rgba(71, 235, 165, 0.05);
    }

    .provider-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 16px;
    }

    .provider-title {
      font-size: 20px;
      font-weight: 700;
      color: var(--text-main);
    }

    .badge {
      padding: 6px 12px;
      border-radius: 20px;
      font-size: 12px;
      font-weight: 600;
    }

    .badge.ready {
      background: var(--accent-gradient);
      color: var(--text-on-accent);
    }

    .badge.pending {
      background: rgba(255, 171, 0, 0.2);
      color: #ffab00;
      border: 1px solid rgba(255, 171, 0, 0.3);
    }

    .steps {
      margin-top: 16px;
    }

    .step {
      margin-bottom: 12px;
      padding-left: 24px;
      color: var(--text-secondary);
      line-height: 1.6;
    }

    .step strong {
      color: var(--text-main);
    }

    .code {
      background: rgba(0, 0, 0, 0.3);
      padding: 12px;
      border-radius: 6px;
      border: 1px solid var(--border-color);
      font-family: 'Courier New', monospace;
      font-size: 13px;
      margin: 8px 0;
      overflow-x: auto;
      color: var(--accent-primary);
    }

    .link {
      color: var(--accent-primary);
      text-decoration: none;
      font-weight: 600;
    }

    .link:hover {
      text-decoration: underline;
      filter: brightness(1.2);
    }

    .continue-button {
      width: 100%;
      background: var(--accent-gradient);
      color: var(--text-on-accent);
      border: none;
      padding: 18px;
      border-radius: 8px;
      font-size: 18px;
      font-weight: 600;
      cursor: pointer;
      margin-top: 24px;
      box-shadow: var(--accent-glow);
    }

    .continue-button:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(71, 235, 165, 0.5);
    }

    .continue-button:disabled {
      background: rgba(255, 255, 255, 0.1);
      color: var(--text-secondary);
      cursor: not-allowed;
      transform: none;
      box-shadow: none;
    }

    .footer {
      margin-top: 24px;
      padding-top: 24px;
      border-top: 1px solid var(--border-color);
      color: var(--text-secondary);
      font-size: 13px;
      text-align: center;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>🚀 GetPay Setup</h1>
    <div class="subtitle">Configure your payment providers to start accepting crypto</div>

    ${!config.pingpay.configured && !config.hotpay.configured ? `
    <div class="status warning">
      <span>⚠️</span>
      <span>No payment provider configured yet. Choose one below to get started.</span>
    </div>
    ` : `
    <div class="status success">
      <span>✅</span>
      <span>Payment provider ready! You can start accepting payments.</span>
    </div>
    `}

    <!-- PingPay Section -->
    <div class="provider-section ${config.pingpay.configured ? 'configured' : ''}">
      <div class="provider-header">
        <div class="provider-title">💳 PingPay</div>
        <div class="badge ${config.pingpay.configured ? 'ready' : 'pending'}">
          ${config.pingpay.configured ? 'READY' : 'NOT CONFIGURED'}
        </div>
      </div>

      ${config.pingpay.configured ? `
        <div style="color: #5feda9; font-weight: 600; margin-bottom: 8px;">
          ✓ API Key configured
        </div>
        <div style="color: #a0a0a0; font-size: 13px;">
          ⚠️ Recipient address is configured in your <a href="https://pingpay.io/dashboard" target="_blank" class="link">PingPay Dashboard</a> account settings
        </div>
      ` : `
        <div style="color: #a0a0a0; margin-bottom: 12px;">
          Best for: Checkout sessions, invoice management, traditional payment flow
        </div>

        <div class="steps">
          <div class="step">
            <strong>1.</strong> Sign up at <a href="https://pingpay.io" target="_blank" class="link">pingpay.io</a>
          </div>
          <div class="step">
            <strong>2.</strong> Set your NEAR wallet address in Dashboard → Settings
          </div>
          <div class="step">
            <strong>3.</strong> Go to Dashboard → Settings → API Keys
          </div>
          <div class="step">
            <strong>4.</strong> Create new API key and copy it
          </div>
          <div class="step">
            <strong>5.</strong> Add to your <code>.env</code> file:
            <div class="code">PINGPAY_API_KEY=your_api_key_here<br>PAYMENT_PROVIDER=pingpay<br>RECIPIENT_ADDRESS=your-account.near</div>
          </div>
          <div class="step">
            <strong>6.</strong> Restart the server
          </div>
        </div>
      `}
    </div>

    <!-- HOT PAY Section -->
    <div class="provider-section ${config.hotpay.configured ? 'configured' : ''}">
      <div class="provider-header">
        <div class="provider-title">🔥 HOT PAY</div>
        <div class="badge ${config.hotpay.configured ? 'ready' : 'pending'}">
          ${config.hotpay.configured ? 'READY' : 'NOT CONFIGURED'}
        </div>
      </div>

      ${config.hotpay.configured ? `
        <div style="color: #5feda9; font-weight: 600; margin-bottom: 8px;">
          ✓ Payment links configured:
        </div>
        <div style="margin-left: 20px; color: #a0a0a0; margin-bottom: 8px;">
          ${config.hotpay.near_item_id ? '✓ NEAR' : '○ NEAR'}<br>
          ${config.hotpay.usdc_item_id ? '✓ USDC' : '○ USDC'}<br>
          ${config.hotpay.usdt_item_id ? '✓ USDT' : '○ USDT'}
        </div>
        <div style="color: #a0a0a0; font-size: 13px;">
          ⚠️ Recipient address is set when you created each payment link at <a href="https://pay.hot-labs.org/admin/overview" target="_blank" class="link">HOT PAY Admin</a>
        </div>
      ` : `
        <div style="color: #a0a0a0; margin-bottom: 12px;">
          Best for: Simple payment links, webhook integration, multi-token support
        </div>

        <div class="steps">
          <div class="step">
            <strong>1.</strong> Visit <a href="https://pay.hot-labs.org/admin/overview" target="_blank" class="link">HOT PAY Admin</a>
          </div>
          <div class="step">
            <strong>2.</strong> Click "Create New Payment Link"
          </div>
          <div class="step">
            <strong>3.</strong> For each token (NEAR, USDC, USDT):
            <div style="margin-left: 20px; margin-top: 8px; color: #a0a0a0;">
              • Choose the token<br>
              • <strong>Set your NEAR wallet as recipient address</strong><br>
              • Copy the <code>item_id</code> from the created link
            </div>
          </div>
          <div class="step">
            <strong>4.</strong> Add to your <code>.env</code> file:
            <div class="code">HOTPAY_NEAR_ITEM_ID=your_near_item_id<br>HOTPAY_USDC_ITEM_ID=your_usdc_item_id<br>HOTPAY_USDT_ITEM_ID=your_usdt_item_id<br>PAYMENT_PROVIDER=hotpay<br>RECIPIENT_ADDRESS=your-account.near</div>
          </div>
          <div class="step">
            <strong>5.</strong> Restart the server
          </div>
        </div>
      `}
    </div>

    ${config.pingpay.configured || config.hotpay.configured ? `
      <a href="/" style="text-decoration: none;">
        <button class="continue-button">
          Continue to Payment Page →
        </button>
      </a>
    ` : `
      <button class="continue-button" disabled>
        Configure a provider above to continue
      </button>
    `}

    <div class="footer">
      Need help? Check the <strong>SKILL.md</strong> documentation
    </div>
  </div>
</body>
</html>
  `;

  res.send(html);
});

// Main payment page - redirect to setup if not configured
app.get('/', (req, res) => {
  // Check if any provider is configured
  if (!config.pingpay.configured && !config.hotpay.configured) {
    return res.redirect('/setup');
  }

  const html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pay with Crypto</title>
  <style>
    :root {
      --bg-body: #0b0c0e;
      --bg-card: rgba(255, 255, 255, 0.05);
      --border-color: rgba(71, 235, 165, 0.2);
      --accent-primary: #5feda9;
      --accent-gradient: linear-gradient(135deg, #a3f9d8 0%, #47eba5 100%);
      --accent-glow: 0 0 20px rgba(71, 235, 165, 0.3);
      --text-main: #ffffff;
      --text-secondary: #a0a0a0;
      --text-on-accent: #000000;
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
      background: var(--bg-body);
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
    }

    .container {
      background: var(--bg-card);
      backdrop-filter: blur(10px);
      border: 1px solid var(--border-color);
      border-radius: 16px;
      padding: 40px;
      max-width: 480px;
      width: 100%;
      box-shadow: var(--accent-glow);
    }

    .header {
      text-align: center;
      margin-bottom: 32px;
    }

    h1 {
      color: var(--text-main);
      font-size: 28px;
      margin-bottom: 8px;
    }

    .recipient {
      color: var(--text-secondary);
      font-size: 14px;
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid var(--border-color);
      padding: 8px 16px;
      border-radius: 20px;
      display: inline-block;
      margin-top: 8px;
    }

    .provider-badge {
      display: inline-block;
      background: var(--accent-gradient);
      color: var(--text-on-accent);
      padding: 4px 12px;
      border-radius: 12px;
      font-size: 11px;
      font-weight: 600;
      margin-top: 8px;
      text-transform: uppercase;
    }

    .token-selector {
      display: flex;
      gap: 8px;
      margin-bottom: 24px;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border-color);
      padding: 6px;
      border-radius: 12px;
    }

    .token-btn {
      flex: 1;
      background: transparent;
      border: none;
      padding: 12px;
      border-radius: 8px;
      cursor: pointer;
      font-size: 15px;
      font-weight: 600;
      color: var(--text-secondary);
      transition: all 0.3s;
    }

    .token-btn.active {
      background: var(--accent-gradient);
      color: var(--text-on-accent);
      box-shadow: var(--accent-glow);
    }

    .token-btn:hover:not(.active):not(.disabled) {
      background: rgba(255, 255, 255, 0.05);
      color: var(--text-main);
    }

    .token-btn.disabled {
      opacity: 0.3;
      cursor: not-allowed;
    }

    .input-group {
      margin-bottom: 24px;
    }

    label {
      display: block;
      color: var(--text-main);
      font-weight: 600;
      margin-bottom: 8px;
      font-size: 14px;
    }

    .amount-input-wrapper {
      position: relative;
    }

    input[type="number"] {
      width: 100%;
      padding: 16px;
      padding-right: 60px;
      border: 2px solid var(--border-color);
      background: rgba(255, 255, 255, 0.03);
      border-radius: 8px;
      font-size: 18px;
      transition: all 0.3s;
      font-family: inherit;
      color: var(--text-main);
    }

    input[type="number"]:focus {
      outline: none;
      border-color: var(--accent-primary);
      background: rgba(255, 255, 255, 0.05);
      box-shadow: var(--accent-glow);
    }

    input[type="number"]::placeholder {
      color: var(--text-secondary);
      opacity: 0.5;
    }

    .currency-label {
      position: absolute;
      right: 16px;
      top: 50%;
      transform: translateY(-50%);
      color: var(--text-secondary);
      font-weight: 600;
      font-size: 16px;
      pointer-events: none;
    }

    .presets {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 12px;
      margin-bottom: 24px;
    }

    .preset-btn {
      background: rgba(255, 255, 255, 0.03);
      border: 2px solid var(--border-color);
      padding: 14px;
      border-radius: 8px;
      cursor: pointer;
      font-size: 16px;
      font-weight: 600;
      color: var(--text-main);
      transition: all 0.3s;
    }

    .preset-btn:hover {
      background: var(--accent-gradient);
      color: var(--text-on-accent);
      border-color: var(--accent-primary);
      box-shadow: var(--accent-glow);
    }

    .pay-button {
      width: 100%;
      background: var(--accent-gradient);
      color: var(--text-on-accent);
      border: none;
      padding: 18px;
      border-radius: 8px;
      font-size: 18px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s;
      box-shadow: var(--accent-glow);
    }

    .pay-button:hover:not(:disabled) {
      transform: translateY(-2px);
      box-shadow: 0 8px 24px rgba(71, 235, 165, 0.5);
    }

    .pay-button:active {
      transform: translateY(0);
    }

    .pay-button:disabled {
      background: rgba(255, 255, 255, 0.1);
      color: var(--text-secondary);
      cursor: not-allowed;
      box-shadow: none;
      transform: none;
    }

    .loading {
      display: none;
      text-align: center;
      margin-top: 16px;
    }

    .loading.active {
      display: block;
    }

    .spinner {
      border: 3px solid rgba(255, 255, 255, 0.1);
      border-top: 3px solid var(--accent-primary);
      border-radius: 50%;
      width: 40px;
      height: 40px;
      animation: spin 1s linear infinite;
      margin: 0 auto 12px;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .loading p {
      color: var(--text-secondary);
    }

    .error {
      background: rgba(255, 82, 82, 0.1);
      color: #ff5252;
      border: 1px solid rgba(255, 82, 82, 0.3);
      padding: 12px;
      border-radius: 8px;
      margin-top: 16px;
      display: none;
      font-size: 14px;
    }

    .error.active {
      display: block;
    }

    .footer {
      text-align: center;
      margin-top: 24px;
      padding-top: 24px;
      border-top: 1px solid var(--border-color);
      color: var(--text-secondary);
      font-size: 13px;
    }

    .footer a {
      color: var(--accent-primary);
      text-decoration: none;
    }

    .footer a:hover {
      text-decoration: underline;
      filter: brightness(1.2);
    }

    @media (max-width: 500px) {
      .container {
        padding: 28px 20px;
      }
      
      h1 {
        font-size: 24px;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>Pay with Crypto</h1>
      <div class="recipient">→ ${config.recipient}</div>
      <div class="provider-badge">Powered by ${config.provider.toUpperCase()}</div>
    </div>

    <div class="token-selector">
      ${config.tokens.map((token, i) => {
        const key = `${token.symbol.toLowerCase()}_item_id` as keyof typeof config.hotpay;
        const isDisabled = config.provider === 'hotpay' && !config.hotpay[key];
        return `
        <button class="token-btn ${i === 0 && !isDisabled ? 'active' : ''} ${isDisabled ? 'disabled' : ''}" 
                data-[REDACTED_SECRET]" 
                data-chain="${token.chain}" 
                data-decimals="${token.decimals}" 
                data-presets='${JSON.stringify(token.presets)}'
                ${isDisabled ? 'disabled' : ''}>
          ${token.symbol}
        </button>
      `}).join('')}
    </div>

    <div class="input-group">
      <label for="amount">Enter Amount</label>
      <div class="amount-input-wrapper">
        <input 
          type="number" 
          id="amount" 
          placeholder="0.0" 
          step="0.01" 
          min="0.01"
          autofocus
        >
        <span class="currency-label" id="currencyLabel">${config.tokens[0].symbol}</span>
      </div>
    </div>

    <div class="presets" id="presets">
      ${config.tokens[0].presets.map(amount => `
        <button class="preset-btn" data-amount="${amount}">
          ${amount} ${config.tokens[0].symbol}
        </button>
      `).join('')}
    </div>

    <button class="pay-button" id="payButton" disabled>
      Enter amount to pay
    </button>

    <div class="loading" id="loading">
      <div class="spinner"></div>
      <p>Creating payment session...</p>
    </div>

    <div class="error" id="error"></div>

    <div class="footer">
      Secure payments via NEAR Protocol · <a href="/setup" style="color: #667eea; text-decoration: none;">⚙️ Setup</a>
    </div>
  </div>

  <script>
    const tokens = ${JSON.stringify(config.tokens)};
    const provider = '${config.provider}';
    const hotpayConfig = ${JSON.stringify(config.hotpay)};
    
    // Filter available tokens for HOT PAY
    let availableTokens = tokens;
    if (provider === 'hotpay') {
      availableTokens = tokens.filter(t => {
        const key = t.symbol.toLowerCase() + '_item_id';
        return hotpayConfig[key];
      });
    }
    
    let currentToken = availableTokens[0];
    const amountInput = document.getElementById('amount');
    const payButton = document.getElementById('payButton');
    const loading = document.getElementById('loading');
    const errorDiv = document.getElementById('error');
    const currencyLabel = document.getElementById('currencyLabel');
    const presetsContainer = document.getElementById('presets');

    // Check for URL parameters
    const urlParams = new URLSearchParams(window.location.search);
    const urlAmount = urlParams.get('amount');
    const urlToken = urlParams.get('token');

    // Helper function to hide non-selected tokens
    function hideOtherTokens() {
      document.querySelectorAll('.token-btn').forEach(btn => {
        if (!btn.classList.contains('active')) {
          btn.style.display = 'none';
        }
      });
    }

    // Pre-select token if specified in URL
    if (urlToken) {
      const matchingToken = availableTokens.find(t => t.symbol === urlToken);
      if (matchingToken) {
        currentToken = matchingToken;
        // Update active button
        document.querySelectorAll('.token-btn').forEach(btn => {
          if (btn.dataset.token === urlToken) {
            btn.classList.add('active');
          } else {
            btn.classList.remove('active');
          }
        });
        currencyLabel.textContent = currentToken.symbol;
        updatePresets();
        // Hide other tokens when coming from URL parameter
        setTimeout(() => hideOtherTokens(), 100);
      }
    }

    // Pre-fill amount if specified in URL
    if (urlAmount && parseFloat(urlAmount) > 0) {
      amountInput.value = urlAmount;
      updatePayButton();
    }

    document.querySelectorAll('.token-btn:not(.disabled)').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.token-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');

        currentToken = {
          symbol: btn.dataset.token,
          chain: btn.dataset.chain,
          decimals: parseInt(btn.dataset.decimals),
          presets: JSON.parse(btn.dataset.presets)
        };

        currencyLabel.textContent = currentToken.symbol;
        updatePresets();
        amountInput.value = '';
        updatePayButton();
        errorDiv.classList.remove('active');
        
        // Hide other tokens after selection to avoid confusion
        hideOtherTokens();
      });
    });

    function updatePresets() {
      presetsContainer.innerHTML = currentToken.presets.map(amount => 
        \`<button class="preset-btn" data-amount="\${amount}">
          \${amount} \${currentToken.symbol}
        </button>\`
      ).join('');

      document.querySelectorAll('.preset-btn').forEach(btn => {
        btn.addEventListener('click', () => {
          amountInput.value = btn.dataset.amount;
          updatePayButton();
        });
      });
    }

    amountInput.addEventListener('input', updatePayButton);

    function updatePayButton() {
      const amount = parseFloat(amountInput.value);
      if (amount > 0) {
        payButton.disabled = false;
        payButton.textContent = \`Pay \${amount} \${currentToken.symbol}\`;
        errorDiv.classList.remove('active');
      } else {
        payButton.disabled = true;
        payButton.textContent = 'Enter amount to pay';
      }
    }

    updatePresets();

    payButton.addEventListener('click', async () => {
      const amount = parseFloat(amountInput.value);
      
      if (!amount || amount <= 0) {
        showError('Please enter a valid amount');
        return;
      }

      payButton.disabled = true;
      payButton.textContent = 'Creating session...';
      loading.classList.add('active');
      errorDiv.classList.remove('active');

      try {
        const response = await fetch('/create-session', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            amount: amount,
            [REDACTED_SECRET],
            chain: currentToken.chain
          })
        });

        const data = await response.json();

        if (data.success) {
          // Redirect directly to checkout
          loading.classList.remove('active');
          window.location.href = data.sessionUrl;
        } else{
          showError(data.error || 'Failed to create payment session');
          resetButton();
        }
      } catch (error) {
        showError('Network error: ' + error.message);
        resetButton();
      }
    });

    function showError(message) {
      errorDiv.textContent = message;
      errorDiv.classList.add('active');
    }

    function resetButton() {
      loading.classList.remove('active');
      updatePayButton();
    }

    amountInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter' && !payButton.disabled) {
        payButton.click();
      }
    });
  </script>
</body>
</html>
  `;

  res.send(html);
});

// Quick link generator endpoint - creates both links at once
app.get('/quick-link', async (req, res) => {
  try {
    const amount = parseFloat(req.query.amount as string || '0');
    const [REDACTED_SECRET] as string || 'USDC').toUpperCase();
    const chain = 'near';

    if (!amount || amount <= 0) {
      return res.json({ success: false, error: 'Invalid amount. Usage: /quick-link?amount=5&token=USDC' });
    }

    console.log(`\n🔗 Quick Link Request: ${amount} ${token}`);

    const baseUrl = `${req.protocol}://${req.get('host')}`;
    const paymentPageUrl = `${baseUrl}/?amount=${amount}&[REDACTED_SECRET]`;

    let checkoutUrl = '';

    if (config.provider === 'pingpay' && client) {
      let amountSmallest: string;
      if (token === 'NEAR') {
        amountSmallest = PingPayClient.nearToSmallestUnit(amount);
      } else if (token === 'USDC' || token === 'USDT') {
        amountSmallest = PingPayClient.usdcToSmallestUnit(amount);
      } else {
        return res.json({ success: false, error: 'Unsupported token' });
      }

      const session = await client.createCheckoutSession({
        amount: amountSmallest,
        asset: { chain, symbol: token },
        successUrl: `${baseUrl}/success`,
        cancelUrl: `${baseUrl}/cancel`,
        metadata: { amount: amount.toString(), token, chain, timestamp: Date.now() }
      });

      checkoutUrl = session.sessionUrl;
      console.log(`✅ PingPay Session: ${session.session.sessionId}`);

    } else if (config.provider === 'hotpay') {
      const itemIdMap: Record<string, string> = {
        'NEAR': config.hotpay.near_item_id,
        'USDC': config.hotpay.usdc_item_id,
        'USDT': config.hotpay.usdt_item_id
      };

      const itemId = itemIdMap[token];
      if (!itemId) {
        return res.json({ success: false, error: `HOT PAY not configured for ${token}` });
      }

      const memo = `pay_${Date.now()}_${token.toLowerCase()}`;
      const successUrl = encodeURIComponent(`${baseUrl}/success`);
      checkoutUrl = `https://pay.hot-labs.org/payment?item_id=${itemId}&amount=${amount}&redirect_url=${successUrl}&memo=${memo}`;
      console.log(`✅ HOT PAY Link: ${memo}`);
    } else {
      return res.json({ success: false, error: 'No provider configured' });
    }

    console.log(`🎨 Payment Page: ${paymentPageUrl}`);
    console.log(`⚡ Direct Checkout: ${checkoutUrl}\n`);

    res.json({
      success: true,
      amount,
      token,
      recipient: config.recipient,
      provider: config.provider,
      links: {
        paymentPage: paymentPageUrl,
        directCheckout: checkoutUrl
      }
    });

  } catch (error: any) {
    console.error('❌ Error:', error.message);
    res.json({ success: false, error: error.message });
  }
});

// Create payment session
app.post('/create-session', async (req, res) => {
  try {
    const { amount, token, chain } = req.body;

    if (!amount || amount <= 0) {
      return res.json({ success: false, error: 'Invalid amount' });
    }

    console.log(`\n💳 Payment Request: ${amount} ${token} (${chain})`);

    if (config.provider === 'hotpay') {
      const itemIdMap: Record<string, string> = {
        'NEAR': config.hotpay.near_item_id,
        'USDC': config.hotpay.usdc_item_id,
        'USDT': config.hotpay.usdt_item_id
      };

      const itemId = itemIdMap[token];
      if (!itemId) {
        return res.json({ 
          success: false, 
          error: `HOT PAY item_id not configured for ${token}. Go to /setup to configure.` 
        });
      }

      const memo = `pay_${Date.now()}_${token.toLowerCase()}`;
      const successUrl = encodeURIComponent(`${req.protocol}://${req.get('host')}/success`);
      const hotpayUrl = `https://pay.hot-labs.org/payment?item_id=${itemId}&amount=${amount}&redirect_url=${successUrl}&memo=${memo}`;

      console.log(`✅ HOT PAY Link`);
      console.log(`   Memo: ${memo}`);
      console.log(`   Direct Checkout: ${hotpayUrl}`);
      
      const baseUrl = `${req.protocol}://${req.get('host')}`;
      const paymentPageUrl = `${baseUrl}/?amount=${amount}&[REDACTED_SECRET]`;
      console.log(`   Payment Page: ${paymentPageUrl}\n`);

      res.json({
        success: true,
        sessionUrl: hotpayUrl,
        paymentPageUrl: paymentPageUrl,
        provider: 'hotpay',
        memo: memo
      });

    } else if (config.provider === 'pingpay') {
      if (!client) {
        return res.json({ 
          success: false, 
          error: 'PingPay not configured. Go to /setup to add API key.' 
        });
      }

      let amountSmallest: string;
      if (token === 'NEAR') {
        amountSmallest = PingPayClient.nearToSmallestUnit(amount);
      } else if (token === 'USDC' || token === 'USDT') {
        amountSmallest = PingPayClient.usdcToSmallestUnit(amount);
      } else {
        return res.json({ success: false, error: 'Unsupported token' });
      }

      console.log(`   Amount in smallest unit: ${amountSmallest}`);

      const session = await client.createCheckoutSession({
        amount: amountSmallest,
        asset: { chain, symbol: token },
        successUrl: `${req.protocol}://${req.get('host')}/success`,
        cancelUrl: `${req.protocol}://${req.get('host')}/cancel`,
        metadata: {
          amount: amount.toString(),
          token,
          chain,
          timestamp: Date.now()
        }
      });

      console.log(`✅ Session: ${session.session.sessionId}`);
      console.log(`🔗 Direct Checkout: ${session.sessionUrl}`);
      
      const baseUrl = `${req.protocol}://${req.get('host')}`;
      const paymentPageUrl = `${baseUrl}/?amount=${amount}&[REDACTED_SECRET]`;
      console.log(`🎨 Payment Page: ${paymentPageUrl}\n`);

      res.json({
        success: true,
        sessionUrl: session.sessionUrl,
        paymentPageUrl: paymentPageUrl,
        sessionId: session.session.sessionId,
        provider: 'pingpay'
      });
    } else {
      res.json({ 
        success: false, 
        error: 'No payment provider configured. Go to /setup.' 
      });
    }

  } catch (error: any) {
    console.error('❌ Error:', error.message);
    res.json({ success: false, error: error.message });
  }
});

// HOT PAY Webhook endpoint
app.post('/webhook/hotpay', (req, res) => {
  try {
    const payload = req.body;
    
    console.log('\n🔔 HOT PAY Webhook Received');
    console.log('='.repeat(70));
    console.log(JSON.stringify(payload, null, 2));
    console.log('='.repeat(70));
    
    if (payload.type === 'PAYMENT_STATUS_UPDATE' && payload.status === 'SUCCESS') {
      console.log(`✅ Payment Confirmed!`);
      console.log(`   Item ID: ${payload.item_id}`);
      console.log(`   Amount: $${payload.amount_float} (${payload.amount_usd} USD)`);
      console.log(`   Memo: ${payload.memo || 'N/A'}`);
      console.log(`   TX Hash: ${payload.near_trx}`);
      console.log(`   Verify: https://nearblocks.io/txns/${payload.near_trx}`);
    }
    
    res.status(200).json({ received: true });
    
  } catch (error: any) {
    console.error('❌ Webhook Error:', error.message);
    res.status(500).json({ error: 'Webhook processing failed' });
  }
});

// Success page
app.get('/success', (req, res) => {
  res.send(`
<!DOCTYPE html>
<html>
<head>
  <title>Payment Successful</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #0b0c0e;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 0;
      padding: 20px;
    }
    .container {
      background: rgba(255, 255, 255, 0.05);
      backdrop-filter: blur(10px);
      border: 1px solid rgba(71, 235, 165, 0.2);
      border-radius: 16px;
      padding: 48px 32px;
      text-align: center;
      max-width: 400px;
      box-shadow: 0 0 20px rgba(71, 235, 165, 0.3);
    }
    .icon { 
      font-size: 64px; 
      margin-bottom: 16px;
      filter: drop-shadow(0 0 10px rgba(71, 235, 165, 0.5));
    }
    h1 { 
      color: #5feda9; 
      margin-bottom: 12px;
      text-shadow: 0 0 20px rgba(71, 235, 165, 0.3);
    }
    p { 
      color: #a0a0a0; 
      line-height: 1.6; 
      margin-bottom: 24px; 
    }
    a {
      display: inline-block;
      background: linear-gradient(135deg, #a3f9d8 0%, #47eba5 100%);
      color: #000000;
      padding: 14px 32px;
      border-radius: 8px;
      text-decoration: none;
      font-weight: 600;
      box-shadow: 0 0 20px rgba(71, 235, 165, 0.3);
      transition: all 0.3s;
    }
    a:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 24px rgba(71, 235, 165, 0.5);
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="icon">✅</div>
    <h1>Payment Successful!</h1>
    <p>Your payment has been processed successfully.</p>
    <a href="/">Make Another Payment</a>
  </div>
</body>
</html>
  `);
});

// Cancel page
app.get('/cancel', (req, res) => {
  res.send(`
<!DOCTYPE html>
<html>
<head>
  <title>Payment Cancelled</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #0b0c0e;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 0;
      padding: 20px;
    }
    .container {
      background: rgba(255, 255, 255, 0.05);
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 171, 0, 0.2);
      border-radius: 16px;
      padding: 48px 32px;
      text-align: center;
      max-width: 400px;
      box-shadow: 0 0 20px rgba(255, 171, 0, 0.2);
    }
    .icon { 
      font-size: 64px; 
      margin-bottom: 16px;
      filter: drop-shadow(0 0 10px rgba(255, 171, 0, 0.3));
    }
    h1 { 
      color: #ffab00; 
      margin-bottom: 12px;
      text-shadow: 0 0 20px rgba(255, 171, 0, 0.2);
    }
    p { 
      color: #a0a0a0; 
      line-height: 1.6; 
      margin-bottom: 24px; 
    }
    a {
      display: inline-block;
      background: linear-gradient(135deg, #ffd54f 0%, #ffab00 100%);
      color: #000000;
      padding: 14px 32px;
      border-radius: 8px;
      text-decoration: none;
      font-weight: 600;
      box-shadow: 0 0 20px rgba(255, 171, 0, 0.2);
      transition: all 0.3s;
    }
    a:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 24px rgba(255, 171, 0, 0.4);
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="icon">❌</div>
    <h1>Payment Cancelled</h1>
    <p>No problem. You can try again anytime.</p>
    <a href="/">Try Again</a>
  </div>
</body>
</html>
  `);
});

app.listen(PORT, () => {
  console.log(`\n✅ GetPay server running on port ${PORT}`);
  console.log(`📍 Local: http://localhost:${PORT}`);
  console.log(`💳 Provider: ${config.provider.toUpperCase()}`);
  console.log(`📧 Recipient: ${config.recipient}`);
  if (config.provider === 'hotpay') {
    console.log(`🔔 Webhook: http://localhost:${PORT}/webhook/hotpay`);
  }
  if (!config.pingpay.configured && !config.hotpay.configured) {
    console.log(`\n⚠️  No provider configured. Visit http://localhost:${PORT}/setup to get started.\n`);
  } else {
    console.log('');
  }
});

export default app;


path: skill.json
content: {
  "name": "near-getpay",
  "version": "1.0.0",
  "description": "Accept crypto payments (NEAR, USDC, USDT) via beautiful payment pages with PingPay or HOT PAY integration",
  "author": "OpenClaw Community",
  "license": "MIT",
  "main": "index.ts",
  "type": "skill",
  "tags": [
    "near",
    "payments",
    "crypto",
    "pingpay",
    "hotpay",
    "usdc",
    "usdt",
    "donations",
    "web3"
  ],
  "requirements": {
    "node": ">=16.0.0",
    "packages": [
      "express",
      "axios",
      "dotenv",
      "typescript",
      "ts-node"
    ]
  },
  "setup": {
    "steps": [
      "Copy .env.example to .env",
      "Choose payment provider (PingPay or HOT PAY)",
      "Configure provider credentials in .env",
      "Run: ./start.sh"
    ],
    "wizardUrl": "/setup"
  },
  "features": [
    "Beautiful mobile-responsive payment UI",
    "Multi-token support (NEAR, USDC, USDT)",
    "Dual provider (PingPay + HOT PAY)",
    "Automatic public URL generation",
    "First-time setup wizard",
    "Webhook integration (HOT PAY)",
    "Customizable preset amounts"
  ],
  "documentation": {
    "main": "SKILL.md",
    "readme": "README.md",
    "examples": "SKILL.md#usage-in-openclaw"
  },
  "environment": {
    "required": [
      "RECIPIENT_ADDRESS",
      "PAYMENT_PROVIDER"
    ],
    "optional": [
      "PORT",
      "PINGPAY_API_KEY",
      "PINGPAY_BASE_URL",
      "HOTPAY_NEAR_ITEM_ID",
      "HOTPAY_USDC_ITEM_ID",
      "HOTPAY_USDT_ITEM_ID"
    ]
  },
  "commands": {
    "start": "./start.sh",
    "dev": "npx ts-node server-simple.ts",
    "install": "npm install"
  }
}


path: package-lock.json
content: {
  "name": "near-pingpay",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "near-pingpay",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "@types/node": "^22.10.5",
        "axios": "^1.7.9",
        "decimal.js": "^10.4.3",
        "dotenv": "^16.4.7",
        "express": "^4.21.2",
        "localtunnel": "^2.0.2",
        "ts-node": "^10.9.2",
        "typescript": "^5.7.3"
      },
      "devDependencies": {
        "@types/express": "^5.0.6",
        "@types/localtunnel": "^2.0.4"
      }
    },
    "node_modules/@cspotcode/source-map-support": {
      "version": "0.8.1",
      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@jridgewell/trace-mapping": "0.3.9"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@jridgewell/resolve-uri": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@jridgewell/trace-mapping": {
      "version": "0.3.9",
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@jridgewell/resolve-uri": "^3.0.3",
        "@jridgewell/sourcemap-codec": "^1.4.10"
      }
    },
    "node_modules/@tsconfig/node10": {
      "version": "1.0.12",
      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@tsconfig/node12": {
      "version": "1.0.11",
      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@tsconfig/node14": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@tsconfig/node16": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@types/body-parser": {
      "version": "1.19.6",
      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/connect": "*",
        "@types/node": "*"
      }
    },
    "node_modules/@types/connect": {
      "version": "3.4.38",
      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/express": {
      "version": "5.0.6",
      "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/body-parser": "*",
        "@types/express-serve-static-core": "^5.0.0",
        "@types/serve-static": "^2"
      }
    },
    "node_modules/@types/express-serve-static-core": {
      "version": "5.1.1",
      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*",
        "@types/qs": "*",
        "@types/range-parser": "*",
        "@types/send": "*"
      }
    },
    "node_modules/@types/http-errors": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/localtunnel": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/@types/localtunnel/-/localtunnel-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/node": {
      "version": "22.19.11",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/@types/qs": {
      "version": "6.14.0",
      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/range-parser": {
      "version": "1.2.7",
      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/send": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/serve-static": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/http-errors": "*",
        "@types/node": "*"
      }
    },
    "node_modules/accepts": {
      "version": "1.3.8",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "~2.1.34",
        "negotiator": "0.6.3"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/acorn": {
      "version": "8.15.0",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/acorn-walk": {
      "version": "8.3.4",
      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "acorn": "^8.11.0"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/arg": {
      "version": "4.1.3",
      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/array-flatten": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/axios": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.11",
        "form-data": "^4.0.5",
        "proxy-from-env": "^1.1.0"
      }
    },
    "node_modules/body-parser": {
      "version": "1.20.4",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "content-type": "~1.0.5",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "~1.2.0",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.4.24",
        "on-finished": "~2.4.1",
        "qs": "~6.14.0",
        "raw-body": "~2.5.3",
        "type-is": "~1.6.18",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/cliui": {
      "version": "7.0.4",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "string-width": "^4.2.0",
        "strip-ansi": "^6.0.0",
        "wrap-ansi": "^7.0.0"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/content-disposition": {
      "version": "0.5.4",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "5.2.1"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/create-require": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/decimal.js": {
      "version": "10.6.0",
      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/destroy": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/diff": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.3.1"
      }
    },
    "node_modules/dotenv": {
      "version": "16.6.1",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escalade": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/express": {
      "version": "4.22.1",
      "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "~1.3.8",
        "array-flatten": "1.1.1",
        "body-parser": "~1.20.3",
        "content-disposition": "~0.5.4",
        "content-type": "~1.0.4",
        "cookie": "~0.7.1",
        "cookie-signature": "~1.0.6",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "finalhandler": "~1.3.1",
        "fresh": "~0.5.2",
        "http-errors": "~2.0.0",
        "merge-descriptors": "1.0.3",
        "methods": "~1.1.2",
        "on-finished": "~2.4.1",
        "parseurl": "~1.3.3",
        "path-to-regexp": "~0.1.12",
        "proxy-addr": "~2.0.7",
        "qs": "~6.14.0",
        "range-parser": "~1.2.1",
        "safe-buffer": "5.2.1",
        "send": "~0.19.0",
        "serve-static": "~1.16.2",
        "setprototypeof": "1.2.0",
        "statuses": "~2.0.1",
        "type-is": "~1.6.18",
        "utils-merge": "1.0.1",
        "vary": "~1.1.2"
      },
      "engines": {
        "node": ">= 0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/finalhandler": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "on-finished": "~2.4.1",
        "parseurl": "~1.3.3",
        "statuses": "~2.0.2",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.15.11",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "0.5.2",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-caller-file": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": "6.* || 8.* || >= 10.*"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.4.24",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-fullwidth-code-point": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/localtunnel": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "axios": "0.21.4",
        "debug": "4.3.2",
        "openurl": "1.1.1",
        "yargs": "17.1.1"
      },
      "bin": {
        "lt": "bin/lt.js"
      },
      "engines": {
        "node": ">=8.3.0"
      }
    },
    "node_modules/localtunnel/node_modules/axios": {
      "version": "0.21.4",
      "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.14.0"
      }
    },
    "node_modules/localtunnel/node_modules/debug": {
      "version": "4.3.2",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "2.1.2"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/localtunnel/node_modules/ms": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/make-error": {
      "version": "1.3.6",
      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "0.3.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/methods": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/openurl": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "0.1.12",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/qs": {
      "version": "6.14.2",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "2.5.3",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.4.24",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/require-directory": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "0.19.2",
      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "1.2.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "fresh": "~0.5.2",
        "http-errors": "~2.0.1",
        "mime": "1.6.0",
        "ms": "2.1.3",
        "on-finished": "~2.4.1",
        "range-parser": "~1.2.1",
        "statuses": "~2.0.2"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/send/node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/serve-static": {
      "version": "1.16.3",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "parseurl": "~1.3.3",
        "send": "~0.19.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/ts-node": {
      "version": "10.9.2",
      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@cspotcode/source-map-support": "^0.8.0",
        "@tsconfig/node10": "^1.0.7",
        "@tsconfig/node12": "^1.0.7",
        "@tsconfig/node14": "^1.0.0",
        "@tsconfig/node16": "^1.0.2",
        "acorn": "^8.4.1",
        "acorn-walk": "^8.1.1",
        "arg": "^4.1.0",
        "create-require": "^1.1.0",
        "diff": "^4.0.1",
        "make-error": "^1.1.1",
        "v8-compile-cache-lib": "^3.0.1",
        "yn": "3.1.1"
      },
      "bin": {
        "ts-node": "dist/bin.js",
        "ts-node-cwd": "dist/bin-cwd.js",
        "ts-node-esm": "dist/bin-esm.js",
        "ts-node-script": "dist/bin-script.js",
        "ts-node-transpile-only": "dist/bin-transpile.js",
        "ts-script": "dist/bin-script-deprecated.js"
      },
      "peerDependencies": {
        "@swc/core": ">=1.2.50",
        "@swc/wasm": ">=1.2.50",
        "@types/node": "*",
        "typescript": ">=2.7"
      },
      "peerDependenciesMeta": {
        "@swc/core": {
          "optional": true
        },
        "@swc/wasm": {
          "optional": true
        }
      }
    },
    "node_modules/type-is": {
      "version": "1.6.18",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "media-typer": "0.3.0",
        "mime-types": "~2.1.24"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/utils-merge": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4.0"
      }
    },
    "node_modules/v8-compile-cache-lib": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/wrap-ansi": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/y18n": {
      "version": "5.0.8",
      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yargs": {
      "version": "17.1.1",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "cliui": "^7.0.2",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.0",
        "y18n": "^5.0.5",
        "yargs-parser": "^20.2.2"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yargs-parser": {
      "version": "20.2.9",
      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yn": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    }
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "moduleResolution": "node"
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "dist"]
}


path: server.ts
content: import express from 'express';
import { PingPayClient } from './scripts/pingpay-client';
import * as dotenv from 'dotenv';
import * as path from 'path';

dotenv.config({ path: path.join(__dirname, '.env') });

const app = express();
const PORT = process.env.PORT || 3000;

const [REDACTED_SECRET];
if (!apiKey) {
  console.error('❌ PINGPAY_API_KEY not found in .env');
  process.exit(1);
}

const client = new PingPayClient(apiKey);

// Serve static HTML page
app.get('/', (req, res) => {
  const html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Buy Me a Coffee ☕ - NEAR Payment</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
    }

    .container {
      background: white;
      border-radius: 20px;
      padding: 40px;
      max-width: 500px;
      width: 100%;
      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
      animation: fadeIn 0.5s ease-in;
    }

    @keyframes fadeIn {
      from {
        opacity: 0;
        transform: translateY(20px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    .header {
      text-align: center;
      margin-bottom: 30px;
    }

    .avatar {
      width: 100px;
      height: 100px;
      border-radius: 50%;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      margin: 0 auto 20px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 50px;
    }

    h1 {
      color: #333;
      font-size: 28px;
      margin-bottom: 10px;
    }

    .subtitle {
      color: #666;
      font-size: 16px;
      margin-bottom: 10px;
    }

    .recipient {
      color: #667eea;
      font-size: 14px;
      font-weight: 600;
      background: #f0f0ff;
      padding: 8px 16px;
      border-radius: 20px;
      display: inline-block;
      margin-top: 10px;
    }

    .description {
      color: #666;
      text-align: center;
      margin-bottom: 30px;
      line-height: 1.6;
    }

    .payment-options {
      display: grid;
      gap: 15px;
      margin-bottom: 30px;
    }

    .payment-card {
      background: #f8f9fa;
      border: 2px solid #e0e0e0;
      border-radius: 15px;
      padding: 20px;
      cursor: pointer;
      transition: all 0.3s ease;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .payment-card:hover {
      border-color: #667eea;
      transform: translateY(-2px);
      box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
    }

    .payment-card.selected {
      border-color: #667eea;
      background: #f0f0ff;
    }

    .payment-info {
      flex: 1;
    }

    .payment-label {
      font-weight: 600;
      color: #333;
      margin-bottom: 5px;
    }

    .payment-amount {
      font-size: 24px;
      font-weight: 700;
      color: #667eea;
    }

    .payment-usd {
      color: #999;
      font-size: 14px;
      margin-left: 8px;
    }

    .emoji {
      font-size: 32px;
    }

    .pay-button {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      padding: 18px 40px;
      border-radius: 50px;
      font-size: 18px;
      font-weight: 600;
      cursor: pointer;
      width: 100%;
      transition: all 0.3s ease;
      box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
    }

    .pay-button:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
    }

    .pay-button:active {
      transform: translateY(0);
    }

    .pay-button:disabled {
      background: #ccc;
      cursor: not-allowed;
      box-shadow: none;
    }

    .loading {
      display: none;
      text-align: center;
      color: #667eea;
      margin-top: 20px;
    }

    .loading.active {
      display: block;
    }

    .spinner {
      border: 3px solid #f3f3f3;
      border-top: 3px solid #667eea;
      border-radius: 50%;
      width: 40px;
      height: 40px;
      animation: spin 1s linear infinite;
      margin: 0 auto 10px;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .footer {
      text-align: center;
      margin-top: 30px;
      padding-top: 20px;
      border-top: 1px solid #e0e0e0;
      color: #999;
      font-size: 14px;
    }

    .powered-by {
      margin-top: 10px;
    }

    .powered-by a {
      color: #667eea;
      text-decoration: none;
      font-weight: 600;
    }

    @media (max-width: 600px) {
      .container {
        padding: 30px 20px;
      }

      h1 {
        font-size: 24px;
      }

      .payment-amount {
        font-size: 20px;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <div class="avatar">☕</div>
      <h1>Buy Me a Coffee</h1>
      <p class="subtitle">Support my work on NEAR Protocol</p>
      <span class="recipient">→ meteorkent.near</span>
    </div>

    <p class="description">
      Your support helps me build amazing things on NEAR! 
      Choose an amount below and pay with crypto from any chain.
    </p>

    <div class="payment-options">
      <div class="payment-card" data-amount="0.5" data-token="NEAR" data-chain="NEAR">
        <div class="payment-info">
          <div class="payment-label">Coffee</div>
          <div class="payment-amount">
            0.5 NEAR
            <span class="payment-usd">~$2</span>
          </div>
        </div>
        <div class="emoji">☕</div>
      </div>

      <div class="payment-card" data-amount="1" data-token="NEAR" data-chain="NEAR">
        <div class="payment-info">
          <div class="payment-label">Double Shot</div>
          <div class="payment-amount">
            1 NEAR
            <span class="payment-usd">~$4</span>
          </div>
        </div>
        <div class="emoji">☕☕</div>
      </div>

      <div class="payment-card" data-amount="5" data-token="USDC" data-chain="Base">
        <div class="payment-info">
          <div class="payment-label">Lunch</div>
          <div class="payment-amount">
            $5 USDC
          </div>
        </div>
        <div class="emoji">🍔</div>
      </div>

      <div class="payment-card" data-amount="10" data-token="USDC" data-chain="Base">
        <div class="payment-info">
          <div class="payment-label">Supporter</div>
          <div class="payment-amount">
            $10 USDC
          </div>
        </div>
        <div class="emoji">❤️</div>
      </div>
    </div>

    <button class="pay-button" id="payButton" disabled>
      Select an amount above
    </button>

    <div class="loading" id="loading">
      <div class="spinner"></div>
      <p>Creating payment link...</p>
    </div>

    <div class="footer">
      <p>✨ Powered by NEAR Intents - Pay from any chain</p>
      <p class="powered-by">
        Built with <a href="https://pingpay.io" target="_blank">PingPay</a>
      </p>
    </div>
  </div>

  <script>
    let selectedAmount = null;
    let selectedToken = null;
    let selectedChain = null;

    // Handle card selection
    document.querySelectorAll('.payment-card').forEach(card => {
      card.addEventListener('click', () => {
        // Remove selected from all cards
        document.querySelectorAll('.payment-card').forEach(c => {
          c.classList.remove('selected');
        });

        // Add selected to clicked card
        card.classList.add('selected');

        // Get payment details
        selectedAmount = parseFloat(card.dataset.amount);
        selectedToken = card.dataset.token;
        selectedChain = card.dataset.chain;

        // Enable button
        const button = document.getElementById('payButton');
        button.disabled = false;
        button.textContent = \`Pay \${selectedAmount} \${selectedToken}\`;
      });
    });

    // Handle payment
    document.getElementById('payButton').addEventListener('click', async () => {
      if (!selectedAmount) return;

      const button = document.getElementById('payButton');
      const loading = document.getElementById('loading');

      // Show loading
      button.disabled = true;
      button.textContent = 'Creating session...';
      loading.classList.add('active');

      try {
        // Call our API to create checkout session
        const response = await fetch('/create-session', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            amount: selectedAmount,
            [REDACTED_SECRET],
            chain: selectedChain
          })
        });

        const data = await response.json();

        if (data.success) {
          // Redirect to PingPay checkout
          window.location.href = data.sessionUrl;
        } else {
          alert('Error creating session: ' + data.error);
          button.disabled = false;
          button.textContent = \`Pay \${selectedAmount} \${selectedToken}\`;
          loading.classList.remove('active');
        }
      } catch (error) {
        alert('Error: ' + error.message);
        button.disabled = false;
        button.textContent = \`Pay \${selectedAmount} \${selectedToken}\`;
        loading.classList.remove('active');
      }
    });
  </script>
</body>
</html>
  `;

  res.send(html);
});

// API endpoint to create checkout session
app.use(express.json());
app.post('/create-session', async (req, res) => {
  try {
    const { amount, token, chain } = req.body;

    if (!amount || !token || !chain) {
      return res.json({ success: false, error: 'Missing required fields' });
    }

    // Calculate amount in smallest unit
    let amountSmallest: string;
    if (token === 'NEAR') {
      amountSmallest = PingPayClient.nearToSmallestUnit(amount);
    } else if (token === 'USDC') {
      amountSmallest = PingPayClient.usdcToSmallestUnit(amount);
    } else {
      return res.json({ success: false, error: 'Unsupported token' });
    }

    console.log(`\n💳 Creating session: ${amount} ${token} on ${chain}`);
    console.log(`   Amount in smallest unit: ${amountSmallest}`);

    // Create checkout session
    const session = await client.createCheckoutSession({
      amount: amountSmallest,
      asset: {
        chain: chain,
        symbol: token
      },
      successUrl: `${req.protocol}://${req.get('host')}/success`,
      cancelUrl: `${req.protocol}://${req.get('host')}/cancel`,
      metadata: {
        orderId: `coffee_${Date.now()}`,
        amount: amount,
        token: token,
        chain: chain
      }
    });

    console.log(`✅ Session created: ${session.session.sessionId}`);
    console.log(`🔗 Checkout URL: ${session.sessionUrl}`);

    res.json({
      success: true,
      sessionUrl: session.sessionUrl,
      sessionId: session.session.sessionId
    });

  } catch (error: any) {
    console.error('❌ Error creating session:', error.message);
    res.json({
      success: false,
      error: error.message
    });
  }
});

// Success page
app.get('/success', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>Payment Successful! ✅</title>
      <style>
        body {
          font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
          background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
          min-height: 100vh;
          display: flex;
          align-items: center;
          justify-content: center;
          margin: 0;
          padding: 20px;
        }
        .container {
          background: white;
          border-radius: 20px;
          padding: 60px 40px;
          text-align: center;
          max-width: 500px;
          box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
        }
        .success-icon {
          font-size: 80px;
          margin-bottom: 20px;
        }
        h1 {
          color: #11998e;
          margin-bottom: 15px;
        }
        p {
          color: #666;
          line-height: 1.6;
          margin-bottom: 30px;
        }
        .button {
          background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
          color: white;
          padding: 15px 40px;
          border-radius: 50px;
          text-decoration: none;
          display: inline-block;
          font-weight: 600;
        }
      </style>
    </head>
    <body>
      <div class="container">
        <div class="success-icon">✅</div>
        <h1>Payment Successful!</h1>
        <p>Thank you for your support! Your payment has been processed successfully via NEAR Intents.</p>
        <a href="/" class="button">Buy Another Coffee ☕</a>
      </div>
    </body>
    </html>
  `);
});

// Cancel page
app.get('/cancel', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>Payment Cancelled</title>
      <style>
        body {
          font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
          background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
          min-height: 100vh;
          display: flex;
          align-items: center;
          justify-content: center;
          margin: 0;
          padding: 20px;
        }
        .container {
          background: white;
          border-radius: 20px;
          padding: 60px 40px;
          text-align: center;
          max-width: 500px;
          box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
        }
        .cancel-icon {
          font-size: 80px;
          margin-bottom: 20px;
        }
        h1 {
          color: #f5576c;
          margin-bottom: 15px;
        }
        p {
          color: #666;
          line-height: 1.6;
          margin-bottom: 30px;
        }
        .button {
          background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
          color: white;
          padding: 15px 40px;
          border-radius: 50px;
          text-decoration: none;
          display: inline-block;
          font-weight: 600;
        }
      </style>
    </head>
    <body>
      <div class="container">
        <div class="cancel-icon">❌</div>
        <h1>Payment Cancelled</h1>
        <p>No worries! Your payment was cancelled. Feel free to try again anytime.</p>
        <a href="/" class="button">Try Again</a>
      </div>
    </body>
    </html>
  `);
});

app.listen(PORT, () => {
  console.log(`\n🚀 Buy Me a Coffee server started!`);
  console.log(`📍 Local: http://localhost:${PORT}`);
  console.log(`\n🔗 Starting tunnel...\n`);
});

export default app;


path: package.json
content: {
  "name": "near-getpay",
  "version": "1.0.0",
  "description": "Accept crypto payments (NEAR, USDC, USDT) via beautiful payment pages with PingPay or HOT PAY",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "npm run test:api",
    "test:api": "ts-node test-final.ts",
    "start": "ts-node start-tunnel.ts",
    "dev": "ts-node server-simple.ts",
    "clean": "rm -rf dist",
    "prepublishOnly": "npm run clean && npm run build"
  },
  "keywords": [
    "near",
    "pingpay",
    "hotpay",
    "getpay",
    "payments",
    "crypto",
    "openclaw",
    "skill",
    "donations",
    "web3",
    "usdc",
    "usdt"
  ],
  "author": "OpenClaw Community",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/near-getpay.git"
  },
  "bugs": {
    "url": "https://github.com/yourusername/near-getpay/issues"
  },
  "homepage": "https://github.com/yourusername/near-getpay#readme",
  "type": "commonjs",
  "dependencies": {
    "@types/node": "^22.10.5",
    "axios": "^1.7.9",
    "decimal.js": "^10.4.3",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "localtunnel": "^2.0.2",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.3"
  },
  "devDependencies": {
    "@types/express": "^5.0.6",
    "@types/localtunnel": "^2.0.4"
  }
}


path: start-tunnel.ts
content: #!/usr/bin/env ts-node

import { spawn } from 'child_process';
import * as path from 'path';

const PORT = 3000;

async function startServer() {
  console.log('\n🚀 Starting GetPay Payment Server...\n');
  console.log('='.repeat(70));

  // Start Express server
  console.log('\n1️⃣  Starting Express server...');
  const serverPath = path.join(__dirname, 'server-simple.ts');
  const server = spawn('npx', ['ts-node', serverPath], {
    stdio: 'inherit',
    env: { ...process.env, PORT: PORT.toString() }
  });

  // Wait for server to start
  await new Promise(resolve => setTimeout(resolve, 3000));

  // Start localhost.run tunnel
  console.log('\n2️⃣  Creating public tunnel with localhost.run...');
  console.log('    (This is more reliable than localtunnel)\n');
  
  const tunnel = spawn('ssh', [
    '-R',
    `80:localhost:${PORT}`,
    'localhost.run',
    '-o',
    'StrictHostKeyChecking=no'
  ]);

  let tunnelUrl = '';
  
  tunnel.stdout.on('data', (data) => {
    const output = data.toString();
    console.log(output);
    
    // Extract URL from localhost.run output
    const urlMatch = output.match(/https:\/\/[a-zA-Z0-9-]+\.lhr\.life/);
    if (urlMatch && !tunnelUrl) {
      tunnelUrl = urlMatch[0];
      console.log('\n' + '='.repeat(70));
      console.log('\n✅ Tunnel created successfully!');
      console.log('\n🌐 PUBLIC URL:');
      console.log(`\n   ${tunnelUrl}\n`);
      console.log('='.repeat(70));
      console.log('\n📱 Share this link to receive payments!');
      console.log('💡 First-time users will be guided through setup');
      console.log('\n⚡ Server is running... Press Ctrl+C to stop\n');
    }
  });

  tunnel.stderr.on('data', (data) => {
    // localhost.run sends connection info to stderr
    const output = data.toString();
    if (output.includes('https://')) {
      console.log(output);
    }
  });

  tunnel.on('close', () => {
    console.log('\n❌ Tunnel closed');
    server.kill();
    process.exit();
  });

  // Handle Ctrl+C
  process.on('SIGINT', () => {
    console.log('\n\n👋 Shutting down...');
    tunnel.kill();
    server.kill();
    process.exit();
  });
}

startServer().catch(error => {
  console.error('❌ Error:', error.message);
  process.exit(1);
});


path: index.ts
content: import { PaymentOrchestrator } from './scripts/payment-orchestrator';
import * as dotenv from 'dotenv';
import * as path from 'path';

// Load environment variables
dotenv.config({ path: path.join(__dirname, '.env') });

/**
 * Main entry point for NEAR PingPay skill
 * Integrates with NEAR Intents skill to enable Web2 invoice payments using any crypto token
 */

export interface PayInvoiceParams {
  invoiceId: string;
  nearAccount: string;
  sourceToken?: string; // Default: "NEAR"
  nearIntentsSkillPath?: string; // Path to near-intents skill
}

export interface ListInvoicesParams {
  status?: 'pending' | 'paid' | 'expired' | 'cancelled';
}

export interface CreateTestInvoiceParams {
  amount: number;
  recipientAddress: string;
  description?: string;
}

/**
 * Pay a PingPay invoice using any token from NEAR wallet
 */
export async function payInvoice(params: PayInvoiceParams): Promise<any> {
  const [REDACTED_SECRET];
  if (!apiKey) {
    throw new Error('PINGPAY_API_KEY not configured in .env');
  }

  const orchestrator = new PaymentOrchestrator(apiKey);
  
  // Import NEAR Intents skill
  const nearIntentsPath = params.nearIntentsSkillPath || '../near-intents';
  const { executeIntent } = await import(nearIntentsPath);

  // Create NEAR Intents executor wrapper
  const nearIntentsExecutor = async (intentParams: {
    fromToken: string;
    toToken: string;
    amount: string;
    fromChain: string;
    toChain: string;
    toAddress: string;
  }) => {
    // Two-step execution for reliability:
    // Step 1: Swap to USDC on NEAR
    const swapResult = await executeIntent({
      operation: 'swap',
      fromToken: intentParams.fromToken,
      toToken: 'USDC',
      amount: intentParams.amount,
      chain: 'NEAR',
      nearAccount: params.nearAccount
    });

    // Step 2: Bridge USDC to Base
    const bridgeResult = await executeIntent({
      operation: 'withdraw',
      token: 'USDC',
      amount: intentParams.amount,
      fromChain: 'NEAR',
      toChain: 'Base',
      toAddress: intentParams.toAddress,
      nearAccount: params.nearAccount
    });

    return {
      swapTxHash: swapResult.txHash,
      bridgeTxHash: bridgeResult.txHash
    };
  };

  return orchestrator.payInvoice({
    invoiceId: params.invoiceId,
    nearAccount: params.nearAccount,
    sourceToken: params.sourceToken || 'NEAR',
    nearIntentsExecutor
  });
}

/**
 * List invoices by status
 */
export async function listInvoices(params?: ListInvoicesParams): Promise<any> {
  const [REDACTED_SECRET];
  if (!apiKey) {
    throw new Error('PINGPAY_API_KEY not configured in .env');
  }

  const orchestrator = new PaymentOrchestrator(apiKey);
  
  if (params?.status === 'pending') {
    return orchestrator.getPendingInvoices();
  }

  // For other statuses, use the PingPay client directly
  const { PingPayClient } = await import('./scripts/pingpay-client');
  const client = new PingPayClient(apiKey);
  return client.listInvoices({ status: params?.status });
}

/**
 * Create a test invoice (for development/testing)
 */
export async function createTestInvoice(params: CreateTestInvoiceParams): Promise<any> {
  const [REDACTED_SECRET];
  if (!apiKey) {
    throw new Error('PINGPAY_API_KEY not configured in .env');
  }

  const orchestrator = new PaymentOrchestrator(apiKey);
  return orchestrator.createTestInvoice({
    amount: params.amount,
    recipient_address: params.recipientAddress,
    description: params.description
  });
}

// For CLI usage
if (require.main === module) {
  const command = process.argv[2];
  
  switch (command) {
    case 'pay':
      const invoiceId = process.argv[3];
      const nearAccount = process.argv[4];
      const sourceToken = process.argv[5] || 'NEAR';
      
      if (!invoiceId || !nearAccount) {
        console.error('Usage: ts-node index.ts pay <invoice_id> <near_account> [source_token]');
        process.exit(1);
      }
      
      payInvoice({ invoiceId, nearAccount, sourceToken })
        .then(result => {
          console.log('\n✅ Payment Result:');
          console.log(JSON.stringify(result, null, 2));
        })
        .catch(error => {
          console.error('❌ Error:', error.message);
          process.exit(1);
        });
      break;

    case 'list':
      const status = process.argv[3] as any;
      listInvoices({ status })
        .then(invoices => {
          console.log('\n📄 Invoices:');
          console.log(JSON.stringify(invoices, null, 2));
        })
        .catch(error => {
          console.error('❌ Error:', error.message);
          process.exit(1);
        });
      break;

    case 'create-test':
      const amount = parseFloat(process.argv[3]);
      const recipientAddress = process.argv[4];
      const description = process.argv[5];
      
      if (!amount || !recipientAddress) {
        console.error('Usage: ts-node index.ts create-test <amount> <recipient_address> [description]');
        process.exit(1);
      }
      
      createTestInvoice({ amount, recipientAddress, description })
        .then(invoice => {
          console.log('\n✅ Test Invoice Created:');
          console.log(JSON.stringify(invoice, null, 2));
        })
        .catch(error => {
          console.error('❌ Error:', error.message);
          process.exit(1);
        });
      break;

    default:
      console.log(`
NEAR PingPay - Pay Web2 invoices with any crypto token

Commands:
  pay <invoice_id> <near_account> [source_token]
    Pay a PingPay invoice using tokens from your NEAR wallet
    Example: ts-node index.ts pay INV123 cuongdcdev.near PEPE

  list [status]
    List invoices (status: pending|paid|expired|cancelled)
    Example: ts-node index.ts list pending

  create-test <amount> <recipient_address> [description]
    Create a test invoice for development
    Example: ts-node index.ts create-test 10 0x30FE694284a082a5D1adfF6D25C0B9B6bF61F77D

Configuration:
  Create .env file with:
    PINGPAY_API_KEY=your_api_key_here
    NEAR_ACCOUNT_ID=your_account.near
    NEAR_PRIVATE_KEY=ed25519:...
      `);
  }
}


path: start.sh
content: #!/bin/bash

echo ""
echo "🚀 Starting GetPay Server with localhost.run tunnel..."
echo ""

cd "$(dirname "$0")"

# Start the server
npx ts-node start-tunnel.ts


path: scripts/pingpay-client.ts
content: import axios, { AxiosInstance } from 'axios';

// Based on ping-checkout-example + Actual API Response
export interface CheckoutAsset {
  chain: string;  // e.g., "NEAR", "Base", "ETH"
  symbol: string; // e.g., "USDC", "NEAR", "ETH"
}

export interface CreateCheckoutSessionInput {
  amount: string;           // Amount in smallest unit (e.g., "100000" for 0.1 USDC)
  asset: CheckoutAsset;     // Chain and symbol (API resolves to assetId)
  successUrl?: string;      // Redirect URL after successful payment
  cancelUrl?: string;       // Redirect URL if payment cancelled
  metadata?: Record<string, unknown>; // Optional metadata (orderId, etc.)
  expiresAt?: string;       // Optional: ISO 8601 timestamp for expiration
  expiresInSeconds?: number; // Optional: Seconds until expiration (alternative to expiresAt)
  // NOTE: Recipient is configured in organization settings, NOT in request!
}

// Actual response structure from PingPay API
export interface CheckoutAmount {
  assetId: string;  // e.g., "nep141:base-0x833...913.omft.near"
  amount: string;   // Amount in smallest unit
  decimals: number; // Token decimals (e.g., 6 for USDC)
}

export interface CheckoutRecipient {
  address: string;  // Recipient NEAR address from org settings
}

export interface CheckoutSession {
  sessionId: string;
  status: string;              // "CREATED", "COMPLETED", "EXPIRED", etc.
  paymentId: string | null;    // Null until payment completed
  amount: CheckoutAmount;
  recipient: CheckoutRecipient;
  successUrl?: string;
  cancelUrl?: string;
  createdAt: string;           // ISO 8601 timestamp
  expiresAt?: string;          // ISO 8601 timestamp (default: 1 hour)
  metadata?: Record<string, unknown>;
}

export interface CreateCheckoutSessionResponse {
  session: CheckoutSession;
  sessionUrl: string;       // Full checkout URL to redirect user to
}

export interface PingPayError {
  message: string;
  code?: string;
}

export class PingPayClient {
  private client: AxiosInstance;
  private [REDACTED_SECRET];
  private baseURL: string;

  constructor([REDACTED_SECRET], baseURL: string = 'https://pay.pingpay.io') {
    this.[REDACTED_SECRET];
    this.baseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
    
    this.client = axios.create({
      baseURL: this.baseURL + '/api', // Add /api prefix
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': apiKey
      },
      timeout: 30000
    });
  }

  /**
   * Create a new checkout session
   * 
   * Creates a payment link that can be shared with users.
   * Recipient address is configured in your PingPay organization settings.
   * 
   * Example:
   * ```typescript
   * const response = await client.createCheckoutSession({
   *   amount: '1000000', // 1 USDC (6 decimals)
   *   asset: { chain: 'NEAR', symbol: 'USDC' },
   *   expiresInSeconds: 31536000, // 1 year
   *   successUrl: 'https://example.com/success',
   *   cancelUrl: 'https://example.com/cancel',
   *   metadata: { orderId: 'order_123' }
   * });
   * 
   * // Share the payment link
   * console.log(response.sessionUrl);
   * // https://pay.pingpay.io/checkout?sessionId=cs_xxx
   * ```
   * 
   * @param input - Checkout session parameters
   * @returns Checkout session with sessionUrl
   */
  async createCheckoutSession(input: CreateCheckoutSessionInput): Promise<CreateCheckoutSessionResponse> {
    try {
      const response = await this.client.post('/checkout/sessions', input);
      return response.data;
    } catch (error: any) {
      const errorMsg = this.formatError(error, 'create checkout session');
      throw new Error(errorMsg);
    }
  }

  /**
   * Get checkout session details by ID
   * 
   * @param sessionId - The session ID
   * @returns Checkout session details
   */
  async getCheckoutSession(sessionId: string): Promise<CheckoutSession> {
    try {
      const response = await this.client.get(`/checkout/sessions/${sessionId}`);
      return response.data.session || response.data;
    } catch (error: any) {
      const errorMsg = this.formatError(error, 'get checkout session');
      throw new Error(errorMsg);
    }
  }

  /**
   * Test API connection
   */
  async testConnection(): Promise<{ success: boolean; message: string; sessionUrl?: string }> {
    try {
      const testInput: CreateCheckoutSessionInput = {
        amount: "1000000", // 1 USDC
        asset: { chain: "NEAR", symbol: "USDC" },
        metadata: { test: true }
      };
      
      const response = await this.client.post('/checkout/sessions', testInput);
      return { 
        success: true, 
        message: `API connection successful! Session: ${response.data.session.sessionId}`,
        sessionUrl: response.data.sessionUrl
      };
    } catch (error: any) {
      return { 
        success: false, 
        message: this.formatError(error, 'test connection')
      };
    }
  }

  /**
   * Helper: Convert decimal USDC to smallest unit (6 decimals)
   */
  static usdcToSmallestUnit(amount: number): string {
    return Math.floor(amount * 1_000_000).toString();
  }

  /**
   * Helper: Convert decimal NEAR to smallest unit (24 decimals)
   */
  static nearToSmallestUnit(amount: number): string {
    const yocto = BigInt(Math.floor(amount * 1e24));
    return yocto.toString();
  }

  /**
   * Helper: Convert smallest unit back to decimal
   */
  static fromSmallestUnit(amount: string, decimals: number): number {
    return parseInt(amount) / Math.pow(10, decimals);
  }

  /**
   * Helper: Format checkout amount for display
   */
  static formatAmount(checkoutAmount: CheckoutAmount): string {
    const decimal = this.fromSmallestUnit(checkoutAmount.amount, checkoutAmount.decimals);
    return `${decimal.toFixed(checkoutAmount.decimals)}`;
  }

  /**
   * Helper: Calculate expiration timestamp
   * @param durationSeconds - Duration in seconds (e.g., 31536000 for 1 year)
   * @returns ISO 8601 timestamp
   */
  static getExpirationTimestamp(durationSeconds: number): string {
    const now = new Date();
    const expiresAt = new Date(now.getTime() + durationSeconds * 1000);
    return expiresAt.toISOString();
  }

  /**
   * Format error messages with useful debugging info
   */
  private formatError(error: any, operation: string): string {
    if (error.response) {
      const status = error.response.status;
      const data = error.response.data;
      const url = error.config?.url || 'unknown';
      
      // PingPay error format
      if (data?.message) {
        return `Failed to ${operation}: ${data.message}${data.code ? ` (${data.code})` : ''}`;
      }
      
      return `Failed to ${operation}: HTTP ${status} from ${url}\n` +
             `Response: ${JSON.stringify(data, null, 2)}`;
    } else if (error.request) {
      return `Failed to ${operation}: No response from server\n` +
             `Check API endpoint: ${this.baseURL}/api`;
    } else {
      return `Failed to ${operation}: ${error.message}`;
    }
  }
}


path: scripts/payment-orchestrator.ts
content: import { PingPayClient, PingPayInvoice } from './pingpay-client';
import Decimal from 'decimal.js';

/**
 * Payment orchestrator that integrates NEAR Intents with PingPay
 * Flow: Any Token → USDC (NEAR) → USDC (Base) → PingPay Invoice
 */
export class PaymentOrchestrator {
  private pingpay: PingPayClient;

  constructor([REDACTED_SECRET]) {
    this.pingpay = new PingPayClient(apiKey);
  }

  /**
   * Pay a PingPay invoice using any token from NEAR wallet
   * 
   * @param invoiceId - PingPay invoice ID
   * @param nearAccount - NEAR account to pay from
   * @param sourceToken - Token to swap from (e.g., "NEAR", "PEPE", "SHIB")
   * @param nearIntentsExecutor - Function to execute NEAR intent (swap + bridge)
   * @returns Payment result with transaction hashes
   */
  async payInvoice(params: {
    invoiceId: string;
    nearAccount: string;
    sourceToken: string;
    nearIntentsExecutor: (params: {
      fromToken: string;
      toToken: string;
      amount: string;
      fromChain: string;
      toChain: string;
      toAddress: string;
    }) => Promise<{ swapTxHash?: string; bridgeTxHash?: string }>;
  }): Promise<{
    success: boolean;
    invoice: PingPayInvoice;
    swapTxHash?: string;
    bridgeTxHash?: string;
    paymentId?: string;
    error?: string;
  }> {
    const { invoiceId, nearAccount, sourceToken, nearIntentsExecutor } = params;

    try {
      // Step 1: Fetch invoice details
      console.log(`📄 Fetching invoice ${invoiceId}...`);
      const invoice = await this.pingpay.getInvoice(invoiceId);

      // Validate invoice
      if (invoice.status !== 'pending') {
        throw new Error(`Invoice status is ${invoice.status}, expected 'pending'`);
      }

      if (invoice.currency.toUpperCase() !== 'USDC') {
        throw new Error(`Invoice currency is ${invoice.currency}, only USDC is supported`);
      }

      const usdcAmount = new Decimal(invoice.amount);
      console.log(`💰 Invoice amount: ${usdcAmount.toString()} USDC`);
      console.log(`📍 Recipient address: ${invoice.recipient_address}`);

      // Step 2: Execute NEAR intent (swap + bridge)
      // Two-step approach for reliability:
      // 1. Swap sourceToken → USDC on NEAR
      // 2. Bridge USDC → Base chain
      console.log(`\n🔄 Executing payment flow: ${sourceToken} → USDC (NEAR) → USDC (Base)...`);
      
      const intentResult = await nearIntentsExecutor({
        fromToken: sourceToken,
        toToken: 'USDC',
        amount: usdcAmount.toString(),
        fromChain: 'NEAR',
        toChain: 'Base',
        toAddress: invoice.recipient_address
      });

      if (!intentResult.bridgeTxHash) {
        throw new Error('Bridge transaction failed - no transaction hash received');
      }

      console.log(`✅ Swap TX: ${intentResult.swapTxHash || 'N/A'}`);
      console.log(`✅ Bridge TX: ${intentResult.bridgeTxHash}`);

      // Step 3: Submit payment proof to PingPay
      console.log(`\n📤 Submitting payment to PingPay...`);
      const paymentResult = await this.pingpay.submitPayment({
        invoice_id: invoiceId,
        tx_hash: intentResult.bridgeTxHash,
        amount: usdcAmount.toNumber(),
        from_address: nearAccount
      });

      console.log(`✅ Payment submitted: ${paymentResult.payment_id}`);
      console.log(`Status: ${paymentResult.status}`);

      return {
        success: true,
        invoice,
        swapTxHash: intentResult.swapTxHash,
        bridgeTxHash: intentResult.bridgeTxHash,
        paymentId: paymentResult.payment_id
      };

    } catch (error: any) {
      console.error(`❌ Payment failed: ${error.message}`);
      return {
        success: false,
        invoice: {} as PingPayInvoice,
        error: error.message
      };
    }
  }

  /**
   * Get pending invoices that need payment
   */
  async getPendingInvoices(): Promise<PingPayInvoice[]> {
    return this.pingpay.listInvoices({ status: 'pending' });
  }

  /**
   * Create a test invoice for development
   */
  async createTestInvoice(params: {
    amount: number;
    recipient_address: string;
    description?: string;
  }): Promise<PingPayInvoice> {
    return this.pingpay.createInvoice({
      amount: params.amount,
      currency: 'USDC',
      recipient_address: params.recipient_address,
      description: params.description || 'Test invoice from NEAR PingPay skill',
      expires_in_hours: 24
    });
  }

  /**
   * Calculate required source token amount for a given USDC invoice
   * (This would integrate with price service to calculate optimal amount)
   */
  async calculateRequiredAmount(params: {
    invoiceAmount: number; // USDC
    sourceToken: string;
    priceService: ([REDACTED_SECRET]) => Promise<number>;
  }): Promise<{
    sourceAmount: string;
    sourceToken: string;
    targetAmount: string;
    estimatedSlippage: string;
  }> {
    const { invoiceAmount, sourceToken, priceService } = params;

    // Get prices
    const usdcPrice = 1; // USDC is pegged to $1
    const sourcePrice = await priceService(sourceToken);

    // Calculate required amount with 1% slippage buffer
    const slippageMultiplier = 1.01;
    const requiredUSD = invoiceAmount * usdcPrice;
    const sourceAmount = (requiredUSD / sourcePrice) * slippageMultiplier;

    return {
      sourceAmount: new Decimal(sourceAmount).toFixed(6),
      sourceToken,
      targetAmount: invoiceAmount.toString(),
      estimatedSlippage: '1%'
    };
  }
}


path: README.md
content: # 💳 NEAR GetPay - Crypto Payment Pages Made Easy

Accept cryptocurrency payments (NEAR, USDC, USDT) through a beautiful hosted payment page with PingPay or HOT PAY integration.

[![OpenClaw Skill](https://img.shields.io/badge/OpenClaw-Skill-blue)](https://openclaw.ai)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)

## 🌟 Features

- ✨ **Beautiful payment UI** - Mobile-responsive with preset amounts
- 💰 **Multi-token support** - Accept NEAR, USDC, or USDT
- 🔄 **Dual provider** - Works with PingPay or HOT PAY
- 🌐 **Public URLs** - Automatic tunnel via localhost.run
- 🧙 **Setup wizard** - Guides first-time users step-by-step
- 🔒 **Secure** - No private keys needed, HTTPS enforced
- 🎯 **Smart token selection** - Once token selected, other options hide (HOT PAY flow)

## 💡 How It Works

### PingPay Flow
1. User selects token (NEAR/USDC/USDT)
2. Enters amount or picks preset
3. Clicks "Pay Now" → Redirects to PingPay checkout

### HOT PAY Flow
1. User sees only configured tokens (those with item_ids)
2. Selects token → **Other tokens hide automatically**
3. Enters amount → Redirects to specific HOT PAY link for that token
4. Each token has its own checkout URL (configured in HOT PAY dashboard)

## 🚀 Quick Start

### 1. Clone & Install

```bash
cd ~/.openclaw/skills
git clone https://github.com/yourusername/near-getpay.git
cd near-getpay
npm install
```

### 2. Configure Provider

**Choose ONE (or both):**

#### Option A: PingPay (Recommended)

1. Sign up at [pingpay.io](https://pingpay.io)
2. Set your NEAR wallet in Dashboard → Settings
3. Get API key from Dashboard → Settings → API Keys
4. Add to `.env`:

```env
RECIPIENT_ADDRESS=your-account.near
PAYMENT_PROVIDER=pingpay
PINGPAY_API_KEY=your_api_key_here
```

#### Option B: HOT PAY

1. Visit [HOT PAY Admin](https://pay.hot-labs.org/admin/overview)
2. Create payment links for each token (NEAR, USDC, USDT)
3. Set your NEAR wallet as recipient for each link
4. Copy each `item_id` and add to `.env`:

```env
RECIPIENT_ADDRESS=your-account.near
PAYMENT_PROVIDER=hotpay
HOTPAY_NEAR_ITEM_ID=your_near_item_id
HOTPAY_USDC_ITEM_ID=your_usdc_item_id
HOTPAY_USDT_ITEM_ID=your_usdt_item_id
```

### 3. Start Server

```bash
./start.sh
```

You'll see output like:

```
✅ GetPay server running on port 3000
🌐 PUBLIC URL:
   https://abc123xyz.lhr.life

📱 Share this link to receive payments!
```

### 4. Accept Payments!

Share the generated URL and start receiving crypto payments instantly.

## 📸 Screenshots

### Payment Page
![Payment Page](https://via.placeholder.com/600x400?text=Beautiful+Payment+UI)

### Setup Wizard
![Setup Wizard](https://via.placeholder.com/600x400?text=First-Time+Setup+Wizard)

## ⚠️ Important: Recipient Configuration

**The recipient address (where payments go) is set at the provider level:**

- **PingPay**: Dashboard → Settings → Wallet Address
- **HOT PAY**: Set when creating each payment link

The `RECIPIENT_ADDRESS` in `.env` is **only for display** on your payment page.

## 🎯 Use Cases

- 💝 Accept donations on your website
- 🛍️ Sell digital products
- ☕ "Buy me a coffee" in crypto
- 💰 Freelance payments
- 🎁 Gift registries
- 📱 Mobile-friendly tipping

## 🔗 API - Generate Payment Links

**NEW:** Use the `/quick-link` endpoint to instantly generate both payment page and direct checkout links!

### Endpoint

```
GET /quick-link?amount={amount}&[REDACTED_SECRET]
```

### Example

```bash
curl "https://your-domain.lhr.life/quick-link?amount=5&token=USDC"
```

### Response

```json
{
  "success": true,
  "amount": 5,
  "token": "USDC",
  "recipient": "your-account.near",
  "provider": "pingpay",
  "links": {
    "paymentPage": "https://your-domain.lhr.life/?amount=5&token=USDC",
    "directCheckout": "https://pay.pingpay.io/checkout?sessionId=cs_..."
  }
}
```

### Use Cases

- **Share both links with customers** (beautiful UI + fallback)
- **Programmatically generate payment links** for invoices
- **Embed in your app/bot** to create checkout sessions
- **Pre-fill payment amounts** from your backend

### Supported Tokens

- `NEAR` - NEAR Protocol
- `USDC` - USD Coin (on NEAR)
- `USDT` - Tether (on NEAR)

**Example usage:**
```bash
# $10 USDC payment
curl "/quick-link?amount=10&token=USDC"

# 5 NEAR payment
curl "/quick-link?amount=5&token=NEAR"

# $25 USDT payment
curl "/quick-link?amount=25&token=USDT"
```

## 🛠️ OpenClaw Integration

This skill works seamlessly with OpenClaw. Example conversation:

```
You: Create a payment page for me

Agent: I'll set up GetPay for you. Do you have PingPay or HOT PAY?

You: What's easier?

Agent: PingPay is simpler! Here's what to do:
       1. Sign up at pingpay.io
       2. Add your wallet in settings
       3. Get an API key
       Share the key and I'll handle the rest!

You: Here's my key: sk_live_abc123...

Agent: Perfect! Starting your payment server...
       ✅ Live at: https://xyz789.lhr.life
       Share this link to accept payments!
```

## 📖 Documentation

- [SKILL.md](SKILL.md) - Complete skill documentation
- [OpenClaw Docs](https://docs.openclaw.ai) - OpenClaw documentation
- [PingPay Docs](https://pingpay.io/docs) - PingPay API reference
- [HOT PAY Docs](https://pay.hot-labs.org/admin) - HOT PAY dashboard

## 🔧 Configuration

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `RECIPIENT_ADDRESS` | ✅ | Display name (actual recipient set in provider dashboard) |
| `PAYMENT_PROVIDER` | ✅ | `pingpay` or `hotpay` |
| `PINGPAY_API_KEY` | For PingPay | Your PingPay API key |
| `HOTPAY_NEAR_ITEM_ID` | For HOT PAY | NEAR payment link item_id |
| `HOTPAY_USDC_ITEM_ID` | For HOT PAY | USDC payment link item_id |
| `HOTPAY_USDT_ITEM_ID` | For HOT PAY | USDT payment link item_id |
| `PORT` | ❌ | Server port (default: 3000) |

### Custom Tunnel

Don't like localhost.run? Edit `start-tunnel.ts` to use:

- ngrok
- Cloudflare Tunnel
- localtunnel
- Your own reverse proxy

## 🎨 Customization

### Change Preset Amounts

Edit `server-simple.ts`:

```typescript
tokens: [
  {
    symbol: 'NEAR',
    presets: [1, 5, 10, 25]  // Your amounts
  }
]
```

### Branding

Update colors, fonts, and text in the HTML templates in `server-simple.ts`.

### Add Tokens

Add more tokens to the config (requires provider support):

```typescript
{
  symbol: 'ETH',
  chain: 'NEAR',
  decimals: 18,
  presets: [0.01, 0.05, 0.1]
}
```

## 🐛 Troubleshooting

| Issue | Solution |
|-------|----------|
| "No provider configured" | Visit `/setup` page for instructions |
| "Permission denied" (SSH) | Run `ssh-keygen -t rsa -f ~/.ssh/id_rsa -N ""` |
| "Tunnel closed" | Restart server (localhost.run has timeouts) |
| "Token not configured" | Create payment link for that token in HOT PAY |
| Provider errors | Verify API key / item_ids are correct |

## 🔐 Security Best Practices

- ✅ Never commit `.env` to git
- ✅ Use environment variables for secrets
- ✅ Rotate API keys periodically
- ✅ Use HTTPS (enforced by tunnels)
- ✅ Verify webhook signatures (HOT PAY)

## 📦 Sharing

### Install from GitHub

```bash
cd ~/.openclaw/skills
git clone https://github.com/yourusername/near-getpay.git
cd near-getpay
npm install
```

### Skill Package

```bash
openclaw skill pack
openclaw skill install near-getpay.skill
```

### Clawhub

Upload to [Clawhub](https://clawhub.com) for one-click installation.

## 🤝 Contributing

PRs welcome! Areas that need help:

- [ ] More payment providers (Stripe Crypto, Coinbase Commerce)
- [ ] Better analytics/dashboard
- [ ] Email notifications
- [ ] Custom domain support
- [ ] Multi-language support

## 📝 License

MIT - See [LICENSE](LICENSE)

## 🙏 Credits

- [NEAR Protocol](https://near.org) - Blockchain infrastructure
- [PingPay](https://pingpay.io) - Payment gateway
- [HOT PAY](https://pay.hot-labs.org) - Payment links
- [OpenClaw](https://openclaw.ai) - AI agent framework
- [localhost.run](https://localhost.run) - Free tunneling

## 📬 Support

- 🐛 Issues: [GitHub Issues](https://github.com/yourusername/near-getpay/issues)
- 💬 Discord: [OpenClaw Community](https://discord.com/invite/clawd)
- 📧 Email: [REDACTED_SECRET]

---

**Made with 🐾 for OpenClaw**


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