세션키와 지출 정책
도입
AI agent나 반복 결제에 master wallet key를 직접 쓰면 안 된다. master key는 계정 전체를 통제하므로 prompt injection, backend compromise, rogue plugin 같은 상황에서 피해 범위가 너무 크다. Session key는 이 문제를 줄이기 위한 제한 권한이다.
좋은 session key policy는 "무엇을 할 수 있는가"보다 "무엇을 할 수 없는가"를 더 명확히 한다. token, chain, merchant, function selector, per-transaction limit, daily limit, valid window, revoke 상태를 모두 제한해야 한다.
학습 목표
- session key의 scope, limit, expiry를 설계한다.
- agent 자동 결제에서 사용자가 통제권을 잃지 않게 한다.
개념 설명
ScopeDrafted
session key의 scope, limit, expiry를 설계한다.
SpendChecked
서명 권한과 지출 한도가 분리되는가
CapNearLimit
paymaster 정책이 설명 가능한가
ScopeRevoked
세션키와 지출 정책 이해 점검
session key policy는 지갑, smart account module, backend policy engine, x402 client가 모두 읽을 수 있는 공통 언어여야 한다.
| 필드 | 예시 | 방어하는 위험 |
|---|---|---|
sessionKey | sub-key address | master key 노출 방지 |
validAfter/validUntil | 2026-05-15 00:00 to 24:00 | 장기 유출 피해 제한 |
chainId 또는 CAIP-2 | eip155:8453 | wrong-chain 결제 방지 |
token | USDC address | symbol spoofing 방지 |
merchantAllowlist | approved API payees | 악성 endpoint 결제 방지 |
methodAllowlist | callX402, payInvoice | 임의 contract call 방지 |
perTxLimit | 10 USDC | 단건 피해 제한 |
dailyLimit | 100 USDC | 누적 피해 제한 |
nonceDomain | app/payment domain | replay 범위 제한 |
revoked | true/false | 사용자 회수 반영 |
session key 상태도 명확해야 한다.
정책은 화면에만 있으면 안 된다. 실제 결제 요청이 들어올 때마다 wallet validation, backend policy engine, spend ledger가 같은 결론을 내야 한다. 한쪽만 통과하면 공격자가 가장 약한 지점을 우회 경로로 사용한다.
이 흐름에서 Policy Engine과 Smart Account는 서로를 대체하지 않는다. backend는 UX, rate limit, merchant policy를 빠르게 판단하고, wallet/module은 최종 onchain 권한 경계를 강제한다. 둘 중 하나라도 느슨하면 session key는 제한 권한이 아니라 자동 결제용 hot key가 된다.
코드로 확인하기
session key는 "자동 결제 허용"이 아니라 제한된 대리 서명자다. 코드에서는 session key가 할 수 있는 일과 할 수 없는 일을 먼저 표현해야 한다.
컨트랙트session key 정책 검증
struct SessionPolicy { address key; address token; address merchant; uint256 maxPerPayment; uint256 maxTotal; uint64 validUntil;}function validateSessionPayment( SessionPolicy memory policy, uint256 amount, address merchant, bytes4 selector) internal view { if (block.timestamp > policy.validUntil) revert SessionExpired(); if (merchant != policy.merchant) revert MerchantNotAllowed(); if (selector != Checkout.pay.selector) revert SelectorNotAllowed(selector); if (amount > policy.maxPerPayment) revert PaymentTooLarge(amount); if (spent[policy.key] + amount > policy.maxTotal) revert SessionBudgetExceeded();}session key 정책은 selector와 merchant를 함께 묶어야 한다. 금액 제한만 두면 같은 token으로 다른 contract에 결제하는 공격을 설명하기 어렵다.
클라이언트session scope를 사용자가 읽을 수 있게 만들기
type SessionScope = { merchantName: string; merchantAddress: `0x${string}`; tokenSymbol: "USDC"; maxPerPayment: string; totalBudget: string; expiresAt: string;};function summarizeSessionScope(scope: SessionScope) { return [ `${scope.merchantName}에게만 결제`, `한 번에 최대 ${scope.maxPerPayment} ${scope.tokenSymbol}`, `전체 예산 ${scope.totalBudget} ${scope.tokenSymbol}`, `${scope.expiresAt} 이후 자동 만료` ];}이 설명이 화면에 없으면 사용자는 agent 결제와 무제한 지출 권한을 구분하지 못한다. 코드와 UI 문구가 같은 정책을 말해야 한다.
강의 포인트
| 관점 | 확인할 질문 | 증거로 남길 것 |
|---|---|---|
| scope | token, chain, merchant, method가 좁게 제한됐는가 | policy schema |
| limit | perTx, daily, monthly cap과 reset rule이 있는가 | spend accounting table |
| expiry | validAfter/validUntil과 renewal UX가 있는가 | lifecycle diagram |
| revocation | 사용자가 즉시 끌 수 있고 UI에서 확인할 수 있는가 | revoke flow |
| incident | key 유출 시 남은 payload가 실패하는가 | compromise runbook |
운영 지표도 설계에 포함한다. session key는 "사용자가 허용했다"는 사실보다 "허용된 범위 안에서만 계속 쓰이고 있는가"를 관찰해야 한다.
| 지표 | 정상 범위 | 이상 신호 |
|---|---|---|
| active sessions per user | 사용자가 인지 가능한 소수 | 알 수 없는 session이 갑자기 증가 |
| rejected payments | 일부 policy mismatch | 특정 merchant에서 반복 거절 |
| cap usage velocity | 사용 패턴과 유사 | 짧은 시간에 daily cap 소진 |
| revocation latency | 즉시 또는 다음 block 내 반영 | revoke 후 payment가 계속 승인 |
| stale sessions | 만료 직후 자동 정리 | 오래된 session이 UI에 남음 |
실무 예시
agent subscription 결제를 위한 session key를 설계해보자. 사용자는 market data API에 하루 20 USDC까지 자동 결제를 허용한다.
| 정책 | 값 |
|---|---|
| token | Base USDC |
| merchant | market-data.example payee only |
| function | x402 payment 또는 payInvoice only |
| per request | 1 USDC |
| daily cap | 20 USDC |
| valid window | 24 hours |
| receipt | required |
| revoke | dashboard에서 즉시 revoke |
이 정책이 있으면 agent가 비싼 endpoint를 반복 호출하도록 유도되어도 per request cap과 daily cap이 피해를 제한한다.
사용자 화면에는 기술 세부사항을 모두 노출하지 않되, 취소 판단에 필요한 정보는 숨기지 않는다.
| UI 영역 | 보여줄 내용 | 이유 |
|---|---|---|
| 활성 세션 | merchant, token, 남은 한도, 만료 시각 | 사용자가 현재 권한을 이해한다 |
| 최근 결제 | request id, endpoint, 금액, receipt | 오남용 여부를 확인한다 |
| 취소 버튼 | 즉시 revoke, pending request 차단 | 피해 확산을 막는다 |
| 경고 상태 | cap 초과, 알 수 없는 merchant, 만료 임박 | 자동 결제 실패를 사용자가 해석한다 |
capstone에서는 이 UI 상태를 단순 mock으로만 두지 않는다. session key policy, spend ledger, revoke action, payment receipt가 같은 identifier를 공유해야 한다.
흔한 오해와 실패 시나리오
| 오해 | 실제로 확인할 것 |
|---|---|
| session key가 있으면 agent 결제가 안전하다고 본다. | session key scope가 넓으면 master key 대신 다른 hot key를 만든 것뿐이다. |
| expiry만 있으면 충분하다고 본다. | merchant, method, token, amount limit이 함께 있어야 한다. |
| revoke를 backend flag로만 처리한다. | smart account 또는 module validation에서도 revoked 상태가 반영되어야 한다. |
| dailyLimit만 있으면 prompt injection을 막는다고 본다. | per request cap과 endpoint allowlist가 같이 필요하다. |
| 사용자 dashboard를 나중에 만든다고 본다. | 사용자가 active session과 남은 한도를 볼 수 있어야 통제권을 잃지 않는다. |
실습 과제
- 세션키와 지출 정책 이해 점검: sessionKey, chainId, token, merchantAllowlist, methodAllowlist, perTxLimit, dailyLimit, validUntil, revoked를 포함한 policy schema를 작성한다.
- agent subscription 설계: 유료 API를 하루 20 USDC 한도로 쓰는 agent payment flow와 UI 상태를 만든다.
- 키 유출 대응 문서화: session key leak을 발견했을 때 revoke, pending payload invalidation, receipt audit, user notification 절차를 작성한다.
- cap reset 정책 작성: dailyLimit reset 기준, timezone, partial failed payment 처리 기준을 정한다.
완료 기준
- session key scope를 정의했다.
- 사용자 취소 UX를 만들었다.
- 키 유출 대응을 문서화했다.
근거 자료
- 세션키 지출정책: 05-계정추상화-에이전트결제/05-세션키-지출정책.md