Skip to content

Latest commit

 

History

History
323 lines (251 loc) · 12.2 KB

File metadata and controls

323 lines (251 loc) · 12.2 KB

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 d ( service ) tag.
  2. Client fetches API offerings by d, i ( input schema ) or o ( output schema ) 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: <endpoint>/<invoice_hash>/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

Initially, 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 ( 402 ) -- OR --
  3. For zero-cost API, return the response ( 200 )

If returning a 402: The issued lightning invoice will contain a successAction url formatted as so:

{
      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.

Calculating Pricing

Pricing may be calculated using the equation: Total Cost = (cost_variable) * cost_units + cost_flat

This allows servers to offer fair & sustainable pricing based on the resources required to provide the service. Likewise this edit on the spec allows backward compatibility with previous pricing (which was flat only) as well as new use cases/business models.

One example would be a voice to text transcription service. The rate may be in seconds and the cost_variable may be 200 msats and cost_flat is 1000 msats. Therefore a 100 second transcription job should cost 21000 msats.

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.

(optional) Zap Request / Receipt

This specification is designed to optionally support the use of zap requests and receipts as a form of proof of payment between the client and service. A receipt represents that the client has purchased a service from the endpoint, which can further be used for publishing metrics, reviews, and other forms of engagement over the nostr network.

Offer Event

When the service publishes an offer event, the service may include a receipt tag, with the value set to true.

Example: tags: [ [ "receipt": "true" ] ]

This indicates that the provider supports the issuance of NIP-57 zap receipts.

Zap Request

When the client makes the initial POST request to the service endpoint, the client may choose to include a zap-request attribute in the header of the request. The value of the zap-request attribute should be a valid kind 9734 zap request event, serialized as a JSON string.

The content field of the zap request should be identical to the body of the POST request, serialized as a JSON string.

The zap request should include an a tag that references the kind:31402 offer event, as specified in NIP-01:

["a", <kind integer>:<32-bytes lowercase hex of a pubkey>:<d tag value>, <recommended relay URL, optional>]

The zap request should follow all standard conventions as defined in the NIP-57 specification.

Lightning Invoice

When the service responds with a lightning invoice, if the client's POST request has specified a valid zap-request in the header, then the description hash of the invoice MUST match the sha256 hash of the complete zap request as a serialized JSON string. This behavior is defined as part of the NIP-57 specification.

Zap Receipt

If the service responds to a successAction url request with a 200 range status code, and if the client POST request had previously specified a valid zap-request in the header, the service should include a zap-receipt attribute in the header of the response. This zap-receipt attribute should include a valid kind 9735 zap receipt, serialized as a JSON string.

The zap request should follow all standard conventions as defined in the NIP-57 specification.

In addition to serving the zap-receipt within the header of the response, the service may choose to publish the zap receipt to a relay.

Event Reference and Examples

Offering Event

kind:31402

.content should be a JSON stringified version of the following JSON:

  enum OfferingStatus {
    up = 'UP',
    down = 'DOWN',
    closed = 'CLOSED'
  }

  enum CostUnits {
    mins = 'MINS',
    secs = 'SECS',
    tokens = 'TOKENS'
  }

export interface OfferingContent {
    /** The POST endpoint you call to pay/fetch */
    endpoint: string,
    /** UP/DOWN/CLOSED */         
    status: OfferingStatus,   
    /** The fixed per call cost in mSats (b in y = mx + b) */ 
    fixedCost: number,
    /** The variable cost based on request's units (i.e. 2000msats per min) */
    variableCost: number,
    /** The units that denominate the variable cost */
    costUnits: number,
    /** Recommended - JSON schema for the POST body of the endpoint */
    schema?: Object,
    /** Recommended - JSON schema for the response of the call */
    outputSchema?: Object,
    /** Optional - Description for the end user */
    description?: string
  }

.tag MUST include the following:

//TODO compile a list of generics and name brands // Take out the s tag // d tag -> your endpoint // i tag for input json hash // o tag for output json hash

tags: [ ['t', 'llvm'], ['t', 'chatgpt'] ['t', 'stable diffusion'] ]

  • 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, this tag allows this event to be replaceable. This s tag should largely describe the service being provided.
  • i and o tag, these tags will be the hash of the input and output json schema provided in the content field. This will help discovery.

Service Provider Heartbeat and State

The service provider should update the kind kind:31402 offering every couple of minutes or so. This will help sort out stale and potentially inactive services. Additionally, when stopping a service, the service provider should mark their state with CLOSED.

Example Service Event

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": [
    ["d", "ChatGPT"]
    ["i", "c189fb5461886d68ef41a3976c7661666923894b8ba948bec90fa276fbdd1829"],
    ["o", "a5c7eb13e10caa40c0b04374ca68d9f19c30177ec3871823881b9e86bab9d549"],
  ],
  "pubkey": "<pubkey>",
  "id": "<id>"
}

Example Schema

The following is for the gpt-3.5-turbo input schema:

{
    "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:

{
    "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"
          }
        }
      }
    }
  }

Extra Considerations

Client Side Usage

There will need to be some consensus on the I/O and discovery/classification of services. There is some preliminary work done being here in the form of a client-side SDK.

The scheme is still TBD.

IO IO

Safety

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