diff --git a/105.md b/105.md new file mode 100644 index 00000000..8d2e72f3 --- /dev/null +++ b/105.md @@ -0,0 +1,229 @@ +NIP-105 +====== + +API Service Marketplace +------------------------------- + +`draft` `optional` `author:coachchuckff` `author:unclejim21` `author:cmdruid` + +This NIP defines `kind:31402` (a _parameterized replaceable event_) for broadcasting API services, endpoints and their costs in mSats. API services are offered ala carte to be paid via lightning invoices. + +API service providers will issue a `kind:31402` event as an API service offering. Clients can fetch `s` (service) tag offerings, call their endpoints, and pay per use. Creating an API service marketplace. + +## Protocol flow + +1. API Service provider creates and hosts API service offerings, they then issue a `kind:31402` event for each `s` ( service ) tag. +2. Client fetches API offerings by `s` tag. +3. Client chooses a provider(s) and POSTs to their `endpoint`. The POST body should match exactly what is required by the underlying API less the API key. ( Or following the note's `schema` ) +4. The service provider should validate their request and return a lightning invoice with a `successAction` url. The `successAction` url should be formatted as such: `//get_result` +5. The client pays. +6. The client will poll the `successAction` until the service provider has returned the result(s) matching exactly what the underlying API will produce ( Or following the note's `outputSchema` ). + +## Server Functions +### Create Invoice + +Initally, the client will make a POST request to the `endpoint`. The POST body should be exactly what the underlying API is expecting less the API key. This information should also be represented in the `schema` portion of the `content` field. + +The service provider will then do the following: + +1. Validate their API request +2. Issue a lightning invoice + +The issued lightning invoice will contain a `successAction` url formatted as so: + +```typescript +{ + tag: "url", + url: `https://example.api/chat/${paymentHash}/get_result`, + description: "Open to get the confirmation code for your purchase." +} +``` + +From here the service provider can either listen and execute their API request "on-payment" or wait until the client calls `get_results`. + +### Get Results + +Once the client has gotten the `successAction` url, they can issue a GET request. The service provider should respond in one of three ways: + +1. 200 ( DONE ) - the service provider returns the results of the API as if it was coming from the product itself ( Errors included ) +2. 202 ( WORKING ) - the service provider returns a status that indicates the results are not ready yet. +3. 402 ( NOT PAID YET ) - the service provider should respond with a 402 error if the client has not paid yet. + +It is up to the Client to poll the `successAction` until a terminal result is reached. + +## Event Reference and Examples + +### Offering Event + +`kind:31402` + +`.content` should be a JSON stringified version of the following JSON: +```typescript + enum OfferingStatus { + up = 'UP', + down = 'DOWN', + closed = 'CLOSED' + } + + interface OfferingContent = { + endpoint: string, // The POST endpoint you call to pay/fetch + status: OfferingStatus, // UP/DOWN/CLOSED + cost: number, // The cost per call in mSats + schema?: Object, // Reccomended - JSON schema for the POST body of the endpoint + outputSchema?: Object, // Reccomended - JSON schema for the response of the call + description?: string // Optional - Description for the end user + } +``` + +`.tag` MUST include the following: + +- `s`, the service tag should simply be the underlying API endpoint of the service provided. For example if you are offering a ChatGPT service, you would set `s` = `https://api.openai.com/v1/chat/completions`. This way the service they are buying is implicit. +- `d`, following **parameterized replaceable events** in [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md), this tag allows this event to be replaceable. Since there should only be one service per pubkey, the `d` tag should match the `s` tag. + +### Example Service Event + +```typescript + +const content = { + endpoint: "https://example.api/chat/", + status: "UP", + cost: 5000, + schema: {...}, + outputSchema: {...}, + description: "This will call the ChatGPT endpoint" +} + +const event = { + "kind": 31402, + "created_at": 1675642635, + "content": JSON.stringify(content), + "tags": [ + ["s", "https://api.openai.com/v1/chat/completions"] + ["d", "https://api.openai.com/v1/chat/completions"], + ], + "pubkey": "", + "id": "" +} +``` + +### Example Schema + +The following is for the `gpt-3.5-turbo` input schema: + +```json +{ + "type": "object", + "properties": { + "model": { + "type": "string", + "enum": ["gpt-3.5-turbo"] + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["system", "user"] + }, + "content": { + "type": "string" + } + }, + "required": ["role", "content"] + }, + "minItems": 1 + } + }, + "required": ["model", "messages"] +} +``` + +The following is for the `gpt-3.5-turbo` output schema: + +```json +{ + "type": "object", + "required": ["id", "object", "created", "model", "choices", "usage"], + "properties": { + "id": { + "type": "string", + "pattern": "^chatcmpl-[a-zA-Z0-9-]+$" + }, + "object": { + "type": "string", + "enum": ["chat.completion"] + }, + "created": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "required": ["index", "message"], + "properties": { + "index": { + "type": "integer" + }, + "message": { + "type": "object", + "required": ["role", "content"], + "properties": { + "role": { + "type": "string", + "enum": ["assistant"] + }, + "content": { + "type": "string" + } + } + }, + "finish_reason": { + "type": "string", + "enum": ["stop"] + } + } + } + }, + "usage": { + "type": "object", + "required": ["prompt_tokens", "completion_tokens", "total_tokens"], + "properties": { + "prompt_tokens": { + "type": "integer" + }, + "completion_tokens": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + } + } + } +``` +### Safey + +It is not mandatory, but to raise the barrier to entry, clients should screen service provider's NIP-05 identifier. The domain used in their NIP-05, should be the same domain used for their endpoint. + +Clients may wish to create a whitelist of trusted service providers once tested. + +### Problems + +- No data integrity - service providers can store/redistribute any data passed to them +- Service providers could take payment and never return the product +- Service providers are not gaurenteed to call the endpoint specified in the `s` tag +- No recourse for errored API fetches +- The `cost` field may not match the actual final price +- No proof of purchase + +### Example Implementations + +- [Server](https://github.com/Team-Pleb-TabConf-2023/nip-105-server) +- [Client](https://github.com/Team-Pleb-TabConf-2023/nip-105-client)