# intent와 권한

서비스가 만드는 결제 intent와, 사용자가 한 번 승인하는 권한(mandate)의 형식.

**테스트넷 데모 구성**
이 문서는 agent-pay 테스트넷 서비스 연동을 설명합니다. 호스팅된 지갑과 기본 쇼핑 도구를 쓰려면 [AI 에이전트 연결](/docs/agents/)의 공개 URL 하나를 추가합니다. 아래 형식은 등록된 자기 서비스의 intent를 만드는 개발자용입니다. 배포 전에 [business@pouchi.app](mailto:business@pouchi.app)으로 레지스트리 접근 권한과 테스트넷 설정을 요청하세요.

**intent**는 서비스가 지갑에 "이것을 실행해 달라"고 내미는 JSON입니다. 지금 실행할 호출, 그 호출이 쓰는 금액, 그리고 그 호출이 속한 **권한(mandate)** 을 담습니다.
사용자는 권한을 한 번 승인합니다. 같은 권한 안의 다음 intent와 서비스 청구는 묻지 않고 실행됩니다. AP2의 intent와 mandate 구조를 따릅니다.

## 세 가지 종류

| `action` | 권한 | 예 |
|---|---|---|
| `purchase` | 1회. 상품과 가격을 `eq` 조건으로 고정. 나중 청구 없음 | 영화표 한 장 |
| `mandate` | 여러 번. 1회 한도, 전체 한도, 횟수, 기간, 주기 한도 | 구독, 예매 예약, 검색 이용료, 예금 운용 |
| `mandate.update` | 같은 권한의 한도, 횟수, 기간만 늘림. 범위는 그대로 | 구독 연장 |

## 예: 월 구독

```json
{
  "type": "agentpay.intent",
  "version": 1,
  "id": "int_8cVd0Hq3pTzR1b2N",
  "service": "acme-news",
  "action": "mandate",
  "summary": "월 구독 월 9,900원, 최대 12개월",
  "spend_okrw": 9900,
  "prefund_okrw": 9900,
  "expires_at": 1792051200,
  "calls": [
    { "target": "0x…token", "value": "0", "data": "0x095ea7b3…" },
    { "target": "0x…merchant", "value": "0", "data": "0x…" }
  ],
  "mandate": {
    "key": "acme-news:plan",
    "label": "Acme 뉴스 월 구독",
    "total_okrw": 118800,
    "per_okrw": 9900,
    "approve_cap_okrw": 9900,
    "calls": 12,
    "days": 360,
    "merchant_charges": true,
    "period": { "seconds": 2592000, "max_runs": 1 },
    "permissions": [{
      "target": "0x…merchant",
      "signature": "pay(bytes32,uint256,uint256)",
      "predicates": [{ "arg": 1, "op": "eq", "uint": "2" }, { "arg": 2, "op": "lte", "okrw": 9900 }]
    }]
  }
}
```

## intent 필드

| 필드 | 형식 | 규칙 |
|---|---|---|
| `type`, `version` | `"agentpay.intent"`, `1` | 고정 |
| `id` | `int_` + 8 ~ 64자 | 키트가 만듦 |
| `service` | 서비스 id | intent를 가져온 origin의 서비스와 같아야 함 |
| `summary` | 문자열 | 160자 이하. 승인 화면과 내역에 표시 |
| `spend_okrw` | 정수 | 지금 호출이 쓰는 금액. `per_okrw` 이하 |
| `prefund_okrw` | 정수, 선택 | 지갑이 이 권한을 위해 갖고 있어야 할 잔액 (충전 안내) |
| `expires_at` | unix 초 | 지금부터 1시간 이내. 키트 기본은 15분 |
| `calls` | 0 ~ 8개 | `{ target, value: "0", data }`. 모두 권한 안에 들어가야 함 |

금액(`*_okrw`)은 결제 토큰의 정수 단위입니다. 지갑이 `10 ** decimals`를 곱해 토큰 최소 단위로 변환합니다.

## 권한(mandate) 필드

| 필드 | 기본 | 규칙 |
|---|---|---|
| `key` | | `<서비스 id>:`로 시작. 같은 key의 살아 있는 권한은 재사용 |
| `label` | | 80자 이하. 승인 화면 제목 |
| `total_okrw`, `per_okrw` | | 전체 한도와 1회 한도. `per_okrw ≤ total_okrw` |
| `approve_cap_okrw` | | 결제 토큰 `approve` 1회 최대. `total_okrw` 이하 |
| `calls` | 100 | 실행 횟수 1 ~ 1000 |
| `days` | 30 | 유효 기간 1 ~ 365일 |
| `valid_after` | 승인 시각 | 시작 시각 (예매 예약). 최대 300일 뒤 |
| `merchant_charges` | `false` | 서비스가 나중에 청구할 수 있음. 레지스트리에서 허용된 서비스만 |
| `period` | 없음 | `{ seconds, max_runs?, max_spend_okrw? }`. `validAfter`부터 고정 길이 창 |
| `permissions` | | 1 ~ 16개. 등록된 자기 컨트랙트의 함수만 |

### 조건(predicate)

`permissions[].predicates`는 함수 인자 하나(32바이트 단어)에 거는 조건입니다. `permissions` 항목 하나에 최대 4개를 지정합니다.

| 필드 | 뜻 |
|---|---|
| `arg` | 0부터 세는 인자 위치 |
| `op` | `eq`(같음) 또는 `lte`(이하) |
| 값 하나 | `okrw`(토큰 단위, `10 ** decimals`를 곱해 변환), `uint`, `bytes32`, `address` |

`purchase`는 최소 하나의 `eq` 조건으로 상품을 고정해야 합니다.

## 권한 변경 (`mandate.update`)

```json
{ "action": "mandate.update", "spend_okrw": 0, "calls": [],
  "mandate": { "key": "acme-news:plan" },
  "update": { "add_total_okrw": 29700, "add_calls": 3, "add_days": 90 } }
```

변경할 때는 호출을 실행하지 않습니다. `add_total_okrw`, `add_calls`, `add_days`는 각 한도를 늘리고, 선택 항목 `per_okrw`는 1회 한도를 올립니다. 대상 컨트랙트, 함수, 인자 조건과 주기 한도는 바뀌지 않으며 현재 주기의 사용량도 초기화되지 않습니다. 다른 범위나 주기에는 새 권한이 필요합니다. 이용자는 `change_mandate`로 변경 내용을 확인하고 승인합니다.

## 서비스 청구 요청

사용자가 없는 결제입니다. 서비스가 게이트웨이에 `POST /merchant/charges`로 보냅니다.

```json
{ "mandate_id": "mnd_…", "charge_id": "sub_123-w1", "summary": "월 구독 2회차",
  "spend_okrw": 9900, "calls": [ … ] }
```

```http
Authorization: AgentPay-Mandate-HMAC id=mcr_…,ts=1792051200,sig=…
```

서명은 `METHOD\npath\nts\nsha256(body)`에 대한 HMAC-SHA256입니다. 요청 시각과 검증 시각의 차이는 앞뒤로 최대 300초(5분)까지 허용합니다. 키트의 `kit.merchant.charge`가 처리합니다.

## 검증 오류

| 코드 | 뜻 |
|---|---|
| `INTENT_SERVICE_MISMATCH` | intent의 `service`가 가져온 origin의 서비스와 다름 |
| `CONTRACT_NOT_REGISTERED` | 권한 대상이 그 서비스에 등록된 컨트랙트가 아님 |
| `INTENT_CALL_OUTSIDE_MANDATE` | 지금 실행할 호출이 권한 범위 밖 |
| `MERCHANT_CHARGES_NOT_ALLOWED` | 나중 청구가 허용되지 않은 서비스 |
| `INTENT_EXPIRED` | 만료됐거나 1시간보다 길게 잡음 |
| `OUTSIDE_MANDATE`, `OVER_LIMIT`, `PERIOD_LIMIT` | 청구가 범위, 한도, 주기 한도를 넘음 |
| `MANDATE_REVOKED`, `MANDATE_RELEASED`, `MANDATE_EXPIRED` | 권한이 꺼졌거나 끝남 |
| `CHARGE_ID_REUSED` | 같은 `charge_id`를 다른 내용으로 다시 보냄 |

서비스에서 intent를 내보내기 전에 키트의 `validateIntent`로 한 번 확인하면, 게이트웨이와 같은 규칙으로 미리 걸러집니다.
