GET /api/v1/admob/ssv
의도
AdMob 리워드 광고 완료 후 Google 서버가 직접 호출하는 SSV(Server-Side Verification) 콜백. ECDSA-SHA256 서명을 검증한 뒤 ad_bonus +1을 원자적으로 지급한다.
클라이언트가 직접 호출하는 엔드포인트가 아님 — Google AdMob 서버만 호출.
인증
- JWT 불필요 (Google이 직접 호출, 일반 rate limit 제외)
- ECDSA-SHA256 서명 검증 (
signature,key_id파라미터 사용) - 서명 공개키:
https://www.gstatic.com/admob/reward/verifier-keys.json(24h TTL 캐시)
입력
Google AdMob이 전달하는 쿼리 파라미터:
| 파라미터 | 설명 |
|---|---|
transaction_id | 광고 시청 고유 ID (멱등성 키) |
user_id | ServerSideVerificationOptions.userId로 앱이 설정한 값 (= dartbrief user ID) |
reward_amount | 보상 수량 (참고용, 현재 항상 1) |
reward_item | 보상 이름 (참고용) |
timestamp | Unix ms |
signature | base64url ECDSA 서명 |
key_id | 서명 검증에 쓸 공개키 ID |
응답
200 OK — 정상 지급 / 중복 모두 200
Google은 non-200만 재시도하므로, 비즈니스 로직 분기는 모두 200으로 반환.
400 Bad Request
transaction_id, key_id, signature 누락 시.
401 Unauthorized
서명 검증 실패 시. Google은 재시도하지 않으므로 실제 invalid signature에만 발생해야 함.
500 Internal Server Error
DB 오류 시 → Google이 제한 횟수 재시도. transaction_id가 아직 기록되지 않으므로 멱등 재시도 안전.
비즈니스 로직
sql
-- 단일 원자 CTE (멱등성 + 보너스 지급 동시 보장)
WITH record_tx AS (
INSERT INTO dartbrief.ad_reward_transactions (transaction_id, user_id, created_at)
VALUES ($1, $2, NOW())
ON CONFLICT (transaction_id) DO NOTHING
RETURNING 1
),
init_quota AS ( ... ),
add_bonus AS (
UPDATE dartbrief.user_analysis_quota
SET ad_bonus = CASE WHEN quota_month = $3 THEN ad_bonus + 1 ELSE 1 END
WHERE user_id = $2
AND EXISTS (SELECT 1 FROM record_tx)
RETURNING 1
)record_txINSERT ON CONFLICT DO NOTHING이 gate — 중복 transaction_id면 이후 CTE 전부 skip- 월 하드캡 없음 — SSV가 Google 서버에서 실제 광고 시청을 검증하므로 클라이언트 cap 불필요
Flutter 앱 연동
dart
// prod: ServerSideVerificationOptions.userId에 dartbrief user ID 설정
await ad.setServerSideOptions(
ServerSideVerificationOptions(userId: userId.toString()),
);
// → SSV 콜백 user_id 파라미터로 전달됨
// → 광고 완료 후 클라이언트 별도 claim 불필요dev flavor는 test ad unit 사용 → SSV 콜백 발생 안 함 → legacy /quota/ad-reward 경로 유지.
Rate limit
일반 IP rate limit 제외 — Google 공유 egress IP에서 발송되므로 버킷 공유 시 429 발생. SSV 엔드포인트는 ECDSA 서명 검증으로 보호.
운영 체크리스트
- AdMob 콘솔 → 각 광고 단위(
dartbrief-aos,dartbrief-ios) → Server-side verification → Callback URL 등록:https://api.dartbrief.com/api/v1/admob/ssv
관련 테이블
dartbrief.ad_reward_transactions— transaction_id UNIQUE, user_id FK → users(id) ON DELETE CASCADEdartbrief.user_analysis_quota— ad_bonus, quota_month