|
| 1 | +# Copyright (c) Microsoft. All rights reserved. |
| 2 | + |
| 3 | +"""Nory x402 Payment Plugin for Semantic Kernel. |
| 4 | +
|
| 5 | +A plugin that enables AI agents to make payments using the x402 HTTP protocol. |
| 6 | +Supports Solana and 7 EVM chains with sub-400ms settlement. |
| 7 | +""" |
| 8 | + |
| 9 | +from typing import Annotated |
| 10 | + |
| 11 | +import aiohttp |
| 12 | + |
| 13 | +from semantic_kernel.functions.kernel_function_decorator import kernel_function |
| 14 | +from semantic_kernel.kernel_pydantic import KernelBaseModel |
| 15 | + |
| 16 | +NORY_API_BASE = "https://noryx402.com" |
| 17 | + |
| 18 | + |
| 19 | +class NoryX402Plugin(KernelBaseModel): |
| 20 | + """A plugin that provides x402 payment functionality via Nory. |
| 21 | +
|
| 22 | + Enables AI agents to make payments using the x402 HTTP payment protocol. |
| 23 | + Supports Solana and 7 EVM chains (Base, Polygon, Arbitrum, Optimism, |
| 24 | + Avalanche, Sei, IoTeX) with sub-400ms settlement times. |
| 25 | +
|
| 26 | + Usage: |
| 27 | + kernel.add_plugin(NoryX402Plugin(), "nory") |
| 28 | +
|
| 29 | + # With API key for authenticated endpoints: |
| 30 | + kernel.add_plugin(NoryX402Plugin(api_key="your-api-key"), "nory") |
| 31 | +
|
| 32 | + Examples: |
| 33 | + {{nory.get_payment_requirements "/api/premium/data" "0.10"}} |
| 34 | + {{nory.settle_payment $payload}} |
| 35 | + {{nory.health_check}} |
| 36 | + """ |
| 37 | + |
| 38 | + api_key: str | None = None |
| 39 | + """Nory API key for authenticated endpoints. Optional for public endpoints.""" |
| 40 | + |
| 41 | + def _get_headers(self, with_json: bool = False) -> dict[str, str]: |
| 42 | + """Get request headers with optional auth.""" |
| 43 | + headers: dict[str, str] = {} |
| 44 | + if with_json: |
| 45 | + headers["Content-Type"] = "application/json" |
| 46 | + if self.api_key: |
| 47 | + headers["Authorization"] = f"Bearer {self.api_key}" |
| 48 | + return headers |
| 49 | + |
| 50 | + @kernel_function( |
| 51 | + name="get_payment_requirements", |
| 52 | + description="Get x402 payment requirements for accessing a paid resource. Use this when you encounter an HTTP 402 Payment Required response.", |
| 53 | + ) |
| 54 | + async def get_payment_requirements( |
| 55 | + self, |
| 56 | + resource: Annotated[str, "The resource path requiring payment (e.g., /api/premium/data)"], |
| 57 | + amount: Annotated[str, "Amount in human-readable format (e.g., '0.10' for $0.10 USDC)"], |
| 58 | + network: Annotated[ |
| 59 | + str | None, |
| 60 | + "Preferred blockchain network (solana-mainnet, base-mainnet, polygon-mainnet, etc.)", |
| 61 | + ] = None, |
| 62 | + ) -> str: |
| 63 | + """Get payment requirements for a resource. |
| 64 | +
|
| 65 | + Returns amount, supported networks, and wallet address needed to make a payment. |
| 66 | +
|
| 67 | + Args: |
| 68 | + resource: The resource path requiring payment. |
| 69 | + amount: Amount in human-readable format. |
| 70 | + network: Preferred blockchain network (optional). |
| 71 | +
|
| 72 | + Returns: |
| 73 | + JSON string with payment requirements. |
| 74 | + """ |
| 75 | + params = {"resource": resource, "amount": amount} |
| 76 | + if network: |
| 77 | + params["network"] = network |
| 78 | + |
| 79 | + async with aiohttp.ClientSession() as session: |
| 80 | + async with session.get( |
| 81 | + f"{NORY_API_BASE}/api/x402/requirements", |
| 82 | + params=params, |
| 83 | + headers=self._get_headers(), |
| 84 | + raise_for_status=True, |
| 85 | + ) as response: |
| 86 | + return await response.text() |
| 87 | + |
| 88 | + @kernel_function( |
| 89 | + name="verify_payment", |
| 90 | + description="Verify a signed payment transaction before settlement. Use this to validate that a payment is correct before submitting to blockchain.", |
| 91 | + ) |
| 92 | + async def verify_payment( |
| 93 | + self, |
| 94 | + payload: Annotated[str, "Base64-encoded payment payload containing signed transaction"], |
| 95 | + ) -> str: |
| 96 | + """Verify a signed payment transaction. |
| 97 | +
|
| 98 | + Validates that a payment transaction is correct before submitting |
| 99 | + it to the blockchain. |
| 100 | +
|
| 101 | + Args: |
| 102 | + payload: Base64-encoded payment payload. |
| 103 | +
|
| 104 | + Returns: |
| 105 | + JSON string with verification result including validity and payer info. |
| 106 | + """ |
| 107 | + async with aiohttp.ClientSession() as session: |
| 108 | + async with session.post( |
| 109 | + f"{NORY_API_BASE}/api/x402/verify", |
| 110 | + json={"payload": payload}, |
| 111 | + headers=self._get_headers(with_json=True), |
| 112 | + raise_for_status=True, |
| 113 | + ) as response: |
| 114 | + return await response.text() |
| 115 | + |
| 116 | + @kernel_function( |
| 117 | + name="settle_payment", |
| 118 | + description="Settle a payment on-chain. Submits a verified payment to the blockchain with ~400ms settlement time.", |
| 119 | + ) |
| 120 | + async def settle_payment( |
| 121 | + self, |
| 122 | + payload: Annotated[str, "Base64-encoded payment payload"], |
| 123 | + ) -> str: |
| 124 | + """Settle a payment on-chain. |
| 125 | +
|
| 126 | + Submits a verified payment transaction to the blockchain. |
| 127 | + Settlement typically completes in under 400ms. |
| 128 | +
|
| 129 | + Args: |
| 130 | + payload: Base64-encoded payment payload. |
| 131 | +
|
| 132 | + Returns: |
| 133 | + JSON string with settlement result including transaction ID. |
| 134 | + """ |
| 135 | + async with aiohttp.ClientSession() as session: |
| 136 | + async with session.post( |
| 137 | + f"{NORY_API_BASE}/api/x402/settle", |
| 138 | + json={"payload": payload}, |
| 139 | + headers=self._get_headers(with_json=True), |
| 140 | + raise_for_status=True, |
| 141 | + ) as response: |
| 142 | + return await response.text() |
| 143 | + |
| 144 | + @kernel_function( |
| 145 | + name="lookup_transaction", |
| 146 | + description="Look up the status of a previously submitted payment transaction.", |
| 147 | + ) |
| 148 | + async def lookup_transaction( |
| 149 | + self, |
| 150 | + transaction_id: Annotated[str, "Transaction ID or signature"], |
| 151 | + network: Annotated[str, "Network where the transaction was submitted"], |
| 152 | + ) -> str: |
| 153 | + """Look up transaction status. |
| 154 | +
|
| 155 | + Check the status of a previously submitted payment. |
| 156 | +
|
| 157 | + Args: |
| 158 | + transaction_id: Transaction ID or signature. |
| 159 | + network: Network where the transaction was submitted. |
| 160 | +
|
| 161 | + Returns: |
| 162 | + JSON string with transaction details including status and confirmations. |
| 163 | + """ |
| 164 | + async with aiohttp.ClientSession() as session: |
| 165 | + async with session.get( |
| 166 | + f"{NORY_API_BASE}/api/x402/transactions/{transaction_id}", |
| 167 | + params={"network": network}, |
| 168 | + headers=self._get_headers(), |
| 169 | + raise_for_status=True, |
| 170 | + ) as response: |
| 171 | + return await response.text() |
| 172 | + |
| 173 | + @kernel_function( |
| 174 | + name="health_check", |
| 175 | + description="Check Nory service health and see supported networks.", |
| 176 | + ) |
| 177 | + async def health_check(self) -> str: |
| 178 | + """Check Nory service health. |
| 179 | +
|
| 180 | + Verify the payment service is operational and see supported networks. |
| 181 | +
|
| 182 | + Returns: |
| 183 | + JSON string with health status and supported networks. |
| 184 | + """ |
| 185 | + async with aiohttp.ClientSession() as session: |
| 186 | + async with session.get( |
| 187 | + f"{NORY_API_BASE}/api/x402/health", |
| 188 | + raise_for_status=True, |
| 189 | + ) as response: |
| 190 | + return await response.text() |
0 commit comments