Skip to content

feat: phone credit (CoinPay), 71 countries, agents on the phone, qc call/phone (0.15.0, qc 0.10.0) - #336

Merged
ralyodio merged 1 commit into
masterfrom
feat/phone-credit-intl-agents
Oct 26, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/phone-credit-intl-agents

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Anthony: "do it all": paid credit, international calling, agents placing calls, and qc.

Credit (CoinPay)

  • Ran coinpay-onboard site qryptchat-web: every step ✓ (business made, 16 payout wallets copied, webhook pointed at /api/phone/coinpay-webhook). The four COINPAY_* keys are in the vault and on dev2.
  • POST /api/phone/topup {dollars: 5|10|25} creates a phone_topups row and a hosted checkout (any coin or PayPal) whose metadata names the row. The webhook verifies x-coinpay-signature and credits exactly once (phone_credit_topup), matching by metadata or, failing that, by checkout session.

Billing, in micro-dollars

  • US/Canada: free seconds first (10 min/day for signed-in accounts), then 1¢ per started minute, charged at hang-up.
  • Other countries: 2× what Telnyx charges us, from its detail records (telnyx_session_id, a cost per leg). These appear a minute or so later, so settlement is retried 90 s / 5 min / 30 min after the call and also runs on GET /api/phone. phone_charge_call charges once. Measured real costs: US phone leg $0.002/min in whole minutes, browser leg $0.002/min.
  • Integer micros throughout: 2 * 0.0072 * 1e6 is 14400.000000000002 in floating point, which over-charged by one micro before the fix.

International

  • The Telnyx profile allows 71 countries, with max_destination_rate $0.10 and the $10/day cap unchanged.
  • libphonenumber-js parsing. Premium-rate, shared-cost and UAN numbers are refused.
  • No free minutes abroad. The allowance is computed at the worst case of 20.4¢/min, so a call can't cost more than the balance.

Agents on the phone

  • New Call Control app qrypt.chat agent calls: its events go to /api/phone/agent-webhook and into phone_agent_events.
  • The agent host takes no inbound connections (no root for nginx, and ufw blocks container → host), so it long-polls /api/phone/agent-events with AGENT_PHONE_SECRET.
  • agent-phone (dev2) uses Telnyx transcription_start plus speak with ElevenLabs voices (integration secret qrypt_elevenlabs). Its first line says it's an AI assistant calling for Anthony.

qc 0.10.0: qc phone (free minutes, credit, recent calls) and qc call <number> (opens the dialer prefilled via /phone?to=).

Already applied on dev2: migrations 20261026150000_phone_credit.sql and 20261026160000_phone_agent_events.sql (existing calls backfilled as settled), plus env COINPAY_*, AGENT_PHONE_SECRET and TELNYX_AGENT_APP_ID (app.env backups bak-010/011, vault).

bun run test:ci passes 841/841. New tests cover rules (international parsing, allowance, charges) and phone (free US call, paid US minutes, international settlement from records, refusals, operators, dial failure, top-up credited once, top-up by session).

🤖 Generated with Claude Code

…all/phone (0.15.0, qc 0.10.0)

Anthony: "do it all" (paid credit, international, agents placing calls, qc).

- Credit: $5/$10/$25 on CoinPay's hosted checkout (business "qryptchat-web" made
  with coinpay-onboard; keys in vault + app.env). POST /api/phone/topup ->
  phone_topups row + checkout session; /api/phone/coinpay-webhook verifies the
  signature and credits exactly once (phone_credit_topup), by metadata or session.
- Billing in micro-dollars: US/Canada free minutes first, then 1 cent per started
  minute, charged at hang-up; other countries 2x the carrier cost read from Telnyx
  detail records (telnyx_session_id), settled when the records exist (retried, and on
  GET /api/phone). phone_charge_call charges once. Integer micros (2 * 0.0072 * 1e6
  is 14400.000000000002 in floating point).
- International: 71 countries on the Telnyx profile with max_destination_rate 0.10,
  libphonenumber-js parsing, premium/shared-cost/UAN refused, no free minutes abroad,
  allowance limited at the worst-case 20.4 cents/min.
- Agents: Call Control app "qrypt.chat agent calls" posts to /api/phone/agent-webhook,
  stored in phone_agent_events; the agent host long-polls /api/phone/agent-events
  (AGENT_PHONE_SECRET) because it takes no inbound connections. agent-phone (on the
  host) transcribes with Telnyx transcription_start and speaks with ElevenLabs via a
  Telnyx integration secret.
- qc phone (minutes, credit, recent calls) and qc call <number> (opens the dialer).
- /phone: credit and buy buttons, + for international numbers, per-call charges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

17 finding(s)

MEDIUM: 13 | LOW: 4

Severity Rule Location
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:66
MEDIUM js-unescaped-html-sink src/app/faq/page.jsx:57
MEDIUM js-unescaped-html-sink src/app/layout.jsx:142
MEDIUM js-unescaped-html-sink src/app/layout.jsx:146
MEDIUM js-unescaped-html-sink src/app/page.jsx:50
MEDIUM js-open-redirect src/app/phone/page.jsx:70
MEDIUM redos-nested-quantifier src/lib/auth/dns-name.js:88
MEDIUM redos-nested-quantifier src/lib/chat/voice-mode.js:116
MEDIUM redos-nested-quantifier src/lib/chat/voice-mode.js:121
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:85
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:130
MEDIUM js-unescaped-html-sink src/lib/components/PitFooter.jsx:15
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/private-key-import-export.test.js:252
LOW secret-generic-credential tests/private-key-import-export.test.js:264

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread src/app/phone/page.jsx
const res = await fetch('/api/phone/topup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dollars }) });
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.url) throw new Error(data.error || 'Could not start the checkout');
window.location.href = data.url;
@socket-security

Copy link
Copy Markdown
Contributor

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​coinpay-onboard@​0.2.0771009892100
Addedlibphonenumber-js@​1.12.2510010010095100

View full report

@socket-security

Copy link
Copy Markdown
Contributor

Caution

Review the following alerts detected in dependencies.

According to your organization's Security Policy, you must resolve all "Block" alerts before proceeding. It is recommended to resolve "Warn" alerts too. Learn more about Socket for GitHub.

Priority Alert  (click "▶" to expand/collapse) Action
High priority
High CVE: Zod can exhaust memory while validating oversized constrained collections

CVE: GHSA-mrhx-c7xc-m3w5 Zod can exhaust memory while validating oversized constrained collections (HIGH)

Affected versions: < 4.6.6

Patched version: 4.6.6

From: package.json → npm/@profullstack/coinpay-onboard@0.2.0 → npm/eslint-config-next@16.4.3 → npm/zod@3.25.76

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/zod@3.25.76. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Block
Low priority
Low adoption: npm @profullstack/coinpay-onboard

Location: Package overview

From: package.json → npm/@profullstack/coinpay-onboard@0.2.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@profullstack/coinpay-onboard@0.2.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn

View full report

@ralyodio
ralyodio merged commit d79e1bb into master Oct 26, 2026
9 of 10 checks passed
@ralyodio
ralyodio deleted the feat/phone-credit-intl-agents branch October 26, 2026 13:14
ralyodio added a commit that referenced this pull request Oct 26, 2026
….15.1) (#337)

#336 was merged with Socket's "Pull Request Alerts" check failing: the package
pulled zod 3.25.76 (GHSA-mrhx-c7xc-m3w5, memory exhaustion validating oversized
constrained collections, fixed in 4.6.6) plus the MCP SDK and express, all for
its CLI/MCP. The app only needs three things from it, now in
src/lib/server/coinpay.js with the same behaviour: createCheckoutSession,
verifyWebhook (HMAC-SHA256 "t.body", 5 min), readWebhook. zod is back to 4.6.7.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants