{
  "openapi": "3.1.0",
  "info": {
    "title": "nimimo public API",
    "version": "1.0.0",
    "summary": "Resolve a nimimo handle to blockchain addresses, and create payment intents for a human to sign.",
    "description": "nimimo maps a human-readable handle, such as `cool-water`, to the\nblockchain addresses its owner controls. This API is the machine\nsurface for that mapping, plus a payment-intent flow that turns an\namount and a recipient into a signing link.\n\n## Authentication\n\nNone. Every operation here is public, unauthenticated, and readable\nfrom any origin (`Access-Control-Allow-Origin: *`). There is no API\nkey, no token, and no OAuth scope to request, which is why every\noperation declares an empty `security` list rather than a scheme.\n\nEvery path also answers `OPTIONS` with the preflight headers a\nbrowser needs. Those preflights are not listed as operations\nbelow: they are transport, not API surface, and an agent that\nturned one into a callable tool would have made itself a tool that\ndoes nothing.\n\nnimimo is non-custodial, so there is nothing here that could move\nfunds even with a credential. Creating an intent produces a URL; a\nhuman signs the transaction on their own device with a key this API\nhas never seen.\n\n## Rate limits\n\nRequests are rate limited per IP at the edge. Limits are enforced\nper path prefix rather than per operation, and are set generously\nfor read traffic. A limited request answers `429` with a\n`Retry-After` header.\n\n## Errors\n\nEvery error is JSON. Errors raised by an endpoint carry `error`, a\nstable machine-readable code, and `message`, prose for a human.\nErrors raised by request-body validation carry `error` set to\n`Validation failed` and `details` listing what failed.",
    "contact": {
      "name": "nimimo",
      "url": "https://nimimo.com/contact"
    },
    "license": {
      "name": "nimimo Terms of Service",
      "url": "https://nimimo.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://nimimo.com",
      "description": "Production"
    }
  ],
  "security": [],
  "externalDocs": {
    "description": "Prose description of nimimo for agents, including when to use it",
    "url": "https://nimimo.com/llms.txt"
  },
  "tags": [
    {
      "name": "resolution",
      "description": "Turn a handle into addresses. Read-only, no side effects."
    },
    {
      "name": "intents",
      "description": "A payment intent records who should be paid, on which chain, how much, and produces a link a human opens to sign. Creating one moves no money."
    }
  ],
  "paths": {
    "/api/v1/resolve": {
      "get": {
        "operationId": "resolveHandle",
        "tags": [
          "resolution"
        ],
        "summary": "Resolve a handle to its addresses",
        "description": "Returns the public blockchain addresses registered to a nimimo\nhandle. Pass `chain` to get a single address; omit it to get the\nwhole map.\n\nHandles that end in `-campaign` resolve against the parent\nhandle's campaign wallet, which is a separately derived set of\naddresses. If the owner has not activated campaigns the endpoint\nanswers `404 not_found`.\n\nUse this when you have a name and need somewhere to send funds.",
        "security": [],
        "parameters": [
          {
            "name": "handle",
            "in": "query",
            "required": true,
            "description": "The nimimo handle, without the leading `@`. Case is normalized to lowercase.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
              "maxLength": 100
            },
            "example": "cool-water"
          },
          {
            "name": "chain",
            "in": "query",
            "required": false,
            "description": "Restrict the answer to one chain. `enjin` is accepted as a legacy spelling of `enjin_relay`.",
            "schema": {
              "type": "string",
              "enum": [
                "bitcoin",
                "ethereum",
                "solana",
                "enjin_relay",
                "enjin_matrix",
                "enjin"
              ]
            },
            "example": "bitcoin"
          }
        ],
        "responses": {
          "200": {
            "description": "The handle resolved. The body carries a single `address` when `chain` was given, and an `addresses` map otherwise.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ResolvedAddresses"
                    },
                    {
                      "$ref": "#/components/schemas/ResolvedAddress"
                    }
                  ]
                },
                "examples": {
                  "allChains": {
                    "summary": "No chain given",
                    "value": {
                      "handle": "cool-water",
                      "addresses": {
                        "bitcoin": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
                        "ethereum": "0x9858EfFD232B4033E47d90003D41EC34EcaEda94",
                        "solana": "GjJyeC1r2RgkuoCWMyPYkCWSGSGLcz266EaAkLA27AhL"
                      }
                    }
                  },
                  "singleChain": {
                    "summary": "chain=bitcoin",
                    "value": {
                      "handle": "cool-water",
                      "chain": "bitcoin",
                      "address": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The handle or the chain is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_handle": {
                    "value": {
                      "error": "invalid_handle",
                      "message": "Handle must be lowercase alphanumeric with optional hyphens"
                    }
                  },
                  "invalid_chain": {
                    "value": {
                      "error": "invalid_chain",
                      "message": "Supported chains: bitcoin, ethereum, solana, enjin_relay, enjin_matrix"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such handle, or the handle exists but has no address on the requested chain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found",
                      "message": "Handle not found"
                    }
                  },
                  "no_address": {
                    "value": {
                      "error": "no_address",
                      "message": "No bitcoin address registered for this handle"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Resolution failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal_error": {
                    "value": {
                      "error": "internal_error",
                      "message": "Resolution failed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/intents": {
      "post": {
        "operationId": "createIntent",
        "tags": [
          "intents"
        ],
        "summary": "Create a payment intent",
        "description": "Records an intent to pay a handle and returns a `sign_url` for a\nhuman to open. Nothing is signed, broadcast, or charged here;\nthe intent is a shared reference the payer and the payee can\nboth point at.\n\nUse this when an agent has agreed a payment on a user's behalf\nand needs to hand that user something to approve. Poll\n`GET /api/v1/intents/{id}` for the outcome.\n\nIntents expire an hour after creation unless `expires_at` says\notherwise.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateIntentRequest"
              },
              "example": {
                "from": "@some-agent",
                "to": "@cool-water",
                "chain": "ethereum",
                "asset": "ETH",
                "amount": "0.05",
                "memo": "Design payment"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Intent created and awaiting a signature.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Intent"
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, or a field failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationError"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The recipient handle does not exist, or has no address on the requested chain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found",
                      "message": "Handle not found"
                    }
                  },
                  "no_address": {
                    "value": {
                      "error": "no_address",
                      "message": "No bitcoin address registered for this handle"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Intent creation failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal_error": {
                    "value": {
                      "error": "internal_error",
                      "message": "Resolution failed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/intents/{id}": {
      "get": {
        "operationId": "getIntent",
        "tags": [
          "intents"
        ],
        "summary": "Read an intent",
        "description": "Returns the current state of an intent. An intent still\n`awaiting_signature` past its expiry is moved to `expired` by\nthis call, so the status you read is always current.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntentId"
          }
        ],
        "responses": {
          "200": {
            "description": "The intent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Intent"
                }
              }
            }
          },
          "400": {
            "description": "The id is not an intent id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_id": {
                    "value": {
                      "error": "invalid_id",
                      "message": "Intent ID must start with int_"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No intent with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found",
                      "message": "Handle not found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Lookup failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal_error": {
                    "value": {
                      "error": "internal_error",
                      "message": "Resolution failed"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateIntent",
        "tags": [
          "intents"
        ],
        "summary": "Advance an intent's status",
        "description": "Moves an intent along its lifecycle. Legal transitions:\n\n- `awaiting_signature` to `signed` or `cancelled`\n- `signed` to `completed` or `cancelled`\n\n`expired`, `completed` and `cancelled` are terminal. Moving to\n`completed` requires `tx_hash`.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntentId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateIntentRequest"
              },
              "example": {
                "status": "completed",
                "tx_hash": "0xabc123"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated intent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Intent"
                }
              }
            }
          },
          "400": {
            "description": "The id is malformed, the body is not JSON, or a field failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationError"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No intent with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "value": {
                      "error": "not_found",
                      "message": "Handle not found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "That transition is not legal from the current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_transition": {
                    "value": {
                      "error": "invalid_transition",
                      "message": "Cannot transition from \"signed\" to \"signed\""
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "The intent expired before it was signed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "intent_expired": {
                    "value": {
                      "error": "intent_expired",
                      "message": "This intent has expired"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Update failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal_error": {
                    "value": {
                      "error": "internal_error",
                      "message": "Resolution failed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {},
    "parameters": {
      "IntentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The intent id returned by `createIntent`. Always starts with `int_`.",
        "schema": {
          "type": "string",
          "pattern": "^int_[a-z0-9]{16}$"
        },
        "example": "int_9x21abcdefgh1234"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "An error raised by an endpoint. `error` is stable; `message` is not.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code.",
            "enum": [
              "invalid_handle",
              "invalid_chain",
              "invalid_body",
              "invalid_id",
              "invalid_transition",
              "intent_expired",
              "not_found",
              "no_address",
              "internal_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          }
        }
      },
      "ValidationError": {
        "type": "object",
        "description": "An error raised while validating a request body.",
        "required": [
          "error",
          "details"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "Validation failed"
          },
          "details": {
            "type": "string",
            "description": "The failing constraints, joined with a semicolon.",
            "examples": [
              "Amount must be a valid number"
            ]
          }
        }
      },
      "ResolvedAddresses": {
        "type": "object",
        "description": "Every registered address for the handle, keyed by chain.",
        "required": [
          "handle",
          "addresses"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
          },
          "addresses": {
            "type": "object",
            "description": "Chain identifier to address. Absent chains have no registered address.",
            "additionalProperties": {
              "type": "string"
            },
            "propertyNames": {
              "enum": [
                "bitcoin",
                "ethereum",
                "solana",
                "enjin_relay",
                "enjin_matrix"
              ]
            }
          }
        }
      },
      "ResolvedAddress": {
        "type": "object",
        "description": "One address, for the chain that was asked for.",
        "required": [
          "handle",
          "chain",
          "address"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
          },
          "chain": {
            "type": "string",
            "enum": [
              "bitcoin",
              "ethereum",
              "solana",
              "enjin_relay",
              "enjin_matrix"
            ]
          },
          "address": {
            "type": "string"
          }
        }
      },
      "CreateIntentRequest": {
        "type": "object",
        "required": [
          "to",
          "chain",
          "amount"
        ],
        "properties": {
          "from": {
            "type": "string",
            "maxLength": 256,
            "description": "Free-form identifier for whoever is paying. Not verified, not authenticated."
          },
          "to": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "Recipient handle. A leading `@` is stripped."
          },
          "chain": {
            "type": "string",
            "enum": [
              "bitcoin",
              "ethereum",
              "solana",
              "enjin",
              "enjin_matrix",
              "base",
              "cardano"
            ]
          },
          "asset": {
            "type": "string",
            "enum": [
              "BTC",
              "ETH",
              "SOL"
            ],
            "description": "Defaults from the chain when omitted."
          },
          "amount": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d+)?$",
            "description": "Human-readable decimal amount, as a string to avoid float rounding."
          },
          "memo": {
            "type": "string",
            "maxLength": 500
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "RFC 3339 with an offset. Defaults to one hour from creation."
          }
        }
      },
      "UpdateIntentRequest": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "signed",
              "completed",
              "cancelled"
            ]
          },
          "tx_hash": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "Required when `status` is `completed`."
          }
        }
      },
      "Intent": {
        "type": "object",
        "required": [
          "intent_id",
          "status",
          "to_handle",
          "to_address",
          "chain",
          "asset",
          "amount",
          "sign_url"
        ],
        "properties": {
          "intent_id": {
            "type": "string",
            "pattern": "^int_[a-z0-9]{16}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_signature",
              "signed",
              "completed",
              "cancelled",
              "expired"
            ]
          },
          "from": {
            "type": [
              "string",
              "null"
            ]
          },
          "to_handle": {
            "type": "string",
            "description": "Recipient handle, `@` prefixed."
          },
          "to_address": {
            "type": "string"
          },
          "to_avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "chain": {
            "type": "string",
            "enum": [
              "bitcoin",
              "ethereum",
              "solana",
              "enjin",
              "enjin_matrix",
              "base",
              "cardano"
            ]
          },
          "asset": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "memo": {
            "type": [
              "string",
              "null"
            ]
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "sign_url": {
            "type": "string",
            "format": "uri",
            "description": "Open this in a browser to sign. This is the only step that can move funds."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}