# Service MCP implementation

Service MCP implementation that agents can pay, with the kit and the template.

**Testnet demo setup**
This page covers the agent-pay testnet service integration. To use the hosted wallet and bundled shopping tools, add the single public URL in [Connect an AI agent](/en/docs/agents/). The service registration and hosting steps below are for developers adding their own service. Arrange registry access and testnet setup with [business@pouchi.app](mailto:business@pouchi.app) before deploying.

Services such as shops, subscriptions and finance build and host their own service MCP. A service quotes and builds **intents** only.
The user's wallet MCP (`pay_intent`) moves the money, and later charges happen only inside a mandate the user approved.

## Service template

The generator is on GitHub Packages. Before running `npm init`, obtain package access from [business@pouchi.app](mailto:business@pouchi.app), configure `@shard-lab:registry=https://npm.pkg.github.com` and `//npm.pkg.github.com/:_authToken=${NC_WAAS_PACKAGES_TOKEN}` in your user `~/.npmrc`, and set that environment variable to an authorized `read:packages` token. Keep the token out of files and chat. The project `.npmrc` is created only after the generator runs.

```sh
npm init @shard-lab/agentpay-service acme-news -- --name "Acme News"
cd acme-news
npm install
npm test        # runs against the test gateway: no chain, no network
```

- acme-news/
  - src/
    - service.mjs  tools, intents, merchant charges
  - test/
    - service.test.mjs
  - server.mjs  reads the environment and starts the server
  - Dockerfile
  - .env.example
  - .npmrc  GitHub Packages (token from the environment)

The template has four tools: `plans` (prices), `buy` (one-time purchase intent), `subscribe` (monthly subscription intent) and `cancel`.

## A one-time purchase intent

One run, exact item and price. The mandate pins the product and price with `eq` predicates.

```js title="src/service.mjs"
const it = kit.intent({
  action: "purchase",
  summary: "Single report 3,000 KRW",
  spend_okrw: 3000,
  calls: [approve(3000), pay(product)],            // calls to run now (approve + your pay function)
  mandate: {
    key: "acme-news:buy:1", label: "Single report",
    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 };              // the agent passes this URL to pay_intent
```

## Subscriptions: merchant-initiated charges

The first payment runs now through the intent; the service charges the next ones on its own schedule. Add `merchant_charges: true` and a `period` limit.

```js
mandate: {
  key: "acme-news:plan", label: "Acme News monthly",
  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 },     // at most once every 30 days
  permissions: [{ target: MERCHANT, signature: "pay(bytes32,uint256,uint256)",
    predicates: [{ arg: 1, op: "eq", uint: "2" }, { arg: 2, op: "lte", okrw: 9900 }] }]
}
```

1. When the user approves the mandate, the gateway sends a credential bound to that mandate to the service's `/agentpay/mandates`. The agent never sees it.

2. The kit confirms the credential with a signed read from the gateway, then calls `onMandate`. Store it encrypted, like a card token.

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

3. When a charge is due, call `kit.merchant.charge`. Every request is HMAC-signed, and a `charge_id` runs at most once, even on retry.

```js
const r = await kit.merchant.charge(credential, {
  charge_id: `${sub.id}-w${window}`,             // one per period window
  summary: "Monthly plan, month 2", spend_okrw: 9900,
  calls: [approve(9900), pay(product)]
});
// r.status === "paid", or r.error: PERIOD_LIMIT, OVER_LIMIT, OUTSIDE_MANDATE, MANDATE_REVOKED ...
```

4. When the user cancels, give the mandate back with `kit.merchant.release(credential)`. The user turns it off on chain with the wallet's `revoke_mandate`.

**The service owns the schedule**

The gateway has no scheduler. Charge from cron or a queue; deriving `charge_id` from the period window makes retries safe.

## Testing

The test gateway in `@shard-lab/agentpay-service-kit/testing` speaks the same HTTP contract as the real gateway: it approves intents, delivers credentials and accepts merchant charges. It uses no chain and no 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);   // as if the user approved
gw.advance(PERIOD);                           // next window
const [charged] = await svc.chargeDue();      // merchant charge
assert.equal(charged.status, "paid");
```

## Registration and deployment

1. Register the service with the tenant: `id`, `name`, `origin`, whether it may charge later, and its contract addresses.
2. Set `PUBLIC_URL` to exactly the registered origin. The wallet fetches intents only from there.
3. Run the Docker image.

Run this in the terminal where `NC_WAAS_PACKAGES_TOKEN` is exported. BuildKit passes `.npmrc` and the token as separate temporary secrets. Do not place the token in an image or build argument.

```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
```

Environment variables and registration are in [Distribution](/en/docs/distribution/).

## Rules at a glance

| Rule | Why |
|---|---|
| Mandate `key` starts with `<service id>:` | Mandates of different services never mix |
| `permissions` target only your own registered contracts | A finance service cannot get a shop contract's permission |
| `calls` to run now must fit the mandate | What runs is what was approved |
| Payment-token `approve` only to mandate targets, at most `approve_cap_okrw` | No leftover allowance to pull later |
| Kit-generated intents expire 15 minutes after creation and are pinned at first fetch | No swapping after approval |

Selected API declarations are in the [service kit reference](/en/docs/reference/service-kit/).
