# 서비스 MCP 구현

키트와 템플릿으로 에이전트가 결제할 수 있는 서비스 MCP를 만듭니다.

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

서비스 MCP는 쇼핑, 구독, 금융 같은 서비스가 직접 만들고 호스팅합니다. 서비스는 견적을 내고 **intent**만 만듭니다.
돈은 사용자의 지갑 MCP(`pay_intent`)가 움직이고, 나중 청구는 사용자가 승인한 권한 안에서만 됩니다.

## 템플릿으로 시작

생성기는 GitHub Packages에 있습니다. `npm init` 전에 [business@pouchi.app](mailto:business@pouchi.app)으로 패키지 접근 권한을 요청하세요. 사용자 `~/.npmrc`에 `@shard-lab:registry=https://npm.pkg.github.com`과 `//npm.pkg.github.com/:_authToken=${NC_WAAS_PACKAGES_TOKEN}`을 설정하고, 환경 변수에 접근 권한이 있는 `read:packages` 토큰을 넣습니다. 토큰 값은 파일이나 채팅에 쓰지 않습니다. 프로젝트의 `.npmrc`는 생성기를 실행한 뒤에 만들어집니다.

```sh
npm init @shard-lab/agentpay-service acme-news -- --name "Acme 뉴스"
cd acme-news
npm install
npm test        # 테스트 게이트웨이로 실행. 체인, 네트워크 없음
```

- acme-news/
  - src/
    - service.mjs  도구, intent, 서비스 청구
  - test/
    - service.test.mjs
  - server.mjs  환경 변수를 읽어 실행
  - Dockerfile
  - .env.example
  - .npmrc  GitHub Packages (토큰은 환경 변수)

템플릿에는 도구 네 개가 있습니다: `plans`(가격), `buy`(단건 구매 intent), `subscribe`(월 구독 intent), `cancel`(해지).

## 단건 구매 intent

한 번, 정확한 상품과 가격입니다. 권한(mandate)의 조건이 `eq`로 상품과 가격을 고정합니다.

```js title="src/service.mjs"
const it = kit.intent({
  action: "purchase",
  summary: "단건 리포트 3,000원",
  spend_okrw: 3000,
  calls: [approve(3000), pay(product)],            // 지금 실행할 호출 (approve + 결제 함수)
  mandate: {
    key: "acme-news:buy:1", label: "단건 리포트",
    total_okrw: 3000, per_okrw: 3000, approve_cap_okrw: 3000, calls: 1, days: 1,
    permissions: [{
      target: MERCHANT, signature: "pay(bytes32,uint256,uint256)",
      predicates: [{ arg: 1, op: "eq", uint: "1" }, { arg: 2, op: "eq", okrw: 3000 }]
    }]
  }
});
return { intent_url: it.intent_url };              // 에이전트는 이 URL을 pay_intent에 넘깁니다
```

## 구독: 서비스 청구

첫 결제는 intent로 지금 실행하고, 다음 결제는 서비스가 일정에 맞춰 청구합니다. `merchant_charges: true`와 주기 한도(`period`)를 넣습니다.

```js
mandate: {
  key: "acme-news:plan", label: "Acme 뉴스 월 구독",
  total_okrw: 9900 * 12, per_okrw: 9900, approve_cap_okrw: 9900, calls: 12, days: 360,
  merchant_charges: true,
  period: { seconds: 30 * 86400, max_runs: 1 },     // 30일마다 최대 1회
  permissions: [{ target: MERCHANT, signature: "pay(bytes32,uint256,uint256)",
    predicates: [{ arg: 1, op: "eq", uint: "2" }, { arg: 2, op: "lte", okrw: 9900 }] }]
}
```

1. 사용자가 권한을 승인하면, 게이트웨이가 그 권한 전용 자격 증명을 서비스의 `/agentpay/mandates`로 보냅니다. 에이전트는 이 값을 보지 못합니다.

2. 키트가 게이트웨이에 서명된 조회로 자격 증명을 확인하고 `onMandate`를 부릅니다. 서비스는 이것을 카드 토큰처럼 암호화해 저장합니다.

```js
async onMandate({ credential, intentId }) {
  await db.subscriptions.attachCredential(intentId, encrypt(credential));
}
```

3. 청구할 때가 되면 `kit.merchant.charge`를 부릅니다. 요청마다 HMAC 서명이 붙고, 같은 `charge_id`는 재시도해도 한 번만 실행됩니다.

```js
const r = await kit.merchant.charge(credential, {
  charge_id: `${sub.id}-w${window}`,             // 주기 창마다 하나
  summary: "월 구독 2회차", spend_okrw: 9900,
  calls: [approve(9900), pay(product)]
});
// r.status === "paid" 또는 r.error: PERIOD_LIMIT, OVER_LIMIT, OUTSIDE_MANDATE, MANDATE_REVOKED ...
```

4. 사용자가 해지하면 `kit.merchant.release(credential)`로 청구 권한을 반납합니다. 체인 권한은 사용자가 지갑의 `revoke_mandate`로 끕니다.

**서비스의 청구 일정**

게이트웨이에는 스케줄러가 없습니다. cron이나 큐로 청구하고, `charge_id`를 주기 창에서 만들면 재시도와 중복 실행이 안전합니다.

## 테스트

`@shard-lab/agentpay-service-kit/testing`의 테스트 게이트웨이는 실제 게이트웨이와 같은 HTTP 계약으로 승인, 자격 증명 전달, 서비스 청구를 흉내 냅니다. 체인과 Pouchi는 쓰지 않습니다.

```js title="test/service.test.mjs"
const gw = await startTestGateway({ service: { id: "acme-news", origin, contracts: [MERCHANT], merchantCharges: true }, asset: TOKEN });
const out = await mcp.call("subscribe", { months: 3 });
const r = await gw.approve(out.intent_url);   // 사용자가 승인한 것처럼
gw.advance(PERIOD);                           // 다음 주기로
const [charged] = await svc.chargeDue();      // 서비스 청구
assert.equal(charged.status, "paid");
```

## 등록과 배포

1. 앱 운영자에게 서비스를 등록합니다: `id`, `name`, `origin`, 나중 청구 허용 여부, 컨트랙트 주소.
2. `PUBLIC_URL`을 등록한 origin과 똑같이 맞춥니다. 지갑은 그 origin에서만 intent를 가져옵니다.
3. Docker 이미지로 띄웁니다.

앞서 설정한 `NC_WAAS_PACKAGES_TOKEN`을 빌드 터미널에 export한 상태로 실행합니다. BuildKit이 `.npmrc`와 토큰을 각각 임시 secret으로 전달하며, 토큰을 이미지나 빌드 인자에 넣지 않습니다.

```sh
docker build --secret id=npmrc,src=$HOME/.npmrc \
  --secret id=packages_token,env=NC_WAAS_PACKAGES_TOKEN -t acme-news-mcp .
docker run -d -p 8080:8080 --env-file .env acme-news-mcp
```

환경 변수와 등록 절차는 [배포와 실행](/docs/distribution/)에 있습니다.

## 규칙 요약

| 규칙 | 이유 |
|---|---|
| 권한 `key`는 `<서비스 id>:`로 시작 | 다른 서비스의 권한과 섞이지 않게 |
| `permissions`의 대상은 등록된 자기 컨트랙트만 | 금융 서비스가 쇼핑 컨트랙트 권한을 받지 못하게 |
| 지금 실행할 `calls`는 권한 안에 들어가야 함 | 승인한 것과 실행하는 것이 같도록 |
| 결제 토큰 `approve`는 권한 대상에게, `approve_cap_okrw` 이하로만 | 남는 허용량으로 나중에 빼 가지 못하게 |
| 키트에서 만든 intent는 생성 후 15분 뒤 만료, 처음 가져온 내용으로 고정 | 승인 뒤 바꿔치기 방지 |

주요 API는 [서비스 키트 레퍼런스](/docs/reference/service-kit/)에 있습니다.
