# Facilitate Engine

This page details the toani Facilitate engine modules, including MCP Gateway, bidirectional compliance engine, Google AP2 triple verification, Intent Mandate lifecycle, and transaction audit.

> For product overview, see [toani Facilitate Overview](/toani-facilitate/overview.md). For system architecture, see [toani Facilitate System Architecture](/toani-facilitate/how-it-works.md).

***

## Tech Stack

| Technology                    | Purpose                                                |
| ----------------------------- | ------------------------------------------------------ |
| **Node.js ESM**               | Gateway runtime                                        |
| **Next.js 14 App Router**     | Dashboard full-stack framework (API routes + frontend) |
| **Prisma 5 + MySQL**          | ORM and persistence layer                              |
| **Redis (ioredis)**           | KYT caching, session management                        |
| **viem**                      | Ethereum signature verification (EIP-191 / EIP-712)    |
| **@modelcontextprotocol/sdk** | MCP protocol SDK                                       |
| **@x402/mcp + @x402/evm**     | x402 payment protocol integration                      |
| **wagmi + RainbowKit**        | Frontend wallet connection                             |
| **zkMe Widget**               | KYC zero-knowledge identity verification               |
| **KYT risk scanning**         | Real-time wallet risk scanning                         |
| **zod**                       | Runtime parameter validation                           |

***

## MCP Gateway

### Transport Architecture

<figure><img src="/files/AGFG1TQjdKoQfLxml5oK" alt=""><figcaption></figcaption></figure>

Downstream connections use auto mode: try Streamable HTTP first, then automatically fall back to SSE. Retry count is configurable (1-10). Each downstream service maintains an independent x402 MCP client instance with `autoPayment: false`; payment decisions are entirely controlled by the Gateway.

### Two Tools

| Tool                    | Description                                                                                                                                                                                            |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list_gateway_services` | Enumerate all registered downstream services and their available tools; Agent discovers capabilities through this tool                                                                                 |
| `call_service_tool`     | Invoke downstream tool; automatically handle payment interception, compliance checks, and Google AP2 verification. Can accept `paymentPayload` (Payment Mandate signature) for phase-2 manual approval |

### GatewayEnvelope Response Structure

All `call_service_tool` responses are wrapped in a unified envelope:

```typescript
interface GatewayEnvelope {
  ok: boolean;
  stage: "tool_execution" | "compliance_check" | "assurance_check"
       | "payment_requested" | "payment_required";
  code: string;
  message: string;
  traceId: string;
  timestamp: string;  // ISO 8601
  detail: {
    serviceId: string;
    toolName: string;
    paymentMade: boolean;
    paymentResponse: object | null;
    complianceDecision: ComplianceDecision | null;
    assuranceResult: AssuranceResult | null;
    payeeComplianceDecision: ComplianceDecision | null;
    paymentContext: object | null;
    downstreamContent: unknown;
  };
}
```

The `ok` field indicates overall success/failure. `stage` tells the Agent where failure occurred. `code` and `message` provide machine-readable and human-readable error descriptions. This structured envelope lets AI Agents programmatically interpret results.

### Environment Variables

| Variable                     | Description                                         | Required                    |
| ---------------------------- | --------------------------------------------------- | --------------------------- |
| `EVM_PRIVATE_KEY`            | Gateway wallet private key (must start with `0x`)   | Yes                         |
| `DOWNSTREAM_MCP_URLS`        | Downstream MCP service URLs, comma-separated        | Yes                         |
| `COMPLIANCE_BASE_URL`        | Dashboard API base address                          | Yes (fail-close if missing) |
| `COMPLIANCE_API_KEY`         | Dashboard API Key                                   | Yes (fail-close if missing) |
| `COMPLIANCE_CHAIN`           | Chain identifier for compliance queries             | No (default `"base"`)       |
| `DOWNSTREAM_MCP_TRANSPORT`   | Transport mode: `auto`, `sse`, or `streamable-http` | No (default `auto`)         |
| `DOWNSTREAM_CONNECT_RETRIES` | Connection retry count (1-10)                       | No (default `3`)            |

***

## Bidirectional Compliance Engine

### Parallel Execution

KYC and KYT checks are initiated in parallel using `Promise.all`, reducing latency from serial (KYC + KYT) to `max(KYC, KYT)`:

<figure><img src="/files/jfDXWZjbNx91kSwobA2y" alt=""><figcaption></figcaption></figure>

### Fail-close Strategy

When critical configuration is missing, the compliance module adopts the strictest approach: reject all payments. There is no degradation mode or silent skip.

### ComplianceDecision Structure

```typescript
interface ComplianceDecision {
  serviceId: string;
  toolName: string;
  counterparty: string;
  passed: boolean;
  reasonCode: string;
  message: string;
  checkedAt: string;
  kycStatus: string;
  kytDecision: string;
  kytRiskLevel: string;
  kytRiskScore: number;
}
```

This structure is attached to the `complianceDecision` field of `GatewayEnvelope`, available to Agent and audit systems.

***

## Google AP2 Triple Verification

The Google AP2 Assurance module runs in parallel with compliance checks using short-circuit design: if any step fails, reject immediately without proceeding to later verifications.

### CartMandate Extraction

Extract the `cartMandate` object from the x402 payment requirement returned by downstream service:

Primary path: `paymentRequired.accepts[0].extra.cartMandate` Fallback path: `paymentRequired.accepts[0].extra` top level

Required fields:

| Field                | Description                                   |
| -------------------- | --------------------------------------------- |
| `merchant_id`        | Merchant's unique identifier on the platform  |
| `merchant_address`   | Merchant wallet address                       |
| `merchant_signature` | Merchant's EIP-191 signature over CartMandate |

If `cartMandate` is missing, Google AP2 verification is skipped (not rejected) for backward compatibility with downstream services not yet using Google AP2 protocol.

### Three-step Verification Details

<figure><img src="/files/wO2GSz5H8YHP5mC0YMNz" alt=""><figcaption></figcaption></figure>

**Step 1: Merchant Signature Verification**

1. Call `POST /api/accounts/public-key` to get merchant's registered public key
2. Derive address from public key: `publicKeyToAddress(publicKey)`
3. Build signature message: `JSON.stringify({ merchant_id, merchant_address, total_amount, currency, pay_to })` (fixed field order, `merchant_signature` excluded)
4. Verify: `verifyMessage(address, message, merchant_signature)`

**Step 2: Payer VC Signature Verification**

1. Call `POST /api/mandates/intent/vc` to get payer's current active Mandate VC
2. Parse DID format address from `vcJson.proof.verificationMethod` (`did:ethr:eip155:{chainId}:{address}#controller`)
3. Rebuild EIP-712 message from `credentialSubject`
4. Call `verifyTypedData` to verify signature (Domain: `"Agentry Intent Mandate"`, Version: `"1"`)

**Step 3: Budget Rule Execution**

Pure local numeric calculation, no network requests needed. Parse `cartMandate.total_amount`, `mandate.perTxLimit`, `remainingBudget` as floats and compare.

### Error Codes

| Error Code                   | Trigger Condition                                                     |
| ---------------------------- | --------------------------------------------------------------------- |
| `MERCHANT_SIGNATURE_INVALID` | Merchant EIP-191 signature verification failed                        |
| `VC_PROOF_INVALID`           | Payer EIP-712 signature verification failed                           |
| `MANDATE_NOT_FOUND`          | Payer's active Mandate not found                                      |
| `MANDATE_EXPIRED`            | Mandate has expired                                                   |
| `PER_TX_LIMIT_EXCEEDED`      | Amount exceeds perTxLimit (can escalate to manual approval)           |
| `TOTAL_BUDGET_EXCEEDED`      | Amount exceeds remaining budget (can escalate to manual approval)     |
| `ASSURANCE_API_ERROR`        | Dashboard API call failed                                             |
| `CART_MANDATE_MISSING`       | CartMandate missing (non-Google AP2 downstream; verification skipped) |

`PER_TX_LIMIT_EXCEEDED` and `TOTAL_BUDGET_EXCEEDED` are not hard rejections. They trigger the Gateway to return `WALLET_APPROVAL_REQUIRED` challenge, returning payment decision authority to the Owner.

### AssuranceResult Structure

```typescript
interface AssuranceResult {
  passed: boolean;
  errorCode?: string;
  errorMessage?: string;
  checkedAt: string;
  mandateId?: string;
}
```

***

## Intent Mandate Lifecycle

Intent Mandate is the core credential through which an Owner authorizes an AI Agent to spend autonomously. It defines budget caps and per-tx limits, becomes effective after wallet signature, and is gradually consumed as it is used until exhausted or expired.

### Data Model

Table: `agentry_dashboard_intent_mandates`:

| Field              | Type           | Description                                                |
| ------------------ | -------------- | ---------------------------------------------------------- |
| `id`               | String         | Primary key (cuid)                                         |
| `accountId`        | String         | Owning account                                             |
| `title`            | String         | Descriptive name                                           |
| `perTxLimit`       | Decimal(36,18) | Per-transaction limit                                      |
| `totalBudget`      | Decimal(36,18) | Total budget                                               |
| `usedAmount`       | Decimal(36,18) | Amount consumed                                            |
| `transactionCount` | Int            | Number of transactions used                                |
| `lastUsedAt`       | DateTime?      | Last usage timestamp                                       |
| `signature`        | Text           | EIP-712 signature                                          |
| `vcJson`           | MediumText?    | W3C verifiable credential JSON                             |
| `status`           | String         | `pending` / `active` / `exhausted` / `expired` / `revoked` |
| `expiresAt`        | DateTime       | Expiration time                                            |

### State Machine

<figure><img src="/files/gTFoG0MP1zLXidPXUyj5" alt=""><figcaption></figcaption></figure>

Key rules:

* Each account has exactly one active Mandate at a time
* Signing a new Mandate automatically revokes the previous active one
* State transitions are irreversible (revoked, exhausted, expired cannot return to active)

### Creation Flow

**Step 1: Create Record**

```
POST /api/mandates/intent/create
```

Authentication: Session (JWT Cookie or Bearer Token)

Request body:

```json
{
  "title": "Daily tool budget",
  "perTxLimit": "10.00",
  "totalBudget": "100.00",
  "validDays": 30
}
```

Server creates database record (`status: pending`), returns `mandateId` and unsigned VC.

**Step 2: Frontend Wallet Signature**

Frontend calls `signMandate(vc, walletClient, chainId)` to initiate EIP-712 `signTypedData`.

**Step 3: Submit Signature**

```
POST /api/mandates/intent/sign
```

Server processing:

1. `recoverTypedDataAddress` recovers signature address, verifies it matches current user wallet
2. `buildSignedVc` constructs complete VC with signature proof
3. Revokes other active Mandates for current account
4. Updates current Mandate: `status` becomes `active`, stores `vcJson` and `signature`

### VC Structure

Intent Mandate VC follows W3C Verifiable Credential spec:

```json
{
  "@context": ["https://www.w3.org/2018/credentials/v1"],
  "type": ["VerifiableCredential", "AP2IntentMandate"],
  "issuer": "did:ethr:eip155:<chainId>:<payerAddress>",
  "issuanceDate": "2026-03-30T00:00:00.000Z",
  "credentialSubject": {
    "mandateId": "cm5abc...",
    "accountId": "acc_xxx",
    "payer": {
      "address": "0xPayerAddress",
      "agentryId": "acc_xxx"
    },
    "budgetConstraints": {
      "totalBudget": "100.00",
      "perTransactionLimit": "10.00",
      "currency": "USDC"
    },
    "title": "Daily tool budget"
  },
  "proof": {
    "type": "EthereumEip712Signature2021",
    "created": "2026-03-30T00:00:00.000Z",
    "verificationMethod": "did:ethr:eip155:<chainId>:<payerAddress>#controller",
    "proofPurpose": "assertionMethod",
    "eip712": {
      "domain": { "name": "Agentry Intent Mandate", "version": "1", "chainId": "<chainId>" },
      "types": { "IntentMandate": "..." },
      "primaryType": "IntentMandate"
    },
    "proofValue": "0xSignatureValue..."
  }
}
```

### EIP-712 Signature Parameters

| Parameter          | Value                      |
| ------------------ | -------------------------- |
| **Domain.name**    | `"Agentry Intent Mandate"` |
| **Domain.version** | `"1"`                      |
| **Domain.chainId** | Base network Chain ID      |
| **PrimaryType**    | `"IntentMandate"`          |

Types definition:

```typescript
{
  IntentMandate: [
    { name: "id", type: "string" },
    { name: "accountId", type: "string" },
    { name: "payerAddress", type: "address" },
    { name: "perTxLimit", type: "string" },
    { name: "totalBudget", type: "string" },
    { name: "expiresAt", type: "string" },
  ];
}
```

### Management API

All endpoints require session authentication (JWT Cookie or Bearer Token):

| Endpoint                                 | Description                                     |
| ---------------------------------------- | ----------------------------------------------- |
| `POST /api/mandates/intent/list`         | List all Mandates for current account           |
| `POST /api/mandates/intent/update`       | Update title and limits (requires re-signature) |
| `POST /api/mandates/intent/adjust`       | Adjust budget amount                            |
| `POST /api/mandates/intent/revoke`       | Revoke active Mandate                           |
| `POST /api/mandates/intent/renew`        | Renew expired Mandate (requires re-signature)   |
| `POST /api/mandates/intent/delete`       | Delete Mandate record                           |
| `POST /api/mandates/intent/prepare-sign` | Prepare unsigned VC for re-signature scenarios  |

Editing limits or renewing the Mandate invalidates the original signature, requiring user to re-sign with their wallet.

### Usage Tracking

After transaction completion, the system updates Mandate consumption state via `updateMandateUsage`:

<figure><img src="/files/orytRDSpPLGpxizHnGO0" alt=""><figcaption></figcaption></figure>

Trigger: `POST /api/transactions` endpoint calls this when `serviceResult === "pass"`. Only successful transactions consume budget; rejected or errored calls do not deduct funds.

***

## Transaction Records and Audit

Every transaction processed by toani Facilitate, whether successful, rejected, or errored, is recorded to the transaction log.

### TransactionLog Data Model

Table: `agentry_dashboard_transaction_logs`:

| Field               | Type           | Description                         |
| ------------------- | -------------- | ----------------------------------- |
| `id`                | Int            | Auto-increment primary key          |
| `serviceName`       | VarChar(64)    | Downstream service name             |
| `serviceResult`     | VarChar(20)    | `pass` / `reject` / `error`         |
| `chain`             | VarChar(20)    | Chain identifier                    |
| `txHash`            | VarChar(128)?  | On-chain transaction hash           |
| `amount`            | Decimal(18,2)? | USDC amount                         |
| `tokenSymbol`       | VarChar(20)?   | Token symbol                        |
| `payer`             | VarChar(42)?   | Payer address                       |
| `payee`             | VarChar(42)?   | Payee address                       |
| `payerKycResult`    | TinyInt?       | 1=pass, 0=fail                      |
| `payerKytResult`    | TinyInt?       | 1=pass, 0=fail                      |
| `payerKytRiskLevel` | TinyInt?       | 0=low, 1=moderate, 2=high, 3=severe |
| `payeeKycResult`    | TinyInt?       | 1=pass, 0=fail                      |
| `payeeKytResult`    | TinyInt?       | 1=pass, 0=fail                      |
| `payeeKytRiskLevel` | TinyInt?       | 0=low, 1=moderate, 2=high, 3=severe |
| `metadata`          | Json?          | Extended metadata                   |
| `intentMandateId`   | String?        | Associated Mandate                  |
| `createdAt`         | DateTime       | Creation timestamp                  |

### Fire-and-forget Reporting

<figure><img src="/files/TWOE7aXIBURzlPGZCYz3" alt=""><figcaption></figcaption></figure>

Key design: transaction reporting uses fire-and-forget mode. The system sends reports asynchronously without waiting for response and does not block the Gateway from returning results to the Agent. If reporting fails, the transaction itself is unaffected.

### Scope Query

| Scope     | Filter Logic                                                      |
| --------- | ----------------------------------------------------------------- |
| `myPayer` | Show only transactions where current user is payer                |
| `myPayee` | Show only transactions where current user is payee                |
| `myAll`   | Combine both: return any transaction current user participated in |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.toani.ai/toani-facilitate/facilitate-engine.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
