Skip to content

Commit f512b39

Browse files
authored
Merge pull request #225 from MorpheusAIs/claude/coinbase-commerce-to-business-YLl89
Migrate Coinbase Commerce to Business Payment Link API
2 parents 8c35bcb + 8a3de23 commit f512b39

13 files changed

Lines changed: 855 additions & 11 deletions

docker-compose.local.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ services:
7272
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
7373
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
7474

75+
# Coinbase Business / CDP (new Payment Link API)
76+
CDP_API_KEY_ID: ${CDP_API_KEY_ID:-}
77+
CDP_API_KEY_SECRET: ${CDP_API_KEY_SECRET:-}
78+
CDP_SANDBOX: ${CDP_SANDBOX:-true}
79+
COINBASE_PAYMENT_LINK_WEBHOOK_SECRET: ${COINBASE_PAYMENT_LINK_WEBHOOK_SECRET:-}
80+
7581
LOCAL_TESTING_MODE: ${LOCAL_TESTING_MODE:-false}
7682

7783
# Cognito authentication (ensure these are passed from .env.local)
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Coinbase Commerce to Business Migration - Backend Review Checklist
2+
3+
**Migration Deadline: March 31, 2026**
4+
**Reference**: [Coinbase Transition Guide](https://help.coinbase.com/en/transitioning-from-coinbase-commerce-to-coinbase-business)
5+
6+
---
7+
8+
## 1. Coinbase Dashboard / Account Setup
9+
10+
- [ ] **Create Coinbase Business account** (or convert existing Commerce account)
11+
- [Getting Started](https://docs.cdp.coinbase.com/coinbase-business/introduction/get-started)
12+
- [ ] **Complete KYB (Know Your Business) verification** if not already done
13+
- [ ] **Generate CDP Secret API Key** in the [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys)
14+
- Go to the **Secret API Keys** tab and click **Create API key**
15+
- Signature algorithm: Ed25519 (recommended) or ECDSA
16+
- Save the **Key ID** (UUID) → `CDP_API_KEY_ID`
17+
- Save the **Key Secret** (base64 string) → `CDP_API_KEY_SECRET`
18+
- These are shown only once
19+
20+
---
21+
22+
## 2. Environment Variables to Configure
23+
24+
| Variable | Description | Where |
25+
|---|---|---|
26+
| `CDP_API_KEY_ID` | Secret API Key ID (UUID) from [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys) | All environments |
27+
| `CDP_API_KEY_SECRET` | Secret API Key secret (base64) from CDP Portal | All environments (secrets manager) |
28+
| `CDP_SANDBOX` | `true` for sandbox (no real transactions), `false` for production | Per environment |
29+
| `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` | From webhook subscription metadata | All environments |
30+
31+
### Variables to Keep (Legacy - Transition Period)
32+
33+
| Variable | Reason |
34+
|---|---|
35+
| `COINBASE_COMMERCE_WEBHOOK_SECRET` | Legacy Commerce API — **kept during transition period** for backward compatibility |
36+
37+
> **Note:** Legacy Commerce variables will be removed after migration is confirmed complete and all in-flight Commerce charges have settled.
38+
39+
---
40+
41+
## 3. Webhook Configuration
42+
43+
- [ ] **Register webhook endpoint** in Coinbase Business dashboard
44+
- URL: `https://<api-domain>/api/v1/webhooks/coinbase`
45+
- Content-Type: `application/json`
46+
- [ ] **Subscribe to events**:
47+
- `payment_link.payment.success`
48+
- `payment_link.payment.failed`
49+
- `payment_link.payment.expired`
50+
- [ ] **Save the webhook secret** from the subscription metadata response
51+
- Set as `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` env var
52+
- [ ] **Test webhook delivery** using Coinbase's test tools or [Postman collection](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/postman-files)
53+
54+
### Webhook Signature Format (new)
55+
- Header: `X-Hook0-Signature`
56+
- Format: `t=<timestamp>,h=<header_names>,v1=<hmac_sha256>`
57+
- Replay protection: Rejects events older than 5 minutes
58+
59+
### Old Format (still supported during transition)
60+
- Header: `X-CC-Webhook-Signature` — legacy Commerce format
61+
- The webhook endpoint auto-detects the format based on which signature header is present
62+
- Both formats are supported simultaneously via `_detect_webhook_format()`
63+
64+
---
65+
66+
## 4. API Authentication Changes
67+
68+
### Old (Commerce)
69+
```
70+
X-CC-Api-Key: <api_key>
71+
X-CC-Version: 2018-03-22
72+
```
73+
74+
### New (CDP / Business)
75+
```
76+
Authorization: Bearer <signed_JWT>
77+
Content-Type: application/json
78+
```
79+
80+
JWT generation is handled by the `cdp-sdk` Python package using:
81+
- `CDP_API_KEY_ID` (UUID) and `CDP_API_KEY_SECRET` (base64)
82+
- Supports both Ed25519 and ECDSA key types (SDK auto-detects)
83+
- See: https://docs.cdp.coinbase.com/api-reference/v2/authentication
84+
85+
Implementation: `src/services/coinbase_auth.py`
86+
87+
---
88+
89+
## 5. Payment Link API Endpoints
90+
91+
### Coinbase Business API (upstream)
92+
93+
Base URL: `https://business.coinbase.com`
94+
95+
| Operation | Method | Production Path | Sandbox Path |
96+
|---|---|---|---|
97+
| Create | `POST` | `/api/v1/payment-links` | `/sandbox/api/v1/payment-links` |
98+
| List | `GET` | `/api/v1/payment-links` | `/sandbox/api/v1/payment-links` |
99+
| Get | `GET` | `/api/v1/payment-links/{id}` | `/sandbox/api/v1/payment-links/{id}` |
100+
| Deactivate | `POST` | `/api/v1/payment-links/{id}/deactivate` | `/sandbox/api/v1/payment-links/{id}/deactivate` |
101+
102+
Controlled by `CDP_SANDBOX` env var. See [Sandbox docs](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/sandbox).
103+
104+
### Our API Endpoints
105+
106+
**User-facing** (under `/api/v1/billing/coinbase/`):
107+
108+
| Operation | Method | Path | Auth |
109+
|---|---|---|---|
110+
| Create Payment Link | `POST` | `/api/v1/billing/coinbase/payment-links` | User (Cognito JWT) |
111+
| Get Payment Link | `GET` | `/api/v1/billing/coinbase/payment-links/{id}` | User (Cognito JWT) |
112+
113+
Implementation: `src/api/v1/billing/coinbase.py`
114+
115+
**Admin-only** (under `/api/v1/billing/`, requires `X-Admin-Secret`):
116+
117+
| Operation | Method | Path | Auth |
118+
|---|---|---|---|
119+
| List Payment Links | `GET` | `/api/v1/billing/payment-links` | Admin (X-Admin-Secret) |
120+
| Deactivate Payment Link | `POST` | `/api/v1/billing/payment-links/{id}/deactivate` | Admin (X-Admin-Secret) |
121+
122+
Implementation: `src/api/v1/billing/admin.py`
123+
124+
### Key Differences from Commerce Charges
125+
126+
| Aspect | Commerce (old) | Payment Link (new) |
127+
|---|---|---|
128+
| ID format | UUID | 24-char hex |
129+
| URL field | `hosted_url` | `url` |
130+
| Amount | `pricing.local.amount` | `amount` (flat) |
131+
| Currency | `pricing.local.currency` | `currency` (flat) |
132+
| Status | `timeline` array | Single `status` field |
133+
| Statuses | NEW, SIGNED, PENDING, COMPLETED | ACTIVE, COMPLETED, EXPIRED, DEACTIVATED |
134+
| Currencies | BTC, ETH, USDC, DAI, USD | **USDC only** |
135+
| Network | Multiple | **Base only** |
136+
| Idempotency | Not required | `X-Idempotency-Key` header |
137+
138+
---
139+
140+
## 6. Currency Limitation — Important
141+
142+
The Payment Link API currently **only supports USDC on Base network**. If users were previously paying with BTC, ETH, or other currencies via Commerce, they will need to use USDC going forward.
143+
144+
Verify:
145+
- [ ] Frontend payment UI reflects USDC-only
146+
- [ ] Any documentation/help text referencing multi-currency is updated
147+
- [ ] Pricing is displayed in USDC (1:1 with USD)
148+
149+
---
150+
151+
## 7. Database / Data Migration
152+
153+
No schema changes required — the `credits_ledger` table already supports both formats via the `payment_metadata` JSONB column.
154+
155+
### Verify
156+
- [ ] Existing `payment_metadata` entries with `"type": "charge"` remain queryable
157+
- [ ] New entries will have `"type": "payment_link"`
158+
- [ ] `external_transaction_id` now stores 24-char hex IDs (was UUID charge codes)
159+
- [ ] No migration script needed for existing data
160+
161+
---
162+
163+
## 8. User Identification in Payment Flow
164+
165+
The `metadata` field on the payment link is used to pass the user identifier through the Coinbase payment flow. Coinbase treats metadata as an opaque key-value store and returns it verbatim in webhook payloads.
166+
167+
### Flow
168+
169+
1. **Create** (`POST /api/v1/billing/coinbase/payment-links`):
170+
The authenticated user's `cognito_user_id` is **automatically injected** into `metadata.user_id` server-side. The caller cannot override this — it is set from the JWT-authenticated session.
171+
172+
```json
173+
// Sent to Coinbase API:
174+
{
175+
"amount": "10.00",
176+
"currency": "USDC",
177+
"metadata": {
178+
"user_id": "<cognito_user_id>"
179+
}
180+
}
181+
```
182+
183+
2. **Webhook** (`POST /api/v1/webhooks/coinbase`):
184+
Coinbase sends the `metadata` back in the event payload. The webhook handler reads `metadata.user_id` to look up the user and credit their account.
185+
186+
```json
187+
// Received from Coinbase:
188+
{
189+
"id": "69163c762331ed43dc64a6ef",
190+
"eventType": "payment_link.payment.success",
191+
"amount": "10.00",
192+
"currency": "USDC",
193+
"metadata": {
194+
"user_id": "<cognito_user_id>"
195+
},
196+
...
197+
}
198+
```
199+
200+
3. **Credit**: The webhook service looks up the user by `cognito_user_id`, validates the amount, and creates a purchase ledger entry.
201+
202+
### Implementation
203+
- Injection: `src/api/v1/billing/coinbase.py``metadata["user_id"] = current_user.cognito_user_id`
204+
- Extraction: `src/services/coinbase_webhook_service.py``_get_user_from_metadata()`
205+
206+
---
207+
208+
## 9. Transition Architecture (Dual Webhook Support)
209+
210+
During the transition period, the system supports **both** Commerce and Payment Link webhooks simultaneously:
211+
212+
```
213+
POST /api/v1/webhooks/coinbase
214+
├── X-Hook0-Signature header present → Payment Link handler (new)
215+
└── X-CC-Webhook-Signature header present → Legacy Commerce handler (deprecated)
216+
```
217+
218+
### Files involved:
219+
- `src/api/v1/webhooks/coinbase.py` — Dual-format webhook endpoint with auto-detection
220+
- `src/services/coinbase_webhook_service.py` — Event handlers for both formats
221+
- `src/api/v1/billing/coinbase.py` — New Payment Link CRUD endpoints (user-facing)
222+
- `src/services/coinbase_payment_link_service.py` — Payment Link API client
223+
- `src/services/coinbase_auth.py` — CDP JWT auth for API calls
224+
- `src/core/config.py` — Both `COINBASE_COMMERCE_WEBHOOK_SECRET` and `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET`
225+
226+
---
227+
228+
## 10. Testing Checklist
229+
230+
### Sandbox Testing
231+
- [ ] Set `CDP_SANDBOX=true` and verify API calls go to `/sandbox/api/v1/payment-links`
232+
- [ ] Create a sandbox payment link and complete payment with [testnet USDC](https://portal.cdp.coinbase.com/products/faucet)
233+
- [ ] Register a sandbox webhook subscription (with `"sandbox": "true"` label)
234+
- [ ] Verify sandbox webhook events are received and processed correctly
235+
236+
### Pre-deployment
237+
- [ ] Verify JWT signing works with test CDP key
238+
- [ ] Create a test payment link via `POST /api/v1/billing/coinbase/payment-links`
239+
- [ ] Verify webhook signature verification with test payload (both formats)
240+
- [ ] Test idempotency (same webhook delivered twice)
241+
- [ ] Test expired/failed webhook handling
242+
- [ ] Verify legacy Commerce webhooks still work during transition
243+
244+
### Post-deployment
245+
- [ ] Create a real payment link and complete payment
246+
- [ ] Verify credits appear in user's balance
247+
- [ ] Verify payment metadata is stored correctly
248+
- [ ] Monitor logs for `coinbase_pl_webhook_*` events
249+
- [ ] Verify deactivation works
250+
251+
### Postman Collection
252+
Coinbase provides a [Postman collection](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/postman-files) for testing all Payment Link API endpoints.
253+
254+
---
255+
256+
## 11. Rollback Plan
257+
258+
If issues are discovered after deployment:
259+
260+
1. The webhook endpoint supports both `X-Hook0-Signature` and `X-CC-Webhook-Signature` — legacy Commerce continues to work without code changes
261+
2. CDP API key credentials coexist with Commerce API keys during transition
262+
3. Both `COINBASE_COMMERCE_WEBHOOK_SECRET` and `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` can be set simultaneously
263+
264+
---
265+
266+
## 12. Post-Migration Cleanup (after transition)
267+
268+
Once all Commerce charges have settled and new system is confirmed working:
269+
270+
- [ ] Remove `COINBASE_COMMERCE_WEBHOOK_SECRET` from config
271+
- [ ] Remove `_detect_webhook_format()` and `verify_legacy_commerce_signature()` from webhook handler
272+
- [ ] Remove `_handle_legacy_commerce_webhook()` from webhook handler
273+
- [ ] Remove legacy event types and `handle_charge_confirmed()` from webhook service
274+
- [ ] Update webhook endpoint to only accept `X-Hook0-Signature`
275+
276+
---
277+
278+
## 13. IP Allowlisting
279+
280+
- [ ] If using CDP API key IP allowlisting, ensure all API server IPs are added
281+
- [ ] If behind a load balancer, verify the outbound IP (NAT gateway) is allowlisted
282+
283+
---
284+
285+
## 14. Monitoring & Alerting
286+
287+
Ensure alerts are configured for these log event types:
288+
289+
**Payment Link (new):**
290+
- `coinbase_pl_webhook_not_configured` — Secret missing (critical)
291+
- `coinbase_pl_webhook_invalid_signature` — Signature mismatch (security)
292+
- `coinbase_pl_webhook_replay` — Replay attack attempt (security)
293+
- `coinbase_payment_link_error` — API call failures (operational)
294+
295+
**Legacy Commerce (transition period):**
296+
- `coinbase_legacy_webhook_not_configured` — Legacy secret missing
297+
- `coinbase_legacy_webhook_invalid_signature` — Legacy signature mismatch
298+
299+
---
300+
301+
## 15. Documentation References
302+
303+
- [Migration Overview](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/overview)
304+
- [API & Schema Mapping](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/api-schema-mapping)
305+
- [Payment Link API Reference](https://docs.cdp.coinbase.com/api-reference/business-api/rest-api/payment-links/introduction)
306+
- [CDP API Key Auth](https://docs.cdp.coinbase.com/api-reference/v2/authentication)
307+
- [Webhook Docs](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/webhooks)
308+
- [Sandbox Environment](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/sandbox)
309+
- [Testnet Faucet (Base Sepolia USDC)](https://portal.cdp.coinbase.com/products/faucet)
310+
- [Migration FAQ](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/faq)

env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,25 @@ STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
157157
# The CLI will display the signing secret (whsec_...)
158158
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_signing_secret
159159

160+
# =============================================================================
161+
# COINBASE BUSINESS CONFIGURATION
162+
# =============================================================================
163+
# CDP Secret API Key for Payment Link CRUD operations
164+
# Generate at: https://portal.cdp.coinbase.com/projects/api-keys (Secret API Keys tab)
165+
# Docs: https://docs.cdp.coinbase.com/api-reference/v2/authentication
166+
# Key ID: UUID from the CDP portal
167+
CDP_API_KEY_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
168+
# Key Secret: base64-encoded secret from the CDP portal
169+
CDP_API_KEY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
170+
# Set to true for sandbox (no real transactions, uses Base Sepolia testnet)
171+
# See: https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/sandbox
172+
CDP_SANDBOX=false
173+
174+
# Payment Link webhook signature verification secret
175+
# From metadata.secret when creating a webhook subscription
176+
# See: https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/webhooks
177+
COINBASE_PAYMENT_LINK_WEBHOOK_SECRET=your_webhook_secret
178+
160179
# =============================================================================
161180
# BUILDERS API CONFIGURATION (MOR Staking Data)
162181
# =============================================================================

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ boto3 = "^1.34.0"
2626
structlog = "^23.2.0"
2727
python-multipart = "0.0.20"
2828
stripe = "^11.0.0"
29-
web3 = "^6.0.0"
30-
eth-account = "^0.10.0"
29+
cdp-sdk = "^1.4.0"
30+
web3 = "^7.0.0"
31+
eth-account = "^0.13.0"
3132
redis = "^5.0.0"
32-
siwe = "^4.0.0"
33+
siwe = "^4.4.0"
3334

3435
[tool.poetry.group.dev.dependencies]
3536
pytest = "^7.4.0"

src/api/v1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .audio.index import router as audio_router
1010
from .billing.index import router as billing_router
1111
from .billing.admin import admin_router as billing_admin_router
12+
from .billing.coinbase import coinbase_billing_router
1213
from .webhooks.stripe import stripe_webhook_router
1314
from .webhooks.coinbase import coinbase_webhook_router
1415
from .wallet.index import router as wallet_router
@@ -37,6 +38,7 @@
3738
# Billing router
3839
billing = APIRouter()
3940
billing.include_router(billing_router)
41+
billing.include_router(coinbase_billing_router)
4042

4143
# Billing admin router (separate Swagger page at /admin/docs)
4244
billing_admin = APIRouter()

0 commit comments

Comments
 (0)