{
  "components": {
    "schemas": {
      "AccountTransfer": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Transfer"
          },
          {
            "properties": {
              "subaccountKeyId": {
                "description": "The subaccount's oldest live key: the `{keyId}` every subaccount route takes.",
                "format": "uuid",
                "type": "string"
              }
            },
            "required": [
              "subaccountKeyId"
            ],
            "type": "object"
          }
        ],
        "description": "A transfer, and the address of the subaccount it moved funds to or from."
      },
      "Ack": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AckBody",
            "description": "The result, flattened into the ack: one key, named for the\noperation, e.g. `unsubscribed`."
          },
          {
            "properties": {
              "nonce": {
                "$ref": "#/components/schemas/Nonce",
                "description": "The nonce of the acknowledged request."
              }
            },
            "required": [
              "nonce"
            ],
            "type": "object"
          }
        ],
        "description": "The reply to one accepted request. Any server message that carries a\n`nonce` is the reply to the request that sent that nonce."
      },
      "AckBody": {
        "description": "An ack's body, keyed by operation: `unsubscribed` lists each dropped\nsubject, `status` the current subscriptions, `markets` and `events` one\ncatalog page, `placed` each accepted order, `canceled` an id batch's result,\nand `canceled_all` the count a scope cancel queued. A subscribe replies with\na [`WsSnapshot`](crate::WsSnapshot) instead.",
        "oneOf": [
          {
            "properties": {
              "unsubscribed": {
                "$ref": "#/components/schemas/SubscriptionMap"
              }
            },
            "required": [
              "unsubscribed"
            ],
            "type": "object"
          },
          {
            "properties": {
              "status": {
                "$ref": "#/components/schemas/Status"
              }
            },
            "required": [
              "status"
            ],
            "type": "object"
          },
          {
            "properties": {
              "markets": {
                "$ref": "#/components/schemas/Page_OpenMarketResponse"
              }
            },
            "required": [
              "markets"
            ],
            "type": "object"
          },
          {
            "properties": {
              "events": {
                "$ref": "#/components/schemas/Page_EventResponse"
              }
            },
            "required": [
              "events"
            ],
            "type": "object"
          },
          {
            "properties": {
              "placed": {
                "items": {
                  "$ref": "#/components/schemas/OrderAccepted"
                },
                "type": "array"
              }
            },
            "required": [
              "placed"
            ],
            "type": "object"
          },
          {
            "properties": {
              "canceled": {
                "$ref": "#/components/schemas/BatchCancelResult"
              }
            },
            "required": [
              "canceled"
            ],
            "type": "object"
          },
          {
            "properties": {
              "canceled_all": {
                "$ref": "#/components/schemas/CancelAllResult"
              }
            },
            "required": [
              "canceled_all"
            ],
            "type": "object"
          }
        ]
      },
      "Algorithm": {
        "description": "The signing algorithm of the stored public key. Your signer must match it.",
        "enum": [
          "Ed25519",
          "P-256"
        ],
        "type": "string"
      },
      "Balance": {
        "properties": {
          "balance": {
            "description": "A decimal string with exactly five decimal places.",
            "example": "1234.50000",
            "type": "string"
          },
          "keyId": {
            "format": "uuid",
            "type": "string"
          }
        },
        "required": [
          "keyId",
          "balance"
        ],
        "type": "object"
      },
      "BatchCancel": {
        "properties": {
          "orderIds": {
            "items": {
              "format": "uuid",
              "type": "string"
            },
            "maxItems": 256,
            "type": "array"
          }
        },
        "required": [
          "orderIds"
        ],
        "type": "object"
      },
      "BatchCancelResult": {
        "description": "The route cancels the rest even when one ID is unknown.",
        "properties": {
          "canceled": {
            "items": {
              "format": "uuid",
              "type": "string"
            },
            "type": "array"
          },
          "notCanceled": {
            "items": {
              "$ref": "#/components/schemas/NotCanceled"
            },
            "type": "array"
          }
        },
        "required": [
          "canceled",
          "notCanceled"
        ],
        "type": "object"
      },
      "BatchOrderError": {
        "description": "One rejected order within a batch placement: which order failed, and why.",
        "properties": {
          "index": {
            "description": "Zero-based position in the request.",
            "minimum": 0,
            "type": "integer"
          },
          "outcomeId": {
            "description": "The outcome ID of the rejected order.",
            "type": "string"
          },
          "reason": {
            "description": "Human-readable. Not stable. Branch on the envelope's `code` and on `index`.",
            "type": "string"
          }
        },
        "required": [
          "index",
          "outcomeId",
          "reason"
        ],
        "type": "object"
      },
      "BatchPlace": {
        "description": "Up to 256 orders, one `place` token each. The exchange judges each order on its own. The edge caps the raw body. A large batch meets that cap before the origin sees it. Split a batch that answers `403` with no error body.",
        "properties": {
          "orders": {
            "items": {
              "$ref": "#/components/schemas/PlaceOrder"
            },
            "maxItems": 256,
            "type": "array"
          }
        },
        "required": [
          "orders"
        ],
        "type": "object"
      },
      "Batch_BboDelta": {
        "description": "A sequenced group of one channel's deltas.",
        "properties": {
          "deltas": {
            "description": "The changes, in order. Never empty.",
            "items": {
              "description": "One outcome's bbo change. `bbo` carries the new best resting bid, and is\nabsent when the outcome's last resting bid left the book.",
              "properties": {
                "bbo": {
                  "oneOf": [
                    {
                      "type": "null"
                    },
                    {
                      "$ref": "#/components/schemas/BboLevel",
                      "description": "The new best resting bid. Absent when the outcome has no resting bid left."
                    }
                  ]
                },
                "outcomeId": {
                  "description": "ID of the outcome whose bbo changed.",
                  "format": "uuid",
                  "type": "string"
                }
              },
              "required": [
                "outcomeId"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "seq": {
            "description": "Position in the market's per-channel stream.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "deltas"
        ],
        "type": "object"
      },
      "Batch_BookDelta": {
        "description": "A sequenced group of one channel's deltas.",
        "properties": {
          "deltas": {
            "description": "The changes, in order. Never empty.",
            "items": {
              "description": "One order-book change, tagged by `kind`. Each delta names the one\norder it changes.",
              "oneOf": [
                {
                  "description": "An order joins the book, at the back of its price level.",
                  "properties": {
                    "kind": {
                      "enum": [
                        "add"
                      ],
                      "type": "string"
                    },
                    "orderId": {
                      "description": "ID of the resting order.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "outcomeId": {
                      "description": "ID of the outcome the order bids on.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "price": {
                      "description": "Limit price: a decimal string between 0 and 1.",
                      "type": "string"
                    },
                    "qty": {
                      "description": "Resting quantity, in the currency's smallest unit.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    }
                  },
                  "required": [
                    "orderId",
                    "outcomeId",
                    "price",
                    "qty",
                    "kind"
                  ],
                  "type": "object"
                },
                {
                  "description": "A resting order's quantity decreases.",
                  "properties": {
                    "kind": {
                      "enum": [
                        "update"
                      ],
                      "type": "string"
                    },
                    "orderId": {
                      "description": "ID of the resting order.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "remaining": {
                      "description": "Resting quantity after the change, in the currency's smallest\nunit.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    }
                  },
                  "required": [
                    "orderId",
                    "remaining",
                    "kind"
                  ],
                  "type": "object"
                },
                {
                  "description": "An order leaves the book.",
                  "properties": {
                    "kind": {
                      "enum": [
                        "remove"
                      ],
                      "type": "string"
                    },
                    "orderId": {
                      "description": "ID of the order that left the book.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "reason": {
                      "$ref": "#/components/schemas/RemoveReason",
                      "description": "Why it left."
                    }
                  },
                  "required": [
                    "orderId",
                    "reason",
                    "kind"
                  ],
                  "type": "object"
                }
              ]
            },
            "type": "array"
          },
          "seq": {
            "description": "Position in the market's per-channel stream.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "deltas"
        ],
        "type": "object"
      },
      "Batch_LifecycleDelta": {
        "description": "A sequenced group of one channel's deltas.",
        "properties": {
          "deltas": {
            "description": "The changes, in order. Never empty.",
            "items": {
              "description": "A market lifecycle change: the transition and the resulting status.",
              "properties": {
                "kind": {
                  "$ref": "#/components/schemas/MarketLifecycle",
                  "description": "The transition."
                },
                "status": {
                  "$ref": "#/components/schemas/MarketStatus",
                  "description": "The market status after the transition."
                }
              },
              "required": [
                "kind",
                "status"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "seq": {
            "description": "Position in the market's per-channel stream.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "deltas"
        ],
        "type": "object"
      },
      "Batch_OrderEvent": {
        "description": "A sequenced group of one channel's deltas.",
        "properties": {
          "deltas": {
            "description": "The changes, in order. Never empty.",
            "items": {
              "description": "One change to one of the caller's orders, tagged by `kind`.",
              "oneOf": [
                {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/OpenOrder",
                      "description": "An order rests on the book."
                    },
                    {
                      "properties": {
                        "kind": {
                          "enum": [
                            "open"
                          ],
                          "type": "string"
                        }
                      },
                      "required": [
                        "kind"
                      ],
                      "type": "object"
                    }
                  ],
                  "description": "An order rests on the book."
                },
                {
                  "description": "A fill against one of the caller's orders. Fees are excluded.",
                  "properties": {
                    "clientId": {
                      "description": "The order's client id, when it sent one.",
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "kind": {
                      "enum": [
                        "fill"
                      ],
                      "type": "string"
                    },
                    "orderId": {
                      "description": "ID of the filled order.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "outcomeId": {
                      "description": "ID of the outcome that traded.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "price": {
                      "description": "Fill price: a decimal string between 0 and 1.",
                      "type": "string"
                    },
                    "qty": {
                      "description": "Quantity filled, in the currency's smallest unit.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    },
                    "remaining": {
                      "description": "Quantity still resting after the fill.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    }
                  },
                  "required": [
                    "orderId",
                    "outcomeId",
                    "price",
                    "qty",
                    "remaining",
                    "kind"
                  ],
                  "type": "object"
                },
                {
                  "description": "An order left the book.",
                  "properties": {
                    "kind": {
                      "enum": [
                        "cancel"
                      ],
                      "type": "string"
                    },
                    "orderId": {
                      "description": "ID of the cancelled order.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "reason": {
                      "oneOf": [
                        {
                          "type": "null"
                        },
                        {
                          "$ref": "#/components/schemas/CancelReason",
                          "description": "Why it left. Absent for an ordinary cancel by the owner, by expiry,\nor by an admin."
                        }
                      ]
                    }
                  },
                  "required": [
                    "orderId",
                    "kind"
                  ],
                  "type": "object"
                },
                {
                  "description": "The engine rejected an order after the gateway accepted it.",
                  "properties": {
                    "kind": {
                      "enum": [
                        "reject"
                      ],
                      "type": "string"
                    },
                    "orderId": {
                      "description": "ID of the rejected order.",
                      "format": "uuid",
                      "type": "string"
                    }
                  },
                  "required": [
                    "orderId",
                    "kind"
                  ],
                  "type": "object"
                }
              ]
            },
            "type": "array"
          },
          "seq": {
            "description": "Position in the market's per-channel stream.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "deltas"
        ],
        "type": "object"
      },
      "Batch_Position": {
        "description": "A sequenced group of one channel's deltas.",
        "properties": {
          "deltas": {
            "description": "The changes, in order. Never empty.",
            "items": {
              "description": "The contracts held and what they cost. Nothing here is derived.",
              "properties": {
                "cost": {
                  "description": "Net dollars the wallet paid for them. The average entry price is `100 × cost / qty`.",
                  "type": "string"
                },
                "marketId": {
                  "format": "uuid",
                  "type": "string"
                },
                "outcomeId": {
                  "format": "uuid",
                  "type": "string"
                },
                "qty": {
                  "description": "Contracts held.",
                  "format": "int32",
                  "minimum": 0,
                  "type": "integer"
                }
              },
              "required": [
                "marketId",
                "outcomeId",
                "qty",
                "cost"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "seq": {
            "description": "Position in the market's per-channel stream.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "deltas"
        ],
        "type": "object"
      },
      "Batch_Trade": {
        "description": "A sequenced group of one channel's deltas.",
        "properties": {
          "deltas": {
            "description": "The changes, in order. Never empty.",
            "items": {
              "description": "One execution on the public tape.",
              "properties": {
                "outcomeId": {
                  "format": "uuid",
                  "type": "string"
                },
                "price": {
                  "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
                  "type": "string"
                },
                "qty": {
                  "description": "Number of contracts. A winning contract pays full value, 1¢.",
                  "format": "int32",
                  "minimum": 0,
                  "type": "integer"
                },
                "tradeId": {
                  "format": "uuid",
                  "type": "string"
                },
                "ts": {
                  "description": "Unix milliseconds.",
                  "format": "int64",
                  "type": "integer"
                }
              },
              "required": [
                "tradeId",
                "outcomeId",
                "price",
                "qty",
                "ts"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "seq": {
            "description": "Position in the market's per-channel stream.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "deltas"
        ],
        "type": "object"
      },
      "BboLevel": {
        "description": "One outcome's best resting bid: its price and the total resting size at that\nprice.",
        "properties": {
          "price": {
            "description": "Limit price: a decimal string between 0 and 1.",
            "type": "string"
          },
          "qty": {
            "description": "Total resting quantity at the price, in the currency's smallest unit.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "price",
          "qty"
        ],
        "type": "object"
      },
      "BboSnapshot": {
        "description": "The bbo channel's part of a snapshot: each outcome's best resting bid at\n`seq`. An outcome with no resting bid is absent.",
        "properties": {
          "bbo": {
            "additionalProperties": {
              "$ref": "#/components/schemas/BboLevel"
            },
            "description": "Each outcome's best resting bid.",
            "propertyNames": {
              "type": "string"
            },
            "type": "object"
          },
          "seq": {
            "description": "The seq of the last bbo batch before the snapshot. The next bbo delta\nfollows this seq. `0` means no batch exists yet.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "bbo"
        ],
        "type": "object"
      },
      "Book": {
        "description": "The order book of one market.",
        "properties": {
          "marketId": {
            "format": "uuid",
            "type": "string"
          },
          "orders": {
            "additionalProperties": {
              "items": {
                "$ref": "#/components/schemas/RestingOrder"
              },
              "type": "array"
            },
            "description": "Resting orders by outcome ID, best price first, earlier order first within a price.\nLimited to `depth` price levels per outcome.",
            "propertyNames": {
              "type": "string"
            },
            "type": "object"
          },
          "seq": {
            "description": "The book's sequence number at this snapshot. The next `book` delta on the websocket follows it.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "marketId",
          "seq",
          "orders"
        ],
        "type": "object"
      },
      "BookSnapshot": {
        "description": "The book channel's part of a snapshot: the resting orders at `seq`, grouped\nby outcome.",
        "properties": {
          "orders": {
            "additionalProperties": {
              "items": {
                "$ref": "#/components/schemas/RestingOrder"
              },
              "type": "array"
            },
            "description": "Each outcome's resting orders in priority order: best price first,\nearlier order first within a price. Limited to the request's `depth`\nprice levels per outcome.",
            "propertyNames": {
              "type": "string"
            },
            "type": "object"
          },
          "seq": {
            "description": "The seq of the last book batch before the snapshot. The next book\ndelta follows this seq. `0` means no batch exists yet.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "orders"
        ],
        "type": "object"
      },
      "BucketLimit": {
        "description": "One bucket's limit: how many tokens it holds and how fast it refills.",
        "properties": {
          "capacity": {
            "description": "The most tokens the bucket holds. That is the largest burst you can send.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "refillPerSec": {
            "description": "Tokens added per second, up to `capacity`.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "capacity",
          "refillPerSec"
        ],
        "type": "object"
      },
      "CancelAccepted": {
        "properties": {
          "orderId": {
            "format": "uuid",
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus",
            "description": "The status at the moment of the cancel request. The `cancel` event on the private stream confirms the cancel."
          }
        },
        "required": [
          "orderId",
          "status"
        ],
        "type": "object"
      },
      "CancelAllResult": {
        "properties": {
          "canceled": {
            "description": "The number of cancels queued.",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "canceled"
        ],
        "type": "object"
      },
      "CancelFilter": {
        "additionalProperties": false,
        "description": "What `DELETE /orders` cancels. See [`EmoScope`] for how the ids narrow. An\nunknown key is refused: a typo must not widen a cancel to every order.",
        "properties": {
          "event": {
            "description": "Every market of one event.",
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "market": {
            "description": "One market. With `event`, the market must belong to that event.",
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "description": "One outcome. With `market`, the outcome must belong to that market.",
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "CancelReason": {
        "description": "Why the engine took one of the caller's orders off the book, when the cause\nis known.",
        "enum": [
          "GO_LIVE",
          "NEUTRALIZED",
          "MARKET_CLOSED",
          "SETTLED"
        ],
        "type": "string"
      },
      "CatalogQuery_EventFilter": {
        "allOf": [
          {
            "description": "Filters for listing events. Every filter must match. A comma-separated\nvalue matches any one of its items. An absent filter matches every event.",
            "properties": {
              "league": {
                "description": "Comma-separated canonical league names.",
                "example": "NFL,NBA",
                "type": [
                  "string",
                  "null"
                ]
              },
              "startsAfter": {
                "description": "Exclusive lower bound on the event's scheduled start.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "startsBefore": {
                "description": "Exclusive upper bound on the event's scheduled start.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "status": {
                "description": "Comma-separated event statuses.",
                "example": "OPEN_PREGAME",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "type": "object"
          },
          {
            "$ref": "#/components/schemas/PageCursor"
          }
        ],
        "description": "A catalog read: a filter over the listing and a page cursor. A JSON frame\nflattens both into one object; a query string carries the same fields."
      },
      "CatalogQuery_MarketFilter": {
        "allOf": [
          {
            "description": "Filters for listing markets. Every filter must match. A comma-separated\nvalue matches any one of its items. An absent filter matches every market.",
            "properties": {
              "event": {
                "description": "Every market of one event.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "eventStatus": {
                "description": "Comma-separated statuses required of the owning event.",
                "example": "OPEN_PREGAME",
                "type": [
                  "string",
                  "null"
                ]
              },
              "league": {
                "description": "Comma-separated canonical league names.",
                "example": "NFL,NBA",
                "type": [
                  "string",
                  "null"
                ]
              },
              "marketType": {
                "description": "Comma-separated canonical market types.",
                "example": "MONEY,SPREAD",
                "type": [
                  "string",
                  "null"
                ]
              },
              "startsAfter": {
                "description": "Exclusive lower bound on the event's scheduled start.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "startsBefore": {
                "description": "Exclusive upper bound on the event's scheduled start.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "type": "object"
          },
          {
            "$ref": "#/components/schemas/PageCursor"
          }
        ],
        "description": "A catalog read: a filter over the listing and a page cursor. A JSON frame\nflattens both into one object; a query string carries the same fields."
      },
      "Chargeability": {
        "description": "When a market charges its taker. `WHEN_LIVE` charges only while the event is `OPEN_INGAME`. An eligible futures market charges `ALWAYS`, because its event never goes live. A PGA, ATP, WTA, UFC, F1 or NASCAR future carries no futures trading fee. It reads `WHEN_LIVE` and never charges.",
        "enum": [
          "ALWAYS",
          "WHEN_LIVE"
        ],
        "type": "string"
      },
      "CreateKey": {
        "additionalProperties": false,
        "description": "This route issues `management::read`. The body has no scope field.",
        "properties": {
          "algorithm": {
            "$ref": "#/components/schemas/Algorithm"
          },
          "expiresAt": {
            "description": "Optional. An instant strictly in the future.",
            "format": "date-time",
            "type": "string"
          },
          "name": {
            "description": "A label you choose.",
            "type": "string"
          },
          "publicKey": {
            "description": "The SPKI PEM, newlines included.",
            "type": "string"
          }
        },
        "required": [
          "name",
          "publicKey",
          "algorithm"
        ],
        "type": "object"
      },
      "CreateSubaccountKey": {
        "additionalProperties": false,
        "description": "The one body that names its own scope.",
        "properties": {
          "algorithm": {
            "$ref": "#/components/schemas/Algorithm"
          },
          "expiresAt": {
            "description": "Optional. An instant strictly in the future.",
            "format": "date-time",
            "type": "string"
          },
          "name": {
            "description": "A label you choose.",
            "type": "string"
          },
          "publicKey": {
            "description": "The SPKI PEM of a fresh keypair.",
            "type": "string"
          },
          "scope": {
            "$ref": "#/components/schemas/SubaccountKeyScope"
          }
        },
        "required": [
          "name",
          "publicKey",
          "algorithm",
          "scope"
        ],
        "type": "object"
      },
      "ErrorBody": {
        "description": "Every error on the surface carries this body.",
        "properties": {
          "code": {
            "description": "Stable identifier. The only field to branch on.",
            "example": "MARKET_NOT_FOUND",
            "type": "string"
          },
          "message": {
            "description": "Human-readable. Not stable. Never match on it.",
            "type": "string"
          },
          "nonce": {
            "description": "The websocket request that failed. Absent on REST.",
            "format": "int64",
            "maximum": 9007199254740991,
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "rejected": {
            "description": "One entry per refused order in a batch. Present only on an error that a batch caused.",
            "items": {
              "$ref": "#/components/schemas/BatchOrderError"
            },
            "type": [
              "array",
              "null"
            ]
          }
        },
        "required": [
          "code",
          "message"
        ],
        "type": "object"
      },
      "EventResponse": {
        "description": "An event in the open set: the contest its markets settle on.",
        "properties": {
          "description": {
            "description": "Human-readable description of the event.",
            "type": "string"
          },
          "eventId": {
            "description": "Event ID.",
            "format": "uuid",
            "type": "string"
          },
          "league": {
            "description": "A canonical name from `GET /v3/types/leagues`. One league belongs to one sport.",
            "example": "NFL",
            "type": "string"
          },
          "sport": {
            "description": "A canonical name from `GET /v3/types/sports`. One league belongs to one sport. An\nunrecognized league reports its raw name here too.",
            "example": "FOOTBALL",
            "type": "string"
          },
          "startsTs": {
            "description": "Unix milliseconds.",
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/EventStatus",
            "description": "Lifecycle status of an event. `OPEN_INGAME` charges the taker on every `WHEN_LIVE` market.\nThe move to `OPEN_INGAME` voids every resting order on the event's markets."
          }
        },
        "required": [
          "eventId",
          "description",
          "sport",
          "league",
          "status",
          "startsTs"
        ],
        "type": "object"
      },
      "EventStatus": {
        "description": "Lifecycle status of an event. `OPEN_INGAME` charges the taker on every `WHEN_LIVE` market. The move to `OPEN_INGAME` voids every resting order on the event's markets.",
        "enum": [
          "OPEN_PREGAME",
          "CLOSED_PREGAME",
          "OPEN_INGAME",
          "SETTLED",
          "FINAL",
          "DELAYED",
          "CANCELED"
        ],
        "type": "string"
      },
      "Fill": {
        "properties": {
          "clientId": {
            "description": "The order's `clientId`, when it had one.",
            "format": "uuid",
            "type": "string"
          },
          "cost": {
            "description": "What this side paid for the contracts, in dollars: `qty × price × 1¢`. The price it traded at is `100 × cost / qty`. That price can be better than your limit.",
            "type": "string"
          },
          "fee": {
            "description": "Absent on an uncharged fill.",
            "type": "string"
          },
          "fillId": {
            "format": "uuid",
            "type": "string"
          },
          "marketId": {
            "format": "uuid",
            "type": "string"
          },
          "orderId": {
            "format": "uuid",
            "type": "string"
          },
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "qty": {
            "description": "Number of contracts. A winning contract pays full value, 1¢.",
            "format": "int32",
            "minimum": 1,
            "type": "integer"
          },
          "taker": {
            "type": "boolean"
          },
          "ts": {
            "description": "Unix milliseconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "fillId",
          "orderId",
          "marketId",
          "outcomeId",
          "qty",
          "cost",
          "taker",
          "ts"
        ],
        "type": "object"
      },
      "HeartbeatSeqs": {
        "description": "The caller's last seq on each subscribed PRIVATE channel, plus the send\ntime. An unsubscribed channel is absent.",
        "properties": {
          "orders": {
            "description": "The last `orders` seq, when the caller subscribes `orders`.",
            "format": "int64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "positions": {
            "description": "The last `positions` seq, when the caller subscribes `positions`.",
            "format": "int64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "ts": {
            "description": "Send time, in epoch milliseconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "ts"
        ],
        "type": "object"
      },
      "HistoricalEventResponse": {
        "description": "An event of any age and status.",
        "properties": {
          "description": {
            "description": "Human-readable description of the event.",
            "type": "string"
          },
          "eventId": {
            "description": "Event ID.",
            "format": "uuid",
            "type": "string"
          },
          "league": {
            "description": "A canonical name from `GET /v3/types/leagues`. Absent on an event with no league.",
            "example": "NFL",
            "type": "string"
          },
          "sport": {
            "description": "A canonical name from `GET /v3/types/sports`. An unrecognized league reports its raw name\nhere too. Absent on an event with no league.",
            "example": "FOOTBALL",
            "type": "string"
          },
          "startsTs": {
            "description": "Unix milliseconds. Absent on an event with no scheduled start.",
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/EventStatus",
            "description": "Lifecycle status of an event. `OPEN_INGAME` charges the taker on every `WHEN_LIVE` market.\nThe move to `OPEN_INGAME` voids every resting order on the event's markets."
          }
        },
        "required": [
          "eventId",
          "description",
          "status"
        ],
        "type": "object"
      },
      "HistoricalMarketResponse": {
        "description": "A market of any age and status.",
        "properties": {
          "description": {
            "description": "Human-readable. Use it for discovery, never as a contract.",
            "type": "string"
          },
          "eventId": {
            "description": "ID of the event the market belongs to.",
            "format": "uuid",
            "type": "string"
          },
          "fee": {
            "$ref": "#/components/schemas/MarketFee",
            "description": "What a taker pays on this market. Read it per market. Never derive it from a league list."
          },
          "marketId": {
            "description": "Market ID.",
            "format": "uuid",
            "type": "string"
          },
          "marketType": {
            "description": "A canonical name from `GET /v3/types/markets`.",
            "example": "MONEY",
            "type": "string"
          },
          "outcomes": {
            "description": "Outcomes of the market. A graded outcome carries its grade in `status`.",
            "items": {
              "$ref": "#/components/schemas/Outcome"
            },
            "type": "array"
          },
          "settledTs": {
            "description": "Unix milliseconds of the most recent settlement. A re-settlement updates it. Absent on an\nunsettled market.",
            "example": 1756512000000,
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/MarketStatus",
            "description": "Lifecycle status of a market."
          },
          "strike": {
            "description": "The line the market settles against, a decimal string. A spread's line is the home side's\nhandicap. Absent on a market without a line.",
            "example": "-3.5",
            "type": "string"
          },
          "voids": {
            "$ref": "#/components/schemas/Voidability",
            "description": "How a market settles if it voids. `PUSH` refunds every fill at its cost. `FMV` settles every\noutcome at its fair market value. The exchange never pushes an `FMV` market. Do not assume a\nrefund."
          }
        },
        "required": [
          "marketId",
          "description",
          "eventId",
          "marketType",
          "status",
          "voids",
          "fee",
          "outcomes"
        ],
        "type": "object"
      },
      "HistoricalPositionResponse": {
        "description": "A position of the caller, with the status of its market and the grade of its outcome.",
        "properties": {
          "cost": {
            "description": "Net dollars the wallet paid for them. The average entry price is `100 × cost / qty`.",
            "type": "string"
          },
          "marketId": {
            "format": "uuid",
            "type": "string"
          },
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "payout": {
            "description": "What the contracts are worth at `result`, in dollars. Absent while `result` is `TBD`.",
            "type": "string"
          },
          "pnl": {
            "description": "`payout` minus `cost`, in dollars. Absent while `result` is `TBD`.",
            "type": "string"
          },
          "positionId": {
            "description": "Position ID.",
            "format": "uuid",
            "type": "string"
          },
          "qty": {
            "description": "Contracts held.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "resettled": {
            "description": "`true` if the market was graded again after it settled.",
            "type": "boolean"
          },
          "result": {
            "description": "The grade of the outcome. It's `TBD` until the market settles.",
            "type": "string"
          },
          "settledTs": {
            "description": "When the market settled. Absent until the market settles.",
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/MarketStatus",
            "description": "Lifecycle status of a market."
          }
        },
        "required": [
          "positionId",
          "marketId",
          "outcomeId",
          "qty",
          "cost",
          "status",
          "result",
          "resettled"
        ],
        "type": "object"
      },
      "Key": {
        "description": "One live key.",
        "properties": {
          "algorithm": {
            "$ref": "#/components/schemas/Algorithm"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "expiresAt": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "fingerprint": {
            "description": "`sha256:` and the hex SHA-256 of the SPKI DER.",
            "example": "sha256:6d4058ea55f365a60868b7a29f302deee40cd738f1ee98f14f43381c13f1a232",
            "type": "string"
          },
          "keyId": {
            "format": "uuid",
            "type": "string"
          },
          "lastUsedAt": {
            "description": "Reserved. Always `null`.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "restriction": {
            "description": "Non-null on a `trading` or `trading::read` key: the subaccount's label. A flag, not an address.",
            "type": [
              "string",
              "null"
            ]
          },
          "scope": {
            "$ref": "#/components/schemas/KeyScope"
          }
        },
        "required": [
          "keyId",
          "name",
          "fingerprint",
          "algorithm",
          "scope",
          "restriction",
          "createdAt",
          "lastUsedAt",
          "expiresAt"
        ],
        "type": "object"
      },
      "KeyCreated": {
        "properties": {
          "fingerprint": {
            "type": "string"
          },
          "keyId": {
            "format": "uuid",
            "type": "string"
          }
        },
        "required": [
          "keyId",
          "fingerprint"
        ],
        "type": "object"
      },
      "KeyScope": {
        "description": "A key's one scope. Every route but one fixes the scope it mints. `POST /v3/account/subaccounts/{keyId}/keys` is the exception. Its body names `trading` or `trading::read`.",
        "enum": [
          "trading",
          "trading::read",
          "management",
          "management::read"
        ],
        "type": "string"
      },
      "LabelSubaccount": {
        "additionalProperties": false,
        "description": "The label is the only part of a subaccount you can change.",
        "properties": {
          "label": {
            "maxLength": 64,
            "type": "string"
          }
        },
        "required": [
          "label"
        ],
        "type": "object"
      },
      "LifecycleSnapshot": {
        "description": "The lifecycle channel's part of a snapshot: the market's current status at\n`seq`.",
        "properties": {
          "seq": {
            "description": "The seq of the last lifecycle batch before the snapshot. The next\nlifecycle delta follows this seq. `0` means no batch exists yet.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/MarketStatus",
            "description": "The market status at `seq`."
          }
        },
        "required": [
          "seq",
          "status"
        ],
        "type": "object"
      },
      "LimitsResponse": {
        "description": "The rate-limit schedule: one `BucketLimit` per bucket. The schedule is\nstatic.",
        "properties": {
          "account": {
            "$ref": "#/components/schemas/BucketLimit",
            "description": "The bucket for private reads: orders, positions, and account data."
          },
          "cancel": {
            "$ref": "#/components/schemas/BucketLimit",
            "description": "The bucket for canceling orders. Its limits are never below `place`."
          },
          "history": {
            "$ref": "#/components/schemas/BucketLimit",
            "description": "The bucket for database-backed record reads: fills, transactions, and\nsettled orders. Each read costs a base plus one token per page of rows."
          },
          "maxWatchedMarkets": {
            "description": "The most markets one websocket connection may watch at once. An event\ncounts as the markets it contains.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "place": {
            "$ref": "#/components/schemas/BucketLimit",
            "description": "The bucket for placing orders. A batch costs one token per order."
          },
          "read": {
            "$ref": "#/components/schemas/BucketLimit",
            "description": "The bucket for public reads: catalog, books, stats, and archive pages."
          },
          "stream": {
            "$ref": "#/components/schemas/BucketLimit",
            "description": "The bucket for websocket requests: subscribe, unsubscribe, snapshot,\nand status."
          }
        },
        "required": [
          "read",
          "account",
          "place",
          "cancel",
          "stream",
          "history",
          "maxWatchedMarkets"
        ],
        "type": "object"
      },
      "MarketChannel": {
        "description": "A market-data feed, one per subscribed market or event. `book`, `bbo`, and\n`trades` also deliver `lifecycle`.",
        "enum": [
          "lifecycle",
          "trades",
          "bbo",
          "book"
        ],
        "type": "string"
      },
      "MarketDelta": {
        "description": "One market's changes in a delta message: one batch per changed channel.\nAt least one batch is present. The market's ID keys this value in the\nmessage.",
        "properties": {
          "bbo": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Batch_BboDelta",
                "description": "Top-of-book changes, if any."
              }
            ]
          },
          "book": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Batch_BookDelta",
                "description": "Book changes, if any."
              }
            ]
          },
          "eventId": {
            "description": "ID of the event that owns the market.",
            "format": "uuid",
            "type": "string"
          },
          "lifecycle": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Batch_LifecycleDelta",
                "description": "Lifecycle transitions, if any."
              }
            ]
          },
          "trades": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Batch_Trade",
                "description": "Trades, if any."
              }
            ]
          }
        },
        "required": [
          "eventId"
        ],
        "type": "object"
      },
      "MarketFee": {
        "description": "What a taker pays on this market. Read it per market. Never derive it from a league list.",
        "properties": {
          "charged": {
            "$ref": "#/components/schemas/Chargeability"
          },
          "coefficient": {
            "description": "`c` in the fee formula, a decimal string.",
            "type": "string"
          },
          "makerCredit": {
            "description": "The maker's share of the taker's fee on the same fill, a decimal string.",
            "type": "string"
          }
        },
        "required": [
          "coefficient",
          "makerCredit",
          "charged"
        ],
        "type": "object"
      },
      "MarketLifecycle": {
        "description": "A market lifecycle transition, as an UPPERCASE token. Not every\ntransition changes the market status.",
        "enum": [
          "OPEN",
          "CLOSE",
          "GRADE",
          "START",
          "END",
          "GOLIVE",
          "UNLIVE"
        ],
        "type": "string"
      },
      "MarketSnapshot": {
        "description": "One market's state in a snapshot message, per subscribed channel. The\nmarket's ID keys this value in the message.",
        "properties": {
          "bbo": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/BboSnapshot",
                "description": "The bbo channel's state, present when subscribed."
              }
            ]
          },
          "book": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/BookSnapshot",
                "description": "The book channel's state, present when subscribed."
              }
            ]
          },
          "eventId": {
            "description": "ID of the event that owns the market.",
            "format": "uuid",
            "type": "string"
          },
          "lifecycle": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/LifecycleSnapshot",
                "description": "The lifecycle channel's state, present when subscribed."
              }
            ]
          },
          "trades": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/TradesSnapshot",
                "description": "The trades channel's state, present when subscribed."
              }
            ]
          }
        },
        "required": [
          "eventId"
        ],
        "type": "object"
      },
      "MarketStatus": {
        "description": "Lifecycle status of a market.",
        "enum": [
          "OPEN",
          "CLOSED",
          "SETTLED"
        ],
        "type": "string"
      },
      "Method": {
        "description": "A request's operation: the single top-level key names the method, its value\nis the payload.",
        "oneOf": [
          {
            "description": "Set the channel for each named market or event, and the private set.",
            "properties": {
              "subscribe": {
                "$ref": "#/components/schemas/SubscriptionMap",
                "description": "Set the channel for each named market or event, and the private set."
              }
            },
            "required": [
              "subscribe"
            ],
            "type": "object"
          },
          {
            "description": "Drop each named market, event, or `PRIVATE`.",
            "properties": {
              "unsubscribe": {
                "description": "Drop each named market, event, or `PRIVATE`.",
                "items": {
                  "$ref": "#/components/schemas/SubscriptionId"
                },
                "type": "array",
                "uniqueItems": true
              }
            },
            "required": [
              "unsubscribe"
            ],
            "type": "object"
          },
          {
            "description": "Read current state and seq without changing the subscription set.",
            "properties": {
              "snapshot": {
                "$ref": "#/components/schemas/SubscriptionMap",
                "description": "Read current state and seq without changing the subscription set."
              }
            },
            "required": [
              "snapshot"
            ],
            "type": "object"
          },
          {
            "description": "Read the connection's current subscriptions.",
            "properties": {
              "status": {
                "description": "Read the connection's current subscriptions.",
                "type": "object"
              }
            },
            "required": [
              "status"
            ],
            "type": "object"
          },
          {
            "description": "List the markets open for trading. Replies with one page under\n`markets` and changes no subscription.",
            "properties": {
              "query_markets": {
                "$ref": "#/components/schemas/CatalogQuery_MarketFilter",
                "description": "List the markets open for trading. Replies with one page under\n`markets` and changes no subscription."
              }
            },
            "required": [
              "query_markets"
            ],
            "type": "object"
          },
          {
            "description": "List the events open for trading. Replies with one page under\n`events` and changes no subscription.",
            "properties": {
              "query_events": {
                "$ref": "#/components/schemas/CatalogQuery_EventFilter",
                "description": "List the events open for trading. Replies with one page under\n`events` and changes no subscription."
              }
            },
            "required": [
              "query_events"
            ],
            "type": "object"
          },
          {
            "description": "Place an array of orders, all or none. A single order is an array of one.\nReplies under `placed`.",
            "properties": {
              "place": {
                "$ref": "#/components/schemas/BatchPlace",
                "description": "Place an array of orders, all or none. A single order is an array of one.\nReplies under `placed`."
              }
            },
            "required": [
              "place"
            ],
            "type": "object"
          },
          {
            "description": "Cancel an array of resting orders by id. An unknown id does not stop the\nrest. Replies under `canceled`.",
            "properties": {
              "cancel": {
                "$ref": "#/components/schemas/BatchCancel",
                "description": "Cancel an array of resting orders by id. An unknown id does not stop the\nrest. Replies under `canceled`."
              }
            },
            "required": [
              "cancel"
            ],
            "type": "object"
          },
          {
            "description": "Cancel this key's resting orders, narrowed by `market`, `event`, or\n`outcome`. Replies under `canceled_all`.",
            "properties": {
              "cancel_all": {
                "$ref": "#/components/schemas/CancelFilter",
                "description": "Cancel this key's resting orders, narrowed by `market`, `event`, or\n`outcome`. Replies under `canceled_all`."
              }
            },
            "required": [
              "cancel_all"
            ],
            "type": "object"
          }
        ]
      },
      "Nonce": {
        "description": "A client-chosen integer that identifies one request. Nonces start at 1 and\nmust increase within a connection; gaps are allowed. The reply echoes the\nnonce. The maximum is 2^53 - 1, the largest exact integer in a JavaScript\n`number`.",
        "format": "int64",
        "minimum": 0,
        "type": "integer"
      },
      "NotCanceled": {
        "properties": {
          "orderId": {
            "format": "uuid",
            "type": "string"
          },
          "reason": {
            "$ref": "#/components/schemas/NotCanceledReason",
            "description": "Why this ID was not cancelled."
          }
        },
        "required": [
          "orderId",
          "reason"
        ],
        "type": "object"
      },
      "NotCanceledReason": {
        "description": "Why an ID in a batch cancel was not cancelled.",
        "enum": [
          "FILLED",
          "CANCELED",
          "NOT_FOUND"
        ],
        "type": "string"
      },
      "OpenMarketResponse": {
        "description": "A market in the open set: enough to pick an outcome and place an order.",
        "properties": {
          "description": {
            "description": "Human-readable. Use it for discovery, never as a contract.",
            "type": "string"
          },
          "eventId": {
            "description": "ID of the event the market belongs to.",
            "format": "uuid",
            "type": "string"
          },
          "fee": {
            "$ref": "#/components/schemas/MarketFee",
            "description": "What a taker pays on this market. Read it per market. Never derive it from a league list."
          },
          "marketId": {
            "description": "Market ID.",
            "format": "uuid",
            "type": "string"
          },
          "marketType": {
            "description": "A canonical name from `GET /v3/types/markets`.",
            "example": "MONEY",
            "type": "string"
          },
          "outcomes": {
            "description": "Outcomes of the market.",
            "items": {
              "$ref": "#/components/schemas/Outcome"
            },
            "type": "array"
          },
          "startsTs": {
            "description": "Unix milliseconds.",
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/MarketStatus",
            "description": "Lifecycle status of a market."
          },
          "strike": {
            "description": "The line the market settles against, a decimal string. A spread's line is the home side's\nhandicap. Absent on a market without a line.",
            "example": "-3.5",
            "type": "string"
          },
          "voids": {
            "$ref": "#/components/schemas/Voidability",
            "description": "How a market settles if it voids. `PUSH` refunds every fill at its cost. `FMV` settles every\noutcome at its fair market value. The exchange never pushes an `FMV` market. Do not assume a\nrefund."
          }
        },
        "required": [
          "marketId",
          "description",
          "eventId",
          "marketType",
          "status",
          "voids",
          "startsTs",
          "fee",
          "outcomes"
        ],
        "type": "object"
      },
      "OpenOrder": {
        "description": "One of the caller's orders resting on the book.",
        "properties": {
          "clientId": {
            "description": "Echoed when the placement sent one.",
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "expiresAt": {
            "description": "Absent for an order that does not expire.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "marketId": {
            "format": "uuid",
            "type": "string"
          },
          "orderId": {
            "format": "uuid",
            "type": "string"
          },
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "price": {
            "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
            "type": "string"
          },
          "qty": {
            "description": "Number of contracts. A winning contract pays full value, 1¢.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "tif": {
            "$ref": "#/components/schemas/TimeInForce"
          }
        },
        "required": [
          "orderId",
          "marketId",
          "outcomeId",
          "price",
          "qty",
          "tif"
        ],
        "type": "object"
      },
      "OpenSubaccount": {
        "additionalProperties": false,
        "description": "Opens the subaccount, its wallet, and its one `trading` key in one transaction.",
        "properties": {
          "algorithm": {
            "$ref": "#/components/schemas/Algorithm"
          },
          "expiresAt": {
            "format": "date-time",
            "type": "string"
          },
          "label": {
            "maxLength": 64,
            "type": "string"
          },
          "publicKey": {
            "description": "The SPKI PEM of a fresh keypair.",
            "type": "string"
          }
        },
        "required": [
          "label",
          "publicKey",
          "algorithm"
        ],
        "type": "object"
      },
      "Order": {
        "properties": {
          "clientId": {
            "description": "Echoed when sent.",
            "format": "uuid",
            "type": "string"
          },
          "createdTs": {
            "description": "Unix milliseconds.",
            "format": "int64",
            "type": "integer"
          },
          "expiresTs": {
            "description": "Absent for an order that does not expire.",
            "format": "int64",
            "type": "integer"
          },
          "marketId": {
            "format": "uuid",
            "type": "string"
          },
          "orderId": {
            "format": "uuid",
            "type": "string"
          },
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "price": {
            "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
            "type": "string"
          },
          "qty": {
            "description": "Number of contracts. A winning contract pays full value, 1¢.",
            "format": "int32",
            "minimum": 1,
            "type": "integer"
          },
          "remaining": {
            "description": "Contracts still resting.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "tif": {
            "$ref": "#/components/schemas/TimeInForce"
          }
        },
        "required": [
          "orderId",
          "marketId",
          "outcomeId",
          "price",
          "qty",
          "remaining",
          "tif",
          "status",
          "createdTs"
        ],
        "type": "object"
      },
      "OrderAccepted": {
        "description": "Accepted and queued, not resting. The order rests when the `open` event arrives on the private stream.",
        "properties": {
          "clientId": {
            "description": "Echoed when sent.",
            "format": "uuid",
            "type": "string"
          },
          "orderId": {
            "format": "uuid",
            "type": "string"
          }
        },
        "required": [
          "orderId"
        ],
        "type": "object"
      },
      "OrderEvent": {
        "description": "One change to one of the caller's orders, tagged by `kind`.",
        "oneOf": [
          {
            "allOf": [
              {
                "$ref": "#/components/schemas/OpenOrder",
                "description": "An order rests on the book."
              },
              {
                "properties": {
                  "kind": {
                    "enum": [
                      "open"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "kind"
                ],
                "type": "object"
              }
            ],
            "description": "An order rests on the book."
          },
          {
            "description": "A fill against one of the caller's orders. Fees are excluded.",
            "properties": {
              "clientId": {
                "description": "The order's client id, when it sent one.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "kind": {
                "enum": [
                  "fill"
                ],
                "type": "string"
              },
              "orderId": {
                "description": "ID of the filled order.",
                "format": "uuid",
                "type": "string"
              },
              "outcomeId": {
                "description": "ID of the outcome that traded.",
                "format": "uuid",
                "type": "string"
              },
              "price": {
                "description": "Fill price: a decimal string between 0 and 1.",
                "type": "string"
              },
              "qty": {
                "description": "Quantity filled, in the currency's smallest unit.",
                "format": "int32",
                "minimum": 0,
                "type": "integer"
              },
              "remaining": {
                "description": "Quantity still resting after the fill.",
                "format": "int32",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "orderId",
              "outcomeId",
              "price",
              "qty",
              "remaining",
              "kind"
            ],
            "type": "object"
          },
          {
            "description": "An order left the book.",
            "properties": {
              "kind": {
                "enum": [
                  "cancel"
                ],
                "type": "string"
              },
              "orderId": {
                "description": "ID of the cancelled order.",
                "format": "uuid",
                "type": "string"
              },
              "reason": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CancelReason",
                    "description": "Why it left. Absent for an ordinary cancel by the owner, by expiry,\nor by an admin."
                  }
                ]
              }
            },
            "required": [
              "orderId",
              "kind"
            ],
            "type": "object"
          },
          {
            "description": "The engine rejected an order after the gateway accepted it.",
            "properties": {
              "kind": {
                "enum": [
                  "reject"
                ],
                "type": "string"
              },
              "orderId": {
                "description": "ID of the rejected order.",
                "format": "uuid",
                "type": "string"
              }
            },
            "required": [
              "orderId",
              "kind"
            ],
            "type": "object"
          }
        ]
      },
      "OrderStatus": {
        "description": "A partly filled order stays `OPEN`. Track `remaining`, not the status.",
        "enum": [
          "PENDING",
          "OPEN",
          "FILLED",
          "CANCELED",
          "REJECTED"
        ],
        "type": "string"
      },
      "OrdersSnapshot": {
        "description": "The caller's resting orders at a sequence number.",
        "properties": {
          "open": {
            "items": {
              "$ref": "#/components/schemas/OpenOrder"
            },
            "type": "array"
          },
          "seq": {
            "description": "The seq of the last order event before this snapshot. `0` means no event yet.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "open"
        ],
        "type": "object"
      },
      "Outcome": {
        "description": "One outcome of a market: what an order names.",
        "properties": {
          "name": {
            "description": "Display name. Not a contract.",
            "type": "string"
          },
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "status": {
            "description": "`TBD`, `WIN`, `LOSS`, `PUSH`, **or a decimal price string** between `\"0.000\"` and\n`\"1.000\"` when the market settles at fair market value. Not an enum. Branch on the four\nkeywords. Treat anything else as a price.",
            "type": "string"
          }
        },
        "required": [
          "outcomeId",
          "name",
          "status"
        ],
        "type": "object"
      },
      "PageCursor": {
        "description": "Where a page starts, and how many items it holds.",
        "properties": {
          "after": {
            "description": "The `next` cursor from the previous page. Opaque.",
            "type": [
              "string",
              "null"
            ]
          },
          "limit": {
            "default": 500,
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "format": "int32",
            "maximum": 5000,
            "minimum": 1,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "PageCursorResponse": {
        "description": "The link to the next page of a listing.",
        "properties": {
          "next": {
            "description": "Pass it as `after` for the next page. Opaque: do not build or parse one. Absent on the last page.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Page_AccountTransfer": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Transfer"
                    },
                    {
                      "properties": {
                        "subaccountKeyId": {
                          "description": "The subaccount's oldest live key: the `{keyId}` every subaccount route takes.",
                          "format": "uuid",
                          "type": "string"
                        }
                      },
                      "required": [
                        "subaccountKeyId"
                      ],
                      "type": "object"
                    }
                  ],
                  "description": "A transfer, and the address of the subaccount it moved funds to or from."
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_EventResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "description": "An event in the open set: the contest its markets settle on.",
                  "properties": {
                    "description": {
                      "description": "Human-readable description of the event.",
                      "type": "string"
                    },
                    "eventId": {
                      "description": "Event ID.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "league": {
                      "description": "A canonical name from `GET /v3/types/leagues`. One league belongs to one sport.",
                      "example": "NFL",
                      "type": "string"
                    },
                    "sport": {
                      "description": "A canonical name from `GET /v3/types/sports`. One league belongs to one sport. An\nunrecognized league reports its raw name here too.",
                      "example": "FOOTBALL",
                      "type": "string"
                    },
                    "startsTs": {
                      "description": "Unix milliseconds.",
                      "format": "int64",
                      "type": "integer"
                    },
                    "status": {
                      "$ref": "#/components/schemas/EventStatus",
                      "description": "Lifecycle status of an event. `OPEN_INGAME` charges the taker on every `WHEN_LIVE` market.\nThe move to `OPEN_INGAME` voids every resting order on the event's markets."
                    }
                  },
                  "required": [
                    "eventId",
                    "description",
                    "sport",
                    "league",
                    "status",
                    "startsTs"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_Fill": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "properties": {
                    "clientId": {
                      "description": "The order's `clientId`, when it had one.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "cost": {
                      "description": "What this side paid for the contracts, in dollars: `qty × price × 1¢`. The price it traded at is `100 × cost / qty`. That price can be better than your limit.",
                      "type": "string"
                    },
                    "fee": {
                      "description": "Absent on an uncharged fill.",
                      "type": "string"
                    },
                    "fillId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "marketId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "orderId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "outcomeId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "qty": {
                      "description": "Number of contracts. A winning contract pays full value, 1¢.",
                      "format": "int32",
                      "minimum": 1,
                      "type": "integer"
                    },
                    "taker": {
                      "type": "boolean"
                    },
                    "ts": {
                      "description": "Unix milliseconds.",
                      "format": "int64",
                      "type": "integer"
                    }
                  },
                  "required": [
                    "fillId",
                    "orderId",
                    "marketId",
                    "outcomeId",
                    "qty",
                    "cost",
                    "taker",
                    "ts"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_HistoricalEventResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "description": "An event of any age and status.",
                  "properties": {
                    "description": {
                      "description": "Human-readable description of the event.",
                      "type": "string"
                    },
                    "eventId": {
                      "description": "Event ID.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "league": {
                      "description": "A canonical name from `GET /v3/types/leagues`. Absent on an event with no league.",
                      "example": "NFL",
                      "type": "string"
                    },
                    "sport": {
                      "description": "A canonical name from `GET /v3/types/sports`. An unrecognized league reports its raw name\nhere too. Absent on an event with no league.",
                      "example": "FOOTBALL",
                      "type": "string"
                    },
                    "startsTs": {
                      "description": "Unix milliseconds. Absent on an event with no scheduled start.",
                      "format": "int64",
                      "type": "integer"
                    },
                    "status": {
                      "$ref": "#/components/schemas/EventStatus",
                      "description": "Lifecycle status of an event. `OPEN_INGAME` charges the taker on every `WHEN_LIVE` market.\nThe move to `OPEN_INGAME` voids every resting order on the event's markets."
                    }
                  },
                  "required": [
                    "eventId",
                    "description",
                    "status"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_HistoricalMarketResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "description": "A market of any age and status.",
                  "properties": {
                    "description": {
                      "description": "Human-readable. Use it for discovery, never as a contract.",
                      "type": "string"
                    },
                    "eventId": {
                      "description": "ID of the event the market belongs to.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "fee": {
                      "$ref": "#/components/schemas/MarketFee",
                      "description": "What a taker pays on this market. Read it per market. Never derive it from a league list."
                    },
                    "marketId": {
                      "description": "Market ID.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "marketType": {
                      "description": "A canonical name from `GET /v3/types/markets`.",
                      "example": "MONEY",
                      "type": "string"
                    },
                    "outcomes": {
                      "description": "Outcomes of the market. A graded outcome carries its grade in `status`.",
                      "items": {
                        "$ref": "#/components/schemas/Outcome"
                      },
                      "type": "array"
                    },
                    "settledTs": {
                      "description": "Unix milliseconds of the most recent settlement. A re-settlement updates it. Absent on an\nunsettled market.",
                      "example": 1756512000000,
                      "format": "int64",
                      "type": "integer"
                    },
                    "status": {
                      "$ref": "#/components/schemas/MarketStatus",
                      "description": "Lifecycle status of a market."
                    },
                    "strike": {
                      "description": "The line the market settles against, a decimal string. A spread's line is the home side's\nhandicap. Absent on a market without a line.",
                      "example": "-3.5",
                      "type": "string"
                    },
                    "voids": {
                      "$ref": "#/components/schemas/Voidability",
                      "description": "How a market settles if it voids. `PUSH` refunds every fill at its cost. `FMV` settles every\noutcome at its fair market value. The exchange never pushes an `FMV` market. Do not assume a\nrefund."
                    }
                  },
                  "required": [
                    "marketId",
                    "description",
                    "eventId",
                    "marketType",
                    "status",
                    "voids",
                    "fee",
                    "outcomes"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_HistoricalPositionResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "description": "A position of the caller, with the status of its market and the grade of its outcome.",
                  "properties": {
                    "cost": {
                      "description": "Net dollars the wallet paid for them. The average entry price is `100 × cost / qty`.",
                      "type": "string"
                    },
                    "marketId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "outcomeId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "payout": {
                      "description": "What the contracts are worth at `result`, in dollars. Absent while `result` is `TBD`.",
                      "type": "string"
                    },
                    "pnl": {
                      "description": "`payout` minus `cost`, in dollars. Absent while `result` is `TBD`.",
                      "type": "string"
                    },
                    "positionId": {
                      "description": "Position ID.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "qty": {
                      "description": "Contracts held.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    },
                    "resettled": {
                      "description": "`true` if the market was graded again after it settled.",
                      "type": "boolean"
                    },
                    "result": {
                      "description": "The grade of the outcome. It's `TBD` until the market settles.",
                      "type": "string"
                    },
                    "settledTs": {
                      "description": "When the market settled. Absent until the market settles.",
                      "format": "int64",
                      "type": "integer"
                    },
                    "status": {
                      "$ref": "#/components/schemas/MarketStatus",
                      "description": "Lifecycle status of a market."
                    }
                  },
                  "required": [
                    "positionId",
                    "marketId",
                    "outcomeId",
                    "qty",
                    "cost",
                    "status",
                    "result",
                    "resettled"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_OpenMarketResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "description": "A market in the open set: enough to pick an outcome and place an order.",
                  "properties": {
                    "description": {
                      "description": "Human-readable. Use it for discovery, never as a contract.",
                      "type": "string"
                    },
                    "eventId": {
                      "description": "ID of the event the market belongs to.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "fee": {
                      "$ref": "#/components/schemas/MarketFee",
                      "description": "What a taker pays on this market. Read it per market. Never derive it from a league list."
                    },
                    "marketId": {
                      "description": "Market ID.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "marketType": {
                      "description": "A canonical name from `GET /v3/types/markets`.",
                      "example": "MONEY",
                      "type": "string"
                    },
                    "outcomes": {
                      "description": "Outcomes of the market.",
                      "items": {
                        "$ref": "#/components/schemas/Outcome"
                      },
                      "type": "array"
                    },
                    "startsTs": {
                      "description": "Unix milliseconds.",
                      "format": "int64",
                      "type": "integer"
                    },
                    "status": {
                      "$ref": "#/components/schemas/MarketStatus",
                      "description": "Lifecycle status of a market."
                    },
                    "strike": {
                      "description": "The line the market settles against, a decimal string. A spread's line is the home side's\nhandicap. Absent on a market without a line.",
                      "example": "-3.5",
                      "type": "string"
                    },
                    "voids": {
                      "$ref": "#/components/schemas/Voidability",
                      "description": "How a market settles if it voids. `PUSH` refunds every fill at its cost. `FMV` settles every\noutcome at its fair market value. The exchange never pushes an `FMV` market. Do not assume a\nrefund."
                    }
                  },
                  "required": [
                    "marketId",
                    "description",
                    "eventId",
                    "marketType",
                    "status",
                    "voids",
                    "startsTs",
                    "fee",
                    "outcomes"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_Order": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "properties": {
                    "clientId": {
                      "description": "Echoed when sent.",
                      "format": "uuid",
                      "type": "string"
                    },
                    "createdTs": {
                      "description": "Unix milliseconds.",
                      "format": "int64",
                      "type": "integer"
                    },
                    "expiresTs": {
                      "description": "Absent for an order that does not expire.",
                      "format": "int64",
                      "type": "integer"
                    },
                    "marketId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "orderId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "outcomeId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "price": {
                      "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
                      "type": "string"
                    },
                    "qty": {
                      "description": "Number of contracts. A winning contract pays full value, 1¢.",
                      "format": "int32",
                      "minimum": 1,
                      "type": "integer"
                    },
                    "remaining": {
                      "description": "Contracts still resting.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    },
                    "status": {
                      "$ref": "#/components/schemas/OrderStatus"
                    },
                    "tif": {
                      "$ref": "#/components/schemas/TimeInForce"
                    }
                  },
                  "required": [
                    "orderId",
                    "marketId",
                    "outcomeId",
                    "price",
                    "qty",
                    "remaining",
                    "tif",
                    "status",
                    "createdTs"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_Trade": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "description": "One execution on the public tape.",
                  "properties": {
                    "outcomeId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "price": {
                      "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
                      "type": "string"
                    },
                    "qty": {
                      "description": "Number of contracts. A winning contract pays full value, 1¢.",
                      "format": "int32",
                      "minimum": 0,
                      "type": "integer"
                    },
                    "tradeId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "ts": {
                      "description": "Unix milliseconds.",
                      "format": "int64",
                      "type": "integer"
                    }
                  },
                  "required": [
                    "tradeId",
                    "outcomeId",
                    "price",
                    "qty",
                    "ts"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "Page_Transaction": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageCursorResponse"
          },
          {
            "properties": {
              "items": {
                "items": {
                  "properties": {
                    "amount": {
                      "description": "Signed. A negative amount debits the wallet.",
                      "type": "string"
                    },
                    "kind": {
                      "$ref": "#/components/schemas/TransactionKind"
                    },
                    "ref": {
                      "description": "The account on the other end of this row.",
                      "format": "uuid",
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "transactionId": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "ts": {
                      "description": "Unix milliseconds.",
                      "format": "int64",
                      "type": "integer"
                    }
                  },
                  "required": [
                    "transactionId",
                    "kind",
                    "amount",
                    "ts"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "items"
            ],
            "type": "object"
          }
        ],
        "description": "One page of a listing: the items in the listing's order, then the link to\nthe next page. Every paged route answers this shape."
      },
      "PlaceOrder": {
        "additionalProperties": false,
        "properties": {
          "clientId": {
            "description": "Your label for the order. Echoed on the answer, on `open`, and on every `fill`. The exchange never checks it for uniqueness, so a replay places a second order.",
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "outcomeId": {
            "description": "The side you are buying. Never a market ID.",
            "format": "uuid",
            "type": "string"
          },
          "price": {
            "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
            "example": "0.667",
            "type": "string"
          },
          "qty": {
            "description": "Number of contracts. A winning contract pays full value, 1¢.",
            "example": 110,
            "format": "int32",
            "minimum": 1,
            "type": "integer"
          },
          "tif": {
            "$ref": "#/components/schemas/TimeInForce"
          },
          "ttl": {
            "description": "Milliseconds. Required for `GTT`, optional for `PO`, forbidden otherwise.",
            "format": "int64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "outcomeId",
          "price",
          "qty",
          "tif"
        ],
        "type": "object"
      },
      "Position": {
        "description": "The contracts held and what they cost. Nothing here is derived.",
        "properties": {
          "cost": {
            "description": "Net dollars the wallet paid for them. The average entry price is `100 × cost / qty`.",
            "type": "string"
          },
          "marketId": {
            "format": "uuid",
            "type": "string"
          },
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "qty": {
            "description": "Contracts held.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "marketId",
          "outcomeId",
          "qty",
          "cost"
        ],
        "type": "object"
      },
      "PositionsSnapshot": {
        "description": "The caller's nonzero positions at a sequence number. A later delta with `qty` `0` reports a close.",
        "properties": {
          "positions": {
            "items": {
              "$ref": "#/components/schemas/Position"
            },
            "type": "array"
          },
          "seq": {
            "description": "The seq of the last position event before this snapshot. `0` means no event yet.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "seq",
          "positions"
        ],
        "type": "object"
      },
      "PrivateChannel": {
        "description": "A private feed under the `PRIVATE` subscription. A client names any set of\nthese; they are independent.",
        "enum": [
          "orders",
          "positions"
        ],
        "type": "string"
      },
      "RemoveReason": {
        "description": "Why an order left the book.",
        "enum": [
          "fill",
          "cancel"
        ],
        "type": "string"
      },
      "RestingOrder": {
        "description": "One order resting on the book.",
        "properties": {
          "orderId": {
            "format": "uuid",
            "type": "string"
          },
          "price": {
            "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
            "type": "string"
          },
          "qty": {
            "description": "Number of contracts. A winning contract pays full value, 1¢.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "orderId",
          "price",
          "qty"
        ],
        "type": "object"
      },
      "Status": {
        "description": "The connection's current subscriptions.",
        "properties": {
          "subscriptions": {
            "$ref": "#/components/schemas/SubscriptionMap",
            "description": "One channel per subscription ID. A market named directly and through\nits event receives the channels of both rows, delivered once per tick."
          }
        },
        "required": [
          "subscriptions"
        ],
        "type": "object"
      },
      "Subaccount": {
        "description": "One row per live key. A subaccount opened through the API has one live key, so the listing reads one row per subaccount.",
        "properties": {
          "balance": {
            "description": "A decimal string with exactly five decimal places.",
            "example": "1234.50000",
            "type": "string"
          },
          "keyId": {
            "description": "The trading key that addresses this subaccount.",
            "format": "uuid",
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        },
        "required": [
          "keyId",
          "label",
          "balance"
        ],
        "type": "object"
      },
      "SubaccountKeyScope": {
        "description": "The two scopes a subaccount mint names.",
        "enum": [
          "trading",
          "trading::read"
        ],
        "type": "string"
      },
      "SubscriptionId": {
        "description": "`market:<id>`, `event:<id>`, or `PRIVATE`.",
        "type": "string"
      },
      "SubscriptionMap": {
        "additionalProperties": false,
        "description": "The channels a request subscribes to. `markets` and `events` each map an id\nto a market channel. `private` is a set of private channels. `depth` is\nthe number of price levels in each `book` snapshot of the reply.",
        "properties": {
          "depth": {
            "description": "Number of price levels per outcome in each `book` snapshot of the reply,\nbest price first. 1 to 20. Defaults to 20. Deltas are not limited by depth.",
            "example": 20,
            "format": "int32",
            "maximum": 20,
            "minimum": 1,
            "type": [
              "integer",
              "null"
            ]
          },
          "events": {
            "additionalProperties": {
              "$ref": "#/components/schemas/MarketChannel"
            },
            "propertyNames": {
              "format": "uuid",
              "type": "string"
            },
            "type": "object"
          },
          "markets": {
            "additionalProperties": {
              "$ref": "#/components/schemas/MarketChannel"
            },
            "propertyNames": {
              "format": "uuid",
              "type": "string"
            },
            "type": "object"
          },
          "private": {
            "items": {
              "$ref": "#/components/schemas/PrivateChannel"
            },
            "type": "array",
            "uniqueItems": true
          }
        },
        "type": "object"
      },
      "TimeInForce": {
        "description": "`ttl` is required for `GTT`, optional for `PO`, forbidden otherwise.",
        "enum": [
          "GTC",
          "GTT",
          "IOC",
          "FOK",
          "PO"
        ],
        "type": "string"
      },
      "Trade": {
        "description": "One execution on the public tape.",
        "properties": {
          "outcomeId": {
            "format": "uuid",
            "type": "string"
          },
          "price": {
            "description": "Decimal probability on the submittable grid, as a string. Three bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange refuses a price off the grid with `INVALID_PRICE`.",
            "type": "string"
          },
          "qty": {
            "description": "Number of contracts. A winning contract pays full value, 1¢.",
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "tradeId": {
            "format": "uuid",
            "type": "string"
          },
          "ts": {
            "description": "Unix milliseconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "tradeId",
          "outcomeId",
          "price",
          "qty",
          "ts"
        ],
        "type": "object"
      },
      "TradesSnapshot": {
        "description": "The trades channel's part of a snapshot: the last [`TRADE_REPLAY_SEQS`]\nbatches, oldest first. Older trades are not included.",
        "properties": {
          "seq": {
            "description": "The seq of the last trades batch. The next trades delta follows it.\n`0` means no batch exists yet.",
            "format": "int64",
            "minimum": 0,
            "type": "integer"
          },
          "trades": {
            "description": "The retained batches, oldest first.",
            "items": {
              "$ref": "#/components/schemas/Batch_Trade"
            },
            "type": "array"
          }
        },
        "required": [
          "seq",
          "trades"
        ],
        "type": "object"
      },
      "Transaction": {
        "properties": {
          "amount": {
            "description": "Signed. A negative amount debits the wallet.",
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/TransactionKind"
          },
          "ref": {
            "description": "The account on the other end of this row.",
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "transactionId": {
            "format": "uuid",
            "type": "string"
          },
          "ts": {
            "description": "Unix milliseconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "transactionId",
          "kind",
          "amount",
          "ts"
        ],
        "type": "object"
      },
      "TransactionKind": {
        "description": "What moved the balance.",
        "enum": [
          "FILL",
          "FEE",
          "MAKER_CREDIT",
          "SETTLEMENT",
          "TRANSFER_IN",
          "TRANSFER_OUT",
          "DEPOSIT",
          "WITHDRAWAL",
          "CREDIT",
          "CREDIT_RECLAIM",
          "ADJUSTMENT",
          "FORWARDING"
        ],
        "type": "string"
      },
      "Transfer": {
        "properties": {
          "actualBalance": {
            "description": "The balance the exchange found. Set only on a rejection.",
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "description": "A decimal string with exactly five decimal places.",
            "example": "1234.50000",
            "type": "string"
          },
          "clientTransferId": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "direction": {
            "$ref": "#/components/schemas/TransferDirection"
          },
          "status": {
            "$ref": "#/components/schemas/TransferStatus"
          },
          "transferId": {
            "format": "uuid",
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "transferId",
          "status",
          "direction",
          "amount",
          "actualBalance",
          "createdAt",
          "updatedAt"
        ],
        "type": "object"
      },
      "TransferDirection": {
        "description": "Named from the subaccount's point of view.",
        "enum": [
          "fund",
          "defund"
        ],
        "type": "string"
      },
      "TransferRequest": {
        "additionalProperties": false,
        "properties": {
          "amount": {
            "description": "Positive, at most five decimal places. Never rounded.",
            "example": "1234.50000",
            "type": "string"
          },
          "clientTransferId": {
            "description": "Your idempotency key. Unique per trader. Send one.",
            "maxLength": 64,
            "type": "string"
          },
          "direction": {
            "$ref": "#/components/schemas/TransferDirection"
          }
        },
        "required": [
          "direction",
          "amount"
        ],
        "type": "object"
      },
      "TransferStatus": {
        "description": "The route writes only `Requested`. The exchange then moves it to exactly one terminal state.",
        "enum": [
          "Requested",
          "Applied",
          "Rejected"
        ],
        "type": "string"
      },
      "Voidability": {
        "description": "How a market settles if it voids. `PUSH` refunds every fill at its cost. `FMV` settles every outcome at its fair market value. The exchange never pushes an `FMV` market. Do not assume a refund.",
        "enum": [
          "PUSH",
          "FMV"
        ],
        "type": "string"
      },
      "WalletBalance": {
        "description": "Your primary wallet's balance. It carries no key id, because no key addresses that wallet.",
        "properties": {
          "balance": {
            "description": "A decimal string with exactly five decimal places.",
            "example": "1234.50000",
            "type": "string"
          }
        },
        "required": [
          "balance"
        ],
        "type": "object"
      },
      "WsDelta": {
        "description": "A delta message: changes to the subscribed markets, keyed by market ID.\nChanges that happen together arrive in one message. A market that opens\nafter an event subscription arrives with no snapshot. Its first delta is the\n`open` lifecycle transition, and its book and trades start empty at seq 0.",
        "properties": {
          "delta": {
            "additionalProperties": {
              "$ref": "#/components/schemas/MarketDelta"
            },
            "description": "The changed markets the connection subscribes to. Empty on a\nprivate-only message.",
            "propertyNames": {
              "format": "uuid",
              "type": "string"
            },
            "type": "object"
          },
          "orders": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Batch_OrderEvent",
                "description": "The caller's order events this tick, when subscribed to `orders`."
              }
            ]
          },
          "positions": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Batch_Position",
                "description": "The caller's position changes this tick, when subscribed to `positions`."
              }
            ]
          },
          "ts": {
            "description": "Time the matching engine emitted the change, in epoch milliseconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "ts",
          "delta"
        ],
        "type": "object"
      },
      "WsHeartbeat": {
        "description": "A keepalive with the caller's last seq on each subscribed PRIVATE channel,\nsent once per interval.",
        "properties": {
          "heartbeat": {
            "$ref": "#/components/schemas/HeartbeatSeqs",
            "description": "The last seq per subscribed channel."
          }
        },
        "required": [
          "heartbeat"
        ],
        "type": "object"
      },
      "WsSnapshot": {
        "description": "A snapshot message: current state and seq per market, keyed by market ID.\nThe server sends one in reply to a subscribe. The first delta on each\nchannel follows the stated seq with no gap.",
        "properties": {
          "nonce": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Nonce",
                "description": "The nonce of the request this replies to."
              }
            ]
          },
          "orders": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/OrdersSnapshot",
                "description": "The caller's open orders and seq, when `PRIVATE` `orders` is subscribed."
              }
            ]
          },
          "positions": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PositionsSnapshot",
                "description": "The caller's open positions and seq, when `PRIVATE` `positions` is\nsubscribed."
              }
            ]
          },
          "snapshot": {
            "additionalProperties": {
              "$ref": "#/components/schemas/MarketSnapshot"
            },
            "description": "The snapshotted markets. Empty on a private-only reply.",
            "propertyNames": {
              "format": "uuid",
              "type": "string"
            },
            "type": "object"
          },
          "subscribed": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/SubscriptionMap",
                "description": "The subscriptions now active. Present only on the reply to a subscribe."
              }
            ]
          },
          "ts": {
            "description": "Time the snapshot was taken, in epoch milliseconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "ts",
          "snapshot"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "keyId": {
        "description": "The key's UUID.",
        "in": "header",
        "name": "Novig-Key-Id",
        "type": "apiKey"
      },
      "signature": {
        "description": "Standard padded base64 of the NOVIG-V3 signature.",
        "in": "header",
        "name": "Novig-Signature",
        "type": "apiKey"
      },
      "timestamp": {
        "description": "Unix milliseconds. ±30 s.",
        "in": "header",
        "name": "Novig-Timestamp",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "contact": {
      "email": "tech@novig.com",
      "name": "Contact",
      "url": "https://novig.com"
    },
    "description": "Place orders over signed REST. Watch the book and your fills on one websocket.",
    "title": "Novig API",
    "version": "3.0.0"
  },
  "openapi": "3.1.0",
  "paths": {
    "/v3/account/balance": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nYour primary wallet is the one that funds every subaccount. No key addresses\nit, so the path carries no `{keyId}`.",
        "operationId": "getAccountBalance",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletBalance"
                }
              }
            },
            "description": "The balance, as a five-place decimal string."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "WALLET_NOT_FOUND",
                  "message": "Wallet not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account holds no primary wallet yet."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get the account balance",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/account/balance\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/balance\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/account/balance\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/account/balance\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/orders": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-info\">304</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getAccountOrders",
        "parameters": [
          {
            "description": "A prior response's `ETag`. A match answers `304`.",
            "in": "header",
            "name": "If-None-Match",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrdersSnapshot"
                }
              }
            },
            "description": "Your resting orders and their seq.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "The orders seq matches the `If-None-Match` ETag.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get your resting orders",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/account/orders\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/orders\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/account/orders\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/account/orders\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/positions": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-info\">304</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getAccountPositions",
        "parameters": [
          {
            "description": "A prior response's `ETag`. A match answers `304`.",
            "in": "header",
            "name": "If-None-Match",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PositionsSnapshot"
                }
              }
            },
            "description": "Your positions and their seq.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "The positions seq matches the `If-None-Match` ETag.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get your positions",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/account/positions\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/positions\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/account/positions\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/account/positions\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listSubaccounts",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Subaccount"
                  },
                  "type": "array"
                }
              }
            },
            "description": "One row per live subaccount."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List subaccounts",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/account/subaccounts\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/subaccounts\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/account/subaccounts\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/account/subaccounts\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">201</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `false` |",
        "operationId": "openSubaccount",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "algorithm": "Ed25519",
                "label": "desk-1",
                "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\n-----END PUBLIC KEY-----\n"
              },
              "schema": {
                "$ref": "#/components/schemas/OpenSubaccount"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subaccount"
                }
              }
            },
            "description": "Opened. `keyId` is the new trading key and the subaccount's address."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_BODY",
                  "message": "label must not be empty"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KYC_REQUIRED",
                  "message": "The account must complete KYC verification before it trades."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route, or the trader has not passed KYC. The code says which."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Open a subaccount",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/account/subaccounts\"\nQUERY=\"\"\nBODY='{\n  \"algorithm\": \"Ed25519\",\n  \"label\": \"desk-1\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst BODY: &[u8] = br#\"{\n  \"algorithm\": \"Ed25519\",\n  \"label\": \"desk-1\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/subaccounts\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/account/subaccounts\"\nquery = \"\"\nbody = b\"\"\"{\n  \"algorithm\": \"Ed25519\",\n  \"label\": \"desk-1\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/account/subaccounts\";\nconst query = \"\";\nconst body = `{\n  \"algorithm\": \"Ed25519\",\n  \"label\": \"desk-1\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts/{keyId}": {
      "patch": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "labelSubaccount",
        "parameters": [
          {
            "description": "The subaccount's trading key.",
            "in": "path",
            "name": "keyId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "label": "desk-2"
              },
              "schema": {
                "$ref": "#/components/schemas/LabelSubaccount"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subaccount"
                }
              }
            },
            "description": "Labelled."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_BODY",
                  "message": "label must not be empty"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SUBACCOUNT_NOT_FOUND",
                  "message": "Subaccount not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No subaccount carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Label a subaccount",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nSUB=b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\n\nREQ_PATH=\"/v3/account/subaccounts/$SUB\"\nQUERY=\"\"\nBODY='{\n  \"label\": \"desk-2\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPATCH\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X PATCH \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst SUB: &str = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\nconst BODY: &[u8] = br#\"{\n  \"label\": \"desk-2\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/account/subaccounts/{SUB}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .patch(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nSUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\"\n\npath = f\"/v3/account/subaccounts/{SUB}\"\nquery = \"\"\nbody = b\"\"\"{\n  \"label\": \"desk-2\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"PATCH\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"PATCH\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst SUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nconst path = `/v3/account/subaccounts/${SUB}`;\nconst query = \"\";\nconst body = `{\n  \"label\": \"desk-2\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"PATCH\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"PATCH\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts/{keyId}/balance": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `any` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getBalance",
        "parameters": [
          {
            "description": "A trading key bound to the subaccount. The address.",
            "in": "path",
            "name": "keyId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            },
            "description": "The balance, as a five-place decimal string."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "NOT_A_SUBACCOUNT_KEY",
                  "message": "That key does not reach a subaccount"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "That key does not reach a subaccount."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SUBACCOUNT_NOT_FOUND",
                  "message": "Subaccount not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No subaccount carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get a balance",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nSUB=b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\n\nREQ_PATH=\"/v3/account/subaccounts/$SUB/balance\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst SUB: &str = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\n        \"/v3/account/subaccounts/{SUB}/balance\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nSUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\"\n\npath = f\"/v3/account/subaccounts/{SUB}/balance\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst SUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nconst path = `/v3/account/subaccounts/${SUB}/balance`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts/{keyId}/keys": {
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">201</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-warn\">409</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `false` |\n\nThe body names the scope. While the subaccount's `trading` key is live, this route mints only `trading::read`.",
        "operationId": "createSubaccountKey",
        "parameters": [
          {
            "description": "A key that reaches the subaccount.",
            "in": "path",
            "name": "keyId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "algorithm": "Ed25519",
                "name": "desk-1-reader",
                "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\n-----END PUBLIC KEY-----\n",
                "scope": "trading::read"
              },
              "schema": {
                "$ref": "#/components/schemas/CreateSubaccountKey"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreated"
                }
              }
            },
            "description": "Created. It reaches the same subaccount."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_BODY",
                  "message": "publicKey must not be empty"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KYC_REQUIRED",
                  "message": "The account must complete KYC verification before it trades."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route, or the trader has not passed KYC. The code says which."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SUBACCOUNT_NOT_FOUND",
                  "message": "Subaccount not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No subaccount carries that ID."
          },
          "409": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KEY_EXISTS",
                  "message": "A live key with this public key already exists."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "A live `trading` key already reaches this subaccount, or a live key already carries this public key."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Issue a subaccount key",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nSUB=b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\n\nREQ_PATH=\"/v3/account/subaccounts/$SUB/keys\"\nQUERY=\"\"\nBODY='{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"desk-1-reader\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\",\n  \"scope\": \"trading::read\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst SUB: &str = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\nconst BODY: &[u8] = br#\"{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"desk-1-reader\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\",\n  \"scope\": \"trading::read\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\n        \"/v3/account/subaccounts/{SUB}/keys\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nSUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\"\n\npath = f\"/v3/account/subaccounts/{SUB}/keys\"\nquery = \"\"\nbody = b\"\"\"{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"desk-1-reader\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\",\n  \"scope\": \"trading::read\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst SUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nconst path = `/v3/account/subaccounts/${SUB}/keys`;\nconst query = \"\";\nconst body = `{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"desk-1-reader\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\",\n  \"scope\": \"trading::read\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts/{keyId}/transactions": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `any` |\n| **Throttle** | `history` |\n| **Cost** | `6 + 1 per 100 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nMoney that moved. A transfer appears after the exchange applies it. Read a\npending or rejected transfer with [Get a\ntransfer](/api-reference/accounts/get-a-transfer).",
        "operationId": "listTransactions",
        "parameters": [
          {
            "description": "The subaccount's trading key.",
            "in": "path",
            "name": "keyId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One kind.",
            "in": "query",
            "name": "kind",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TransactionKind"
            }
          },
          {
            "description": "Exclusive lower bound on the row time, in epoch milliseconds.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the row time, in epoch milliseconds.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_Transaction"
                }
              }
            },
            "description": "One page of ledger rows, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "NOT_A_SUBACCOUNT_KEY",
                  "message": "That key does not reach a subaccount"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key reaches no subaccount, or the query is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SUBACCOUNT_NOT_FOUND",
                  "message": "Subaccount not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No subaccount carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List transactions",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nSUB=b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\n\nREQ_PATH=\"/v3/account/subaccounts/$SUB/transactions\"\nQUERY=\"kind=FILL\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst SUB: &str = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\n        \"/v3/account/subaccounts/{SUB}/transactions\");\n    let url = format!(\"{HOST}{path}?kind=FILL\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nSUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\"\n\npath = f\"/v3/account/subaccounts/{SUB}/transactions\"\nquery = \"kind=FILL\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst SUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nconst path =\n  `/v3/account/subaccounts/${SUB}/transactions`;\nconst query = \"kind=FILL\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts/{keyId}/transfer": {
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">202</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAccepted, not settled. Read the verdict with [Get a transfer](/api-reference/accounts/get-a-transfer). An applied transfer also appears in [List transactions](/api-reference/accounts/list-transactions).",
        "operationId": "transfer",
        "parameters": [
          {
            "description": "The subaccount's trading key.",
            "in": "path",
            "name": "keyId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "amount": "1234.50000",
                "clientTransferId": "fund-desk-1-0001",
                "direction": "fund"
              },
              "schema": {
                "$ref": "#/components/schemas/TransferRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                }
              }
            },
            "description": "Accepted and outstanding."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_BODY",
                  "message": "amount must be a positive decimal string with at most 5 decimal places"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KYC_REQUIRED",
                  "message": "The account must complete KYC verification before it trades."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route, or the trader has not passed KYC. The code says which."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SUBACCOUNT_NOT_FOUND",
                  "message": "Subaccount not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No subaccount carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Fund a subaccount",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nSUB=b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\n\nREQ_PATH=\"/v3/account/subaccounts/$SUB/transfer\"\nQUERY=\"\"\nBODY='{\n  \"amount\": \"1234.50000\",\n  \"clientTransferId\": \"fund-desk-1-0001\",\n  \"direction\": \"fund\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst SUB: &str = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\nconst BODY: &[u8] = br#\"{\n  \"amount\": \"1234.50000\",\n  \"clientTransferId\": \"fund-desk-1-0001\",\n  \"direction\": \"fund\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\n        \"/v3/account/subaccounts/{SUB}/transfer\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nSUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\"\n\npath = f\"/v3/account/subaccounts/{SUB}/transfer\"\nquery = \"\"\nbody = b\"\"\"{\n  \"amount\": \"1234.50000\",\n  \"clientTransferId\": \"fund-desk-1-0001\",\n  \"direction\": \"fund\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst SUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\n\nconst path = `/v3/account/subaccounts/${SUB}/transfer`;\nconst query = \"\";\nconst body = `{\n  \"amount\": \"1234.50000\",\n  \"clientTransferId\": \"fund-desk-1-0001\",\n  \"direction\": \"fund\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/subaccounts/{keyId}/transfers/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nOne transfer request and its verdict. A rejected transfer appears here, never in [List transactions](/api-reference/accounts/list-transactions).",
        "operationId": "getTransfer",
        "parameters": [
          {
            "description": "The subaccount's trading key.",
            "in": "path",
            "name": "keyId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Transfer ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                }
              }
            },
            "description": "The transfer and its verdict."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "TRANSFER_NOT_FOUND",
                  "message": "Transfer not found."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No transfer carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get a transfer",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nSUB=b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/account/subaccounts/$SUB/transfers/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst SUB: &str = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\n        \"/v3/account/subaccounts/{SUB}/transfers/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nSUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/account/subaccounts/{SUB}/transfers/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst SUB = \"b7c3a1e9-4d52-4a1f-9c8e-6f0a2b3d5e71\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path =\n  `/v3/account/subaccounts/${SUB}/transfers/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/transactions": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `history` |\n| **Cost** | `6 + 1 per 100 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nEvery movement of your primary wallet's balance. A fund of a subaccount\nreads `TRANSFER_OUT`, and a defund reads `TRANSFER_IN`.",
        "operationId": "listAccountTransactions",
        "parameters": [
          {
            "description": "One kind.",
            "in": "query",
            "name": "kind",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TransactionKind"
            }
          },
          {
            "description": "Exclusive lower bound on the row time, in epoch milliseconds.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the row time, in epoch milliseconds.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_Transaction"
                }
              }
            },
            "description": "One page of ledger rows, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "WALLET_NOT_FOUND",
                  "message": "Wallet not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account holds no primary wallet yet."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List account transactions",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/account/transactions\"\nQUERY=\"kind=FILL\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/transactions\";\n    let url = format!(\"{HOST}{path}?kind=FILL\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/account/transactions\"\nquery = \"kind=FILL\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/account/transactions\";\nconst query = \"kind=FILL\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/transfers": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `history` |\n| **Cost** | `6 + 1 per 100 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nEvery transfer between your primary wallet and your subaccounts, newest\nfirst. `subaccountKeyId` names the subaccount each one moved money to or\nfrom.",
        "operationId": "listAccountTransfers",
        "parameters": [
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_AccountTransfer"
                }
              }
            },
            "description": "One page of transfers, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The query is malformed, or `after` names no transfer of yours."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "WALLET_NOT_FOUND",
                  "message": "Wallet not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account holds no primary wallet yet."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List account transfers",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/account/transfers\"\nQUERY=\"limit=100\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/account/transfers\";\n    let url = format!(\"{HOST}{path}?limit=100\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/account/transfers\"\nquery = \"limit=100\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/account/transfers\";\nconst query = \"limit=100\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/account/transfers/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nOne transfer of yours, whichever subaccount it moved money to or from.",
        "operationId": "getAccountTransfer",
        "parameters": [
          {
            "description": "Transfer ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountTransfer"
                }
              }
            },
            "description": "The transfer, its verdict, and its subaccount's address."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "TRANSFER_NOT_FOUND",
                  "message": "Transfer not found."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No transfer carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get an account transfer",
        "tags": [
          "Accounts"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/account/transfers/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/account/transfers/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/account/transfers/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/account/transfers/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/catalog/events": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/catalog/events`. The edge throttles it per IP.",
        "operationId": "listEvents",
        "parameters": [
          {
            "description": "Comma-separated canonical league names.",
            "example": "NFL,NBA",
            "in": "query",
            "name": "league",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated event statuses.",
            "example": "OPEN_PREGAME",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Exclusive lower bound on the event's scheduled start.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the event's scheduled start.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_EventResponse"
                }
              }
            },
            "description": "One page of events."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List events",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/catalog/events\"\nQUERY=\"league=NFL\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/catalog/events\";\n    let url = format!(\"{HOST}{path}?league=NFL\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/catalog/events\"\nquery = \"league=NFL\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/catalog/events\";\nconst query = \"league=NFL\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/catalog/events/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/catalog/events/{id}`. The edge throttles it per IP.",
        "operationId": "getEvent",
        "parameters": [
          {
            "description": "Event ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventResponse"
                }
              }
            },
            "description": "The event."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "EVENT_NOT_FOUND",
                  "message": "Event not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No event carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get an event",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/catalog/events/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/catalog/events/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/catalog/events/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/catalog/events/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/catalog/markets": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/catalog/markets`. The edge throttles it per IP.",
        "operationId": "listMarkets",
        "parameters": [
          {
            "description": "Comma-separated canonical league names.",
            "example": "NFL,NBA",
            "in": "query",
            "name": "league",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated canonical market types.",
            "example": "MONEY,SPREAD",
            "in": "query",
            "name": "marketType",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated statuses required of the owning event.",
            "example": "OPEN_PREGAME",
            "in": "query",
            "name": "eventStatus",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Exclusive lower bound on the event's scheduled start.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the event's scheduled start.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_OpenMarketResponse"
                }
              }
            },
            "description": "One page of markets, each with its outcomes."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List markets",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/catalog/markets\"\nQUERY=\"league=NFL\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/catalog/markets\";\n    let url = format!(\"{HOST}{path}?league=NFL\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/catalog/markets\"\nquery = \"league=NFL\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/catalog/markets\";\nconst query = \"league=NFL\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/catalog/markets/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/catalog/markets/{id}`. The edge throttles it per IP.",
        "operationId": "getMarket",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenMarketResponse"
                }
              }
            },
            "description": "The market."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get a market",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/catalog/markets/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/catalog/markets/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/catalog/markets/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/catalog/markets/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/catalog/markets/{id}/book": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-info\">304</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/catalog/markets/{id}/book`. The edge throttles it per IP.",
        "operationId": "getBook",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Number of price levels per outcome, best price first. Every order at a kept level is\nincluded. Defaults to **20**. 1 to 20. A value outside that range answers `400`.",
            "in": "query",
            "name": "depth",
            "required": false,
            "schema": {
              "default": 20,
              "format": "int32",
              "maximum": 20,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "A prior response's `ETag`. A match answers `304`.",
            "in": "header",
            "name": "If-None-Match",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Book"
                }
              }
            },
            "description": "Resting orders by outcome.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "The book seq matches the `If-None-Match` ETag.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "BOOK_DEPTH_OUT_OF_RANGE",
                  "message": "`depth` must be between 1 and 20."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "`depth` is outside 1 to 20."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get the order book",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/catalog/markets/$ID/book\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/catalog/markets/{ID}/book\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/catalog/markets/{ID}/book\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/catalog/markets/${ID}/book`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/catalog/markets/{id}/trades": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/catalog/markets/{id}/trades`. The edge throttles it per IP.",
        "operationId": "listTrades",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_Trade"
                }
              }
            },
            "description": "One page of trades, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List trades",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/catalog/markets/$ID/trades\"\nQUERY=\"limit=100\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/catalog/markets/{ID}/trades\");\n    let url = format!(\"{HOST}{path}?limit=100\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/catalog/markets/{ID}/trades\"\nquery = \"limit=100\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/catalog/markets/${ID}/trades`;\nconst query = \"limit=100\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/echo": {
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `any` |\n| **Throttle** | `free` |\n| **Cost** | `0 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">413</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nA `200` proves your host, key, clock and canonical string at once.",
        "operationId": "echo",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "hello": "world"
              },
              "schema": {}
            }
          },
          "description": "Any payload. The route echoes it without interpreting it.",
          "required": true
        },
        "responses": {
          "200": {
            "description": "Your request body, byte for byte."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "413": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAYLOAD_TOO_LARGE",
                  "message": "request entity too large"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The body is too large. The edge refuses most oversized bodies before the origin sees them. The edge answers a plain `403` with no error body."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Echo",
        "tags": [
          "Authentication"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/echo\"\nQUERY=\"\"\nBODY='{\n  \"hello\": \"world\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst BODY: &[u8] = br#\"{\n  \"hello\": \"world\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/echo\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/echo\"\nquery = \"\"\nbody = b\"\"\"{\n  \"hello\": \"world\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/echo\";\nconst query = \"\";\nconst body = `{\n  \"hello\": \"world\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/history/events": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `4 + 1 per 100 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listHistoricalEvents",
        "parameters": [
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_HistoricalEventResponse"
                }
              }
            },
            "description": "One page of events, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List event history",
        "tags": [
          "History"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/history/events\"\nQUERY=\"limit=100\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/history/events\";\n    let url = format!(\"{HOST}{path}?limit=100\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/history/events\"\nquery = \"limit=100\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/history/events\";\nconst query = \"limit=100\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/history/events/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getHistoricalEvent",
        "parameters": [
          {
            "description": "Event ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HistoricalEventResponse"
                }
              }
            },
            "description": "The event."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "EVENT_NOT_FOUND",
                  "message": "Event not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No event carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get an event from history",
        "tags": [
          "History"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/history/events/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/history/events/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/history/events/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/history/events/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/history/markets": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `4 + 1 per 50 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listHistoricalMarkets",
        "parameters": [
          {
            "description": "Exclusive lower bound on the market's most recent settlement. Excludes unsettled markets.",
            "example": 1756512000000,
            "in": "query",
            "name": "settledAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_HistoricalMarketResponse"
                }
              }
            },
            "description": "One page of markets, newest first. With `settledAfter`, most recent settlement first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List market history",
        "tags": [
          "History"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/history/markets\"\nQUERY=\"limit=100\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/history/markets\";\n    let url = format!(\"{HOST}{path}?limit=100\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/history/markets\"\nquery = \"limit=100\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/history/markets\";\nconst query = \"limit=100\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/history/markets/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getHistoricalMarket",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HistoricalMarketResponse"
                }
              }
            },
            "description": "The market."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get a market from history",
        "tags": [
          "History"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/history/markets/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/history/markets/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/history/markets/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/history/markets/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/history/positions": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `4 + 1 per 50 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listHistoricalPositions",
        "parameters": [
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One market. With `event`, the market must belong to that event.",
            "in": "query",
            "name": "market",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One outcome. With `market`, the outcome must belong to that market.",
            "in": "query",
            "name": "outcome",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Comma-separated market statuses: `OPEN`, `CLOSED`, `SETTLED`.",
            "example": "SETTLED",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_HistoricalPositionResponse"
                }
              }
            },
            "description": "One page of your positions, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List position history",
        "tags": [
          "History"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/history/positions\"\nQUERY=\"status=OPEN\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/history/positions\";\n    let url = format!(\"{HOST}{path}?status=OPEN\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/history/positions\"\nquery = \"status=OPEN\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/history/positions\";\nconst query = \"status=OPEN\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/history/positions/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getHistoricalPosition",
        "parameters": [
          {
            "description": "Position ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HistoricalPositionResponse"
                }
              }
            },
            "description": "The position."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "POSITION_NOT_FOUND",
                  "message": "Position not found."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No position carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get a position from history",
        "tags": [
          "History"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/history/positions/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/history/positions/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/history/positions/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/history/positions/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/keys": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listKeys",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Key"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Your live keys."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List keys",
        "tags": [
          "Authentication"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/keys\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/keys\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/keys\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/keys\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">201</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">409</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `false` |\n\nAlways issues `management::read`. The body has no scope field. To get a `trading` or `trading::read` key, use [Issue a subaccount key](/api-reference/accounts/issue-a-subaccount-key).",
        "operationId": "createKey",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "algorithm": "Ed25519",
                "name": "management",
                "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\n-----END PUBLIC KEY-----\n"
              },
              "schema": {
                "$ref": "#/components/schemas/CreateKey"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreated"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_BODY",
                  "message": "publicKey must not be empty"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KYC_REQUIRED",
                  "message": "The account must complete KYC verification before it trades."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route, or the trader has not passed KYC. The code says which."
          },
          "409": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KEY_EXISTS",
                  "message": "A live key with this public key already exists."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "A live key with this public key already exists, anywhere on Novig."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Create a key",
        "tags": [
          "Authentication"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/keys\"\nQUERY=\"\"\nBODY='{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"management\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst BODY: &[u8] = br#\"{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"management\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/keys\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/keys\"\nquery = \"\"\nbody = b\"\"\"{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"management\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/keys\";\nconst query = \"\";\nconst body = `{\n  \"algorithm\": \"Ed25519\",\n  \"name\": \"management\",\n  \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEAxgWNZGF7JgS1F3napw5Os55J+66imCjrzLMq/BEyOxM=\\n-----END PUBLIC KEY-----\\n\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/keys/{id}": {
      "delete": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">204</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nRevocation takes effect within 60s. It does not cancel resting orders.",
        "operationId": "revokeKey",
        "parameters": [
          {
            "description": "Key ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KEY_NOT_FOUND",
                  "message": "Key not found."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No key carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Revoke a key",
        "tags": [
          "Authentication"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/keys/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nDELETE\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X DELETE \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/keys/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .delete(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/keys/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"DELETE\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"DELETE\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/keys/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"DELETE\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"DELETE\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `management` `management::read` |\n| **Throttle** | `account` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getKey",
        "parameters": [
          {
            "description": "Key ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Key"
                }
              }
            },
            "description": "The key."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KEY_NOT_FOUND",
                  "message": "Key not found."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Revoked, expired, or another trader's. All three read the same."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get a key",
        "tags": [
          "Authentication"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/keys/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/keys/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/keys/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/keys/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/limits": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `any` |\n| **Throttle** | `free` |\n| **Cost** | `0 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "getLimits",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "account": {
                    "capacity": 64,
                    "refillPerSec": 8
                  },
                  "cancel": {
                    "capacity": 256,
                    "refillPerSec": 16
                  },
                  "history": {
                    "capacity": 512,
                    "refillPerSec": 4
                  },
                  "maxWatchedMarkets": 8192,
                  "place": {
                    "capacity": 256,
                    "refillPerSec": 8
                  },
                  "read": {
                    "capacity": 64,
                    "refillPerSec": 16
                  },
                  "stream": {
                    "capacity": 512,
                    "refillPerSec": 32
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/LimitsResponse"
                }
              }
            },
            "description": "Your schedule."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get your throttle schedule",
        "tags": [
          "Throttle"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\nPEM=novig-api-key-mgmt-1.pem\n\nREQ_PATH=\"/v3/limits\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM: &str = \"novig-api-key-mgmt-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/limits\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\"\nPEM = \"novig-api-key-mgmt-1.pem\"\n\npath = \"/v3/limits\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24\";\nconst PEM = \"novig-api-key-mgmt-1.pem\";\n\nconst path = \"/v3/limits\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/orders": {
      "delete": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` |\n| **Throttle** | `cancel` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |",
        "operationId": "cancelAll",
        "parameters": [
          {
            "description": "One market. With `event`, the market must belong to that event.",
            "in": "query",
            "name": "market",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One outcome. With `market`, the outcome must belong to that market.",
            "in": "query",
            "name": "outcome",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelAllResult"
                }
              }
            },
            "description": "Cancels queued."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_QUERY",
                  "message": "UUID parsing failed: invalid character: found `z` at 1"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Cancel all orders",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/orders\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nDELETE\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X DELETE \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/orders\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .delete(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/orders\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"DELETE\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"DELETE\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/orders\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"DELETE\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"DELETE\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` or `history` |\n| **Cost** | `1 /request` open, `4 + 1 per 100 rows` settled |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listOrders",
        "parameters": [
          {
            "description": "One market. With `event`, the market must belong to that event.",
            "in": "query",
            "name": "market",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One outcome. With `market`, the outcome must belong to that market.",
            "in": "query",
            "name": "outcome",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Filter by status. Default `OPEN`.",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/OrderStatus"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_Order"
                }
              }
            },
            "description": "One page of your orders."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List orders",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/orders\"\nQUERY=\"status=OPEN\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/orders\";\n    let url = format!(\"{HOST}{path}?status=OPEN\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/orders\"\nquery = \"status=OPEN\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/orders\";\nconst query = \"status=OPEN\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` |\n| **Throttle** | `place` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">201</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-warn\">409</span> <span class=\"st st-warn\">413</span> <span class=\"st st-warn\">422</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> <span class=\"st st-bad\">503</span> |\n| **Idempotent** | `false` |\n\n`201` means queued, not resting. The `open` event on the private stream\nconfirms the order rests.",
        "operationId": "placeOrder",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "clientId": "0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90",
                "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
                "price": "0.665",
                "qty": 110,
                "tif": "GTC"
              },
              "schema": {
                "$ref": "#/components/schemas/PlaceOrder"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "clientId": "0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90",
                  "orderId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                },
                "schema": {
                  "$ref": "#/components/schemas/OrderAccepted"
                }
              }
            },
            "description": "Accepted and queued."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_PRICE",
                  "message": "`price` is not on the submittable price grid."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KYC_REQUIRED",
                  "message": "The account must complete KYC verification before it trades."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route, or the trader has not passed KYC. The code says which."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "OUTCOME_NOT_FOUND",
                  "message": "Outcome not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No outcome carries that ID."
          },
          "409": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_CLOSED",
                  "message": "The market is closed."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The market is closed or inactive."
          },
          "413": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAYLOAD_TOO_LARGE",
                  "message": "request entity too large"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The body is too large. The edge refuses most oversized bodies before the origin sees them. The edge answers a plain `403` with no error body."
          },
          "422": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INSUFFICIENT_BALANCE",
                  "message": "Insufficient balance"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The bound wallet does not cover the order, or a position cap refused it."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SELF_EXCLUDED",
                  "message": "The account is self-excluded from trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading, or the key holder is self-excluded. The code says which."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "code": "GEOLOCATION_SCREENING_UNAVAILABLE",
                  "message": "Geolocation screening is unavailable. Placements are paused; retry shortly."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation screening is unavailable, so this route refuses rather than take on exposure it cannot screen. Retriable: reads and cancels still answer."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Place an order",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/orders\"\nQUERY=\"\"\nBODY='{\n  \"clientId\": \"0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90\",\n  \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n  \"price\": \"0.665\",\n  \"qty\": 110,\n  \"tif\": \"GTC\"\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst BODY: &[u8] = br#\"{\n  \"clientId\": \"0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90\",\n  \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n  \"price\": \"0.665\",\n  \"qty\": 110,\n  \"tif\": \"GTC\"\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/orders\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/orders\"\nquery = \"\"\nbody = b\"\"\"{\n  \"clientId\": \"0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90\",\n  \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n  \"price\": \"0.665\",\n  \"qty\": 110,\n  \"tif\": \"GTC\"\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/orders\";\nconst query = \"\";\nconst body = `{\n  \"clientId\": \"0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90\",\n  \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n  \"price\": \"0.665\",\n  \"qty\": 110,\n  \"tif\": \"GTC\"\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/orders/batch": {
      "delete": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` |\n| **Throttle** | `cancel` |\n| **Cost** | `1 /order` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-ok\">207</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-warn\">413</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nPartial. One unknown ID does not stop the rest.",
        "operationId": "batchCancel",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "orderIds": [
                  "7c9e6679-7425-40de-944b-e07fc1f90ae7",
                  "1b4e28ba-2fa1-11d2-883f-0016d3cca427"
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/BatchCancel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "canceled": [
                    "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                  ],
                  "notCanceled": []
                },
                "schema": {
                  "$ref": "#/components/schemas/BatchCancelResult"
                }
              }
            },
            "description": "Every ID cancelled."
          },
          "207": {
            "content": {
              "application/json": {
                "example": {
                  "canceled": [
                    "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                  ],
                  "notCanceled": [
                    {
                      "orderId": "1b4e28ba-2fa1-11d2-883f-0016d3cca427",
                      "reason": "FILLED"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/BatchCancelResult"
                }
              }
            },
            "description": "Some cancelled, some not. Read both arrays."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "EMPTY_BATCH",
                  "message": "`orders` must hold at least one order."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "canceled": [],
                  "notCanceled": [
                    {
                      "orderId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
                      "reason": "NOT_FOUND"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/BatchCancelResult"
                }
              }
            },
            "description": "None cancelled. Every entry carries `NOT_FOUND`."
          },
          "413": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAYLOAD_TOO_LARGE",
                  "message": "request entity too large"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The body is too large. The edge refuses most oversized bodies before the origin sees them. The edge answers a plain `403` with no error body."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Batch cancel orders",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/orders/batch\"\nQUERY=\"\"\nBODY='{\n  \"orderIds\": [\n    \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"1b4e28ba-2fa1-11d2-883f-0016d3cca427\"\n  ]\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nDELETE\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X DELETE \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst BODY: &[u8] = br#\"{\n  \"orderIds\": [\n    \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"1b4e28ba-2fa1-11d2-883f-0016d3cca427\"\n  ]\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/orders/batch\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .delete(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/orders/batch\"\nquery = \"\"\nbody = b\"\"\"{\n  \"orderIds\": [\n    \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"1b4e28ba-2fa1-11d2-883f-0016d3cca427\"\n  ]\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"DELETE\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"DELETE\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/orders/batch\";\nconst query = \"\";\nconst body = `{\n  \"orderIds\": [\n    \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"1b4e28ba-2fa1-11d2-883f-0016d3cca427\"\n  ]\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"DELETE\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"DELETE\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "post": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` |\n| **Throttle** | `place` |\n| **Cost** | `1 /order` |\n| **Answers** | <span class=\"st st-ok\">201</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">413</span> <span class=\"st st-warn\">422</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> <span class=\"st st-bad\">503</span> |\n| **Idempotent** | `false` |\n\nAll or nothing. A resend places the batch again.",
        "operationId": "batchPlace",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "orders": [
                  {
                    "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
                    "price": "0.665",
                    "qty": 110,
                    "tif": "GTC"
                  },
                  {
                    "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3302",
                    "price": "0.330",
                    "qty": 50,
                    "tif": "IOC"
                  }
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/BatchPlace"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/OrderAccepted"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every order accepted and queued."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "BATCH_REJECTED",
                  "message": "2 order(s) refused. `rejected` names each one.",
                  "rejected": [
                    {
                      "index": 1,
                      "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
                      "reason": "Invalid price"
                    },
                    {
                      "index": 3,
                      "outcomeId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
                      "reason": "Invalid price"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "An order is malformed or the exchange refuses it. Nothing was placed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "KYC_REQUIRED",
                  "message": "The account must complete KYC verification before it trades."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route, or the trader has not passed KYC. The code says which."
          },
          "413": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAYLOAD_TOO_LARGE",
                  "message": "request entity too large"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The body is too large. The edge refuses most oversized bodies before the origin sees them. The edge answers a plain `403` with no error body."
          },
          "422": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INSUFFICIENT_BALANCE",
                  "message": "Insufficient balance",
                  "rejected": [
                    {
                      "index": 1,
                      "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
                      "reason": "Insufficient balance"
                    },
                    {
                      "index": 3,
                      "outcomeId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
                      "reason": "Insufficient balance"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The bound wallet does not cover an order. The exchange placed no order."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SELF_EXCLUDED",
                  "message": "The account is self-excluded from trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading, or the key holder is self-excluded. The code says which."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "code": "GEOLOCATION_SCREENING_UNAVAILABLE",
                  "message": "Geolocation screening is unavailable. Placements are paused; retry shortly."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation screening is unavailable, so this route refuses rather than take on exposure it cannot screen. Retriable: reads and cancels still answer."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Batch place orders",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/orders/batch\"\nQUERY=\"\"\nBODY='{\n  \"orders\": [\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n      \"price\": \"0.665\",\n      \"qty\": 110,\n      \"tif\": \"GTC\"\n    },\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3302\",\n      \"price\": \"0.330\",\n      \"qty\": 50,\n      \"tif\": \"IOC\"\n    }\n  ]\n}'\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nPOST\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X POST \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\" \\\n  --data-binary \"$BODY\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst BODY: &[u8] = br#\"{\n  \"orders\": [\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n      \"price\": \"0.665\",\n      \"qty\": 110,\n      \"tif\": \"GTC\"\n    },\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3302\",\n      \"price\": \"0.330\",\n      \"qty\": 50,\n      \"tif\": \"IOC\"\n    }\n  ]\n}\"#;\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/orders/batch\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .post(url)\n        .header(\"Content-Type\", \"application/json\")\n        .body(BODY)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/orders/batch\"\nquery = \"\"\nbody = b\"\"\"{\n  \"orders\": [\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n      \"price\": \"0.665\",\n      \"qty\": 110,\n      \"tif\": \"GTC\"\n    },\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3302\",\n      \"price\": \"0.330\",\n      \"qty\": 50,\n      \"tif\": \"IOC\"\n    }\n  ]\n}\"\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"POST\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n    \"Content-Type\": \"application/json\",\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"POST\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/orders/batch\";\nconst query = \"\";\nconst body = `{\n  \"orders\": [\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3301\",\n      \"price\": \"0.665\",\n      \"qty\": 110,\n      \"tif\": \"GTC\"\n    },\n    {\n      \"outcomeId\": \"3f2504e0-4f89-11d3-9a0c-0305e82c3302\",\n      \"price\": \"0.330\",\n      \"qty\": 50,\n      \"tif\": \"IOC\"\n    }\n  ]\n}`;\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"POST\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n  \"Content-Type\": \"application/json\",\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"POST\",\n  headers,\n  body,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/orders/{id}": {
      "delete": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` |\n| **Throttle** | `cancel` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |",
        "operationId": "cancelOrder",
        "parameters": [
          {
            "description": "Order ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelAccepted"
                }
              }
            },
            "description": "Cancel queued."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ORDER_NOT_FOUND",
                  "message": "Order not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No order carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Cancel an order",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/orders/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nDELETE\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X DELETE \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/orders/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .delete(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/orders/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"DELETE\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"DELETE\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/orders/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"DELETE\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"DELETE\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      },
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nCan answer `404` right after a `201`. Wait for the `open` event.",
        "operationId": "getOrder",
        "parameters": [
          {
            "description": "Order ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            },
            "description": "The order."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ORDER_NOT_FOUND",
                  "message": "Order not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No order carries that ID."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Get an order",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\nID=6f9619ff-8b86-d011-b42d-00c04fc964ff\n\nREQ_PATH=\"/v3/orders/$ID\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\nconst ID: &str = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = format!(\"/v3/orders/{ID}\");\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\nID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n\npath = f\"/v3/orders/{ID}\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\nconst ID = \"6f9619ff-8b86-d011-b42d-00c04fc964ff\";\n\nconst path = `/v3/orders/${ID}`;\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/portfolio/fills": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `history` |\n| **Cost** | `8 + 1 per 50 rows` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listFills",
        "parameters": [
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One market. With `event`, the market must belong to that event.",
            "in": "query",
            "name": "market",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One outcome. With `market`, the outcome must belong to that market.",
            "in": "query",
            "name": "outcome",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One order.",
            "in": "query",
            "name": "order",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Exclusive lower bound on the fill time, in epoch milliseconds.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the fill time, in epoch milliseconds.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_Fill"
                }
              }
            },
            "description": "One page of your fills, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List fills",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/portfolio/fills\"\nQUERY=\"limit=100\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/portfolio/fills\";\n    let url = format!(\"{HOST}{path}?limit=100\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/portfolio/fills\"\nquery = \"limit=100\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}?{query}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/portfolio/fills\";\nconst query = \"limit=100\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}?${query}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/portfolio/positions": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |",
        "operationId": "listPositions",
        "parameters": [
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One market. With `event`, the market must belong to that event.",
            "in": "query",
            "name": "market",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "One outcome. With `market`, the outcome must belong to that market.",
            "in": "query",
            "name": "outcome",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Position"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every open position."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "INVALID_QUERY",
                  "message": "UUID parsing failed: invalid character: found `z` at 1"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List positions",
        "tags": [
          "Execution"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/portfolio/positions\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/portfolio/positions\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/portfolio/positions\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/portfolio/positions\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/public/catalog/events": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listEventsPublic",
        "parameters": [
          {
            "description": "Comma-separated canonical league names.",
            "example": "NFL,NBA",
            "in": "query",
            "name": "league",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated event statuses.",
            "example": "OPEN_PREGAME",
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Exclusive lower bound on the event's scheduled start.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the event's scheduled start.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_EventResponse"
                }
              }
            },
            "description": "One page of events."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List events",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/catalog/events?league=NFL\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/catalog/events?league=NFL\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/catalog/events?league=NFL\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/catalog/events?league=NFL\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/catalog/events/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "getEventPublic",
        "parameters": [
          {
            "description": "Event ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventResponse"
                }
              }
            },
            "description": "The event."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "EVENT_NOT_FOUND",
                  "message": "Event not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No event carries that ID."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "Get an event",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/catalog/events/6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/catalog/events/6f9619ff-8b86-d011-b42d-00c04fc964ff\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/catalog/events/6f9619ff-8b86-d011-b42d-00c04fc964ff\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/catalog/events/6f9619ff-8b86-d011-b42d-00c04fc964ff\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/catalog/markets": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listMarketsPublic",
        "parameters": [
          {
            "description": "Comma-separated canonical league names.",
            "example": "NFL,NBA",
            "in": "query",
            "name": "league",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated canonical market types.",
            "example": "MONEY,SPREAD",
            "in": "query",
            "name": "marketType",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated statuses required of the owning event.",
            "example": "OPEN_PREGAME",
            "in": "query",
            "name": "eventStatus",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Every market of one event.",
            "in": "query",
            "name": "event",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Exclusive lower bound on the event's scheduled start.",
            "example": 1756512000000,
            "in": "query",
            "name": "startsAfter",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Exclusive upper bound on the event's scheduled start.",
            "example": 1756598400000,
            "in": "query",
            "name": "startsBefore",
            "required": false,
            "schema": {
              "format": "int64",
              "type": "integer"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_OpenMarketResponse"
                }
              }
            },
            "description": "One page of markets, each with its outcomes."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List markets",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/catalog/markets?league=NFL\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/catalog/markets?league=NFL\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/catalog/markets?league=NFL\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/catalog/markets?league=NFL\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/catalog/markets/{id}": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "getMarketPublic",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenMarketResponse"
                }
              }
            },
            "description": "The market."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "Get a market",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/catalog/markets/{id}/book": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-info\">304</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "getBookPublic",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Number of price levels per outcome, best price first. Every order at a kept level is\nincluded. Defaults to **20**. 1 to 20. A value outside that range answers `400`.",
            "in": "query",
            "name": "depth",
            "required": false,
            "schema": {
              "default": 20,
              "format": "int32",
              "maximum": 20,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "A prior response's `ETag`. A match answers `304`.",
            "in": "header",
            "name": "If-None-Match",
            "required": false,
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Book"
                }
              }
            },
            "description": "Resting orders by outcome.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "The book seq matches the `If-None-Match` ETag.",
            "headers": {
              "ETag": {
                "description": "An opaque validator for this snapshot.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "BOOK_DEPTH_OUT_OF_RANGE",
                  "message": "`depth` must be between 1 and 20."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "`depth` is outside 1 to 20."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "Get the order book",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/book\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/book\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/book\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/book\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/catalog/markets/{id}/trades": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">400</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">404</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listTradesPublic",
        "parameters": [
          {
            "description": "Market ID.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Page size. Defaults to **500**, not the maximum. 1 to 5000. A value outside that range\nanswers `400`.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 500,
              "format": "int32",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `next` cursor from the previous page. Opaque.",
            "in": "query",
            "name": "after",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_Trade"
                }
              }
            },
            "description": "One page of trades, newest first."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "PAGE_LIMIT_OUT_OF_RANGE",
                  "message": "Limit must be between 1 and 5000"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is malformed."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "MARKET_NOT_FOUND",
                  "message": "Market not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "No market carries that ID."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List trades",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/trades?limit=100\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/trades?limit=100\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/trades?limit=100\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/catalog/markets/6f9619ff-8b86-d011-b42d-00c04fc964ff/trades?limit=100\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/types/event-statuses": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listEventStatusesPublic",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/EventStatus"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every event status."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List event statuses",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/types/event-statuses\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/types/event-statuses\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/types/event-statuses\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/types/event-statuses\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/types/leagues": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listLeaguesPublic",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  "NFL",
                  "NBA"
                ],
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every canonical league name."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List leagues",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/types/leagues\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/types/leagues\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/types/leagues\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/types/leagues\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/types/markets": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listMarketTypesPublic",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  "MONEY",
                  "SPREAD",
                  "TOTAL"
                ],
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every canonical market type."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List market types",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/types/markets\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/types/markets\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/types/markets\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/types/markets\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/public/types/sports": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `none` |\n| **Throttle** | `public` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-hold\">429</span> |\n| **Idempotent** | `true` |\n\nThe unauthenticated, cacheable form of this read. It needs no credentials. The edge network caches each response, so it can be a few seconds old.",
        "operationId": "listSportsPublic",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  "FOOTBALL",
                  "BASKETBALL"
                ],
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every canonical sport name."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The request is missing the viewer address the edge network adds."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          }
        },
        "summary": "List sports",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "curl -s \"https://api.paper.novig.com/v3/public/types/sports\"\n"
          },
          {
            "lang": "rust",
            "source": "fn main() -> anyhow::Result<()> {\n    let body = reqwest::blocking::get(\"https://api.paper.novig.com/v3/public/types/sports\")?.text()?;\n    println!(\"{body}\");\n    Ok(())\n}\n"
          },
          {
            "lang": "python",
            "source": "import urllib.request\nprint(urllib.request.urlopen(\"https://api.paper.novig.com/v3/public/types/sports\").read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "const r = await fetch(\"https://api.paper.novig.com/v3/public/types/sports\");\nconsole.log(await r.text());\n"
          }
        ],
        "x-hidden": true
      }
    },
    "/v3/types/event-statuses": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/types/event-statuses`. The edge throttles it per IP.",
        "operationId": "listEventStatuses",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/EventStatus"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every event status."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List event statuses",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/types/event-statuses\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/types/event-statuses\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/types/event-statuses\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/types/event-statuses\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/types/leagues": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/types/leagues`. The edge throttles it per IP.",
        "operationId": "listLeagues",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  "NFL",
                  "NBA"
                ],
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every canonical league name."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List leagues",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/types/leagues\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/types/leagues\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/types/leagues\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/types/leagues\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/types/markets": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/types/markets`. The edge throttles it per IP.",
        "operationId": "listMarketTypes",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  "MONEY",
                  "SPREAD",
                  "TOTAL"
                ],
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every canonical market type."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List market types",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/types/markets\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/types/markets\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/types/markets\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/types/markets\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/types/sports": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `read` |\n| **Cost** | `1 /request` |\n| **Answers** | <span class=\"st st-ok\">200</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nAlso served without a signature at `GET /v3/public/types/sports`. The edge throttles it per IP.",
        "operationId": "listSports",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  "FOOTBALL",
                  "BASKETBALL"
                ],
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Every canonical sport name."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "List sports",
        "tags": [
          "Catalog"
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "source": "HOST=https://api.paper.novig.com\nKEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\nPEM=desk-1.pem\n\nREQ_PATH=\"/v3/types/sports\"\nQUERY=\"\"\nBODY=''\nTS=$(date +%s000)\nHASH=$(printf %s \"$BODY\" \\\n  | openssl dgst -sha256 -r | cut -d\" \" -f1)\n\n# openssl signs a file, not a pipe. base64 -A never wraps.\nprintf 'NOVIG-V3\\n%s\\nGET\\n%s\\n%s\\n%s' \\\n  \"$TS\" \"$REQ_PATH\" \"$QUERY\" \"$HASH\" > canon.bin\nSIG=$(openssl pkeyutl -sign -rawin -inkey \"$PEM\" \\\n  -in canon.bin | openssl base64 -A)\n\ncurl -s -X GET \"$HOST$REQ_PATH${QUERY:+?$QUERY}\" \\\n  -H \"Novig-Key-Id: $KEY_ID\" \\\n  -H \"Novig-Timestamp: $TS\" \\\n  -H \"Novig-Signature: $SIG\"\n"
          },
          {
            "lang": "rust",
            "source": "use base64::prelude::*;\nuse ed25519_dalek::pkcs8::DecodePrivateKey;\nuse ed25519_dalek::{Signer, SigningKey};\nuse reqwest::blocking::{Client, Request};\nuse reqwest::{Method, Url};\nuse sha2::{Digest, Sha256};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nconst HOST: &str = \"https://api.paper.novig.com\";\nconst KEY_ID: &str =\n    \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM: &str = \"desk-1.pem\";\n\nfn main() -> anyhow::Result<()> {\n    let key = Key::load(KEY_ID, PEM)?;\n    let client = Client::new();\n    let path = \"/v3/types/sports\";\n    let url = format!(\"{HOST}{path}\");\n    let req = client\n        .get(url)\n        .build()?\n        .sign(&key)?;\n    println!(\"{}\", client.execute(req)?.text()?);\n    Ok(())\n}\n\n/// An API key: the id the server looks up, and the\n/// private half that signs.\nstruct Key {\n    id: &'static str,\n    signer: SigningKey,\n}\n\nimpl Key {\n    fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {\n        let signer =\n            SigningKey::read_pkcs8_pem_file(pem)?;\n        Ok(Self { id, signer })\n    }\n}\n\n/// Signs a built request over the method, path, query and\n/// body it will send, so the two can never disagree.\ntrait Sign: Sized {\n    fn sign(self, key: &Key) -> anyhow::Result<Self>;\n}\n\nimpl Sign for Request {\n    fn sign(mut self, key: &Key) -> anyhow::Result<Self> {\n        let ts = SystemTime::now()\n            .duration_since(UNIX_EPOCH)?\n            .as_millis()\n            .to_string();\n        let body = self.body().and_then(|b| b.as_bytes());\n        let body = body.unwrap_or_default();\n        let canon = Canonical::new(\n            &ts,\n            self.method(),\n            self.url(),\n            body,\n        );\n        let sig = key.signer.sign(canon.0.as_bytes());\n        let sig = BASE64_STANDARD.encode(sig.to_bytes());\n        let headers = self.headers_mut();\n        headers.insert(\"Novig-Key-Id\", key.id.parse()?);\n        headers.insert(\"Novig-Timestamp\", ts.parse()?);\n        headers.insert(\"Novig-Signature\", sig.parse()?);\n        Ok(self)\n    }\n}\n\n/// The six NOVIG-V3 lines, joined by LF.\nstruct Canonical(String);\n\nimpl Canonical {\n    fn new(\n        ts: &str,\n        method: &Method,\n        url: &Url,\n        body: &[u8],\n    ) -> Self {\n        let query =\n            Query::from(url.query().unwrap_or(\"\"));\n        let hash = format!(\"{:x}\", Sha256::digest(body));\n        let method = method.as_str();\n        let path = url.path();\n        let lines = [\n            \"NOVIG-V3\", ts, method, path, &query.0, &hash,\n        ];\n        Self(lines.join(\"\\n\"))\n    }\n}\n\n/// Each part decoded and re-encoded, then sorted by\n/// name and value.\nstruct Query(String);\n\nimpl From<&str> for Query {\n    fn from(raw: &str) -> Self {\n        let mut pairs = raw\n            .split('&')\n            .filter(|pair| !pair.is_empty())\n            .map(|pair| {\n                pair.split_once('=').unwrap_or((pair, \"\"))\n            })\n            .map(|(k, v)| {\n                (Self::encode(k), Self::encode(v))\n            })\n            .collect::<Vec<_>>();\n        pairs.sort();\n        let pairs =\n            pairs.iter().map(|(k, v)| format!(\"{k}={v}\"));\n        Self(pairs.collect::<Vec<_>>().join(\"&\"))\n    }\n}\n\nimpl Query {\n    fn encode(part: &str) -> String {\n        Self::decode(part)\n            .iter()\n            .map(|&b| match b {\n                b'-' | b'.' | b'_' | b'~' => {\n                    (b as char).to_string()\n                }\n                _ if b.is_ascii_alphanumeric() => {\n                    (b as char).to_string()\n                }\n                _ => format!(\"%{b:02X}\"),\n            })\n            .collect()\n    }\n\n    /// Only `%XX` decodes. A bare `+` stays a `+`.\n    fn decode(part: &str) -> Vec<u8> {\n        let raw = part.as_bytes();\n        let mut out = Vec::with_capacity(raw.len());\n        let mut i = 0;\n        while i < raw.len() {\n            let escape =\n                raw.get(i + 1..i + 3).and_then(Self::hex);\n            match (raw[i], escape) {\n                (b'%', Some(byte)) => {\n                    out.push(byte);\n                    i += 3;\n                }\n                (byte, _) => {\n                    out.push(byte);\n                    i += 1;\n                }\n            }\n        }\n        out\n    }\n\n    fn hex(pair: &[u8]) -> Option<u8> {\n        let hi = (pair[0] as char).to_digit(16)?;\n        let lo = (pair[1] as char).to_digit(16)?;\n        Some((hi * 16 + lo) as u8)\n    }\n}\n"
          },
          {
            "lang": "python",
            "source": "import base64, hashlib, time, urllib.request\nfrom cryptography.hazmat.primitives.serialization import (\n    load_pem_private_key)\n\nHOST = \"https://api.paper.novig.com\"\nKEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\"\nPEM = \"desk-1.pem\"\n\npath = \"/v3/types/sports\"\nquery = \"\"\nbody = b\"\"\n\nts = str(int(time.time() * 1000))\ncanon = \"\\n\".join([\"NOVIG-V3\", ts, \"GET\", path, query,\n                   hashlib.sha256(body).hexdigest()])\nkey = load_pem_private_key(open(PEM, \"rb\").read(), None)\nheaders = {\n    \"Novig-Key-Id\": KEY_ID,\n    \"Novig-Timestamp\": ts,\n    \"Novig-Signature\": base64.b64encode(\n        key.sign(canon.encode())).decode(),\n}\nurl = f\"{HOST}{path}\"\nreq = urllib.request.Request(\n    url, data=body or None,\n    method=\"GET\", headers=headers)\nprint(urllib.request.urlopen(req).read().decode())\n"
          },
          {
            "lang": "typescript",
            "source": "import {\n  createHash, createPrivateKey, sign,\n} from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nconst HOST = \"https://api.paper.novig.com\";\nconst KEY_ID = \"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0\";\nconst PEM = \"desk-1.pem\";\n\nconst path = \"/v3/types/sports\";\nconst query = \"\";\nconst body = \"\";\n\nconst ts = Date.now().toString();\nconst hash =\n  createHash(\"sha256\").update(body).digest(\"hex\");\nconst canon = [\n  \"NOVIG-V3\", ts, \"GET\", path, query, hash,\n].join(\"\\n\");\nconst key = createPrivateKey(readFileSync(PEM));\nconst headers: Record<string, string> = {\n  \"Novig-Key-Id\": KEY_ID,\n  \"Novig-Timestamp\": ts,\n  \"Novig-Signature\": sign(null, Buffer.from(canon), key)\n    .toString(\"base64\"),\n};\nconst url = `${HOST}${path}`;\nconst r = await fetch(url, {\n  method: \"GET\",\n  headers,\n});\nconsole.log(await r.text());\n"
          }
        ]
      }
    },
    "/v3/ws": {
      "get": {
        "description": "| | |\n| --- | --- |\n| **Key** | `trading` `trading::read` |\n| **Throttle** | `stream` |\n| **Cost** | `32 /request` |\n| **Answers** | <span class=\"st st-info\">101</span> <span class=\"st st-warn\">401</span> <span class=\"st st-warn\">403</span> <span class=\"st st-hold\">423</span> <span class=\"st st-hold\">429</span> <span class=\"st st-warn\">451</span> |\n| **Idempotent** | `true` |\n\nSign the upgrade request like any route. The subjects are `market:<id>`,\n`event:<id>`, and `PRIVATE`. The channels are `lifecycle`, `trades`, `bbo`,\n`book`, `orders`, and `positions`. Once per interval, a `heartbeat` message\nstates your last seq on each subscribed private channel.",
        "operationId": "connectWebsocket",
        "responses": {
          "101": {
            "description": "The connection continues as a websocket."
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SIGNATURE_REJECTED",
                  "message": "signature verification failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The signature is absent or invalid, or the key is unknown, revoked, or expired."
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "SCOPE_INSUFFICIENT",
                  "message": "api key scope is insufficient for this route"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The key's scope does not grant this route."
          },
          "423": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ACCOUNT_LOCKED",
                  "message": "The account is locked out of trading."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The account is locked out of trading."
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "RATE_LIMIT_EXCEEDED",
                  "message": "Rate limit exceeded. Please wait before retrying."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "The throttle is empty. Wait the number of seconds in `Retry-After`. Then retry."
          },
          "451": {
            "content": {
              "application/json": {
                "example": {
                  "code": "ANONYMIZED_NETWORK",
                  "message": "The request came over a VPN, a proxy, or a Tor exit."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            },
            "description": "Geolocation refused the request: an anonymized network, a restricted region, or, for a placement, no device geolocation in the last 3 days."
          }
        },
        "security": [
          {
            "keyId": [],
            "signature": [],
            "timestamp": []
          }
        ],
        "summary": "Open the websocket",
        "tags": [
          "Streaming"
        ]
      }
    }
  },
  "servers": [
    {
      "description": "Paper",
      "url": "https://api.paper.novig.com"
    }
  ],
  "tags": [
    {
      "description": "The tradable world: events, markets and outcomes, and the vocabularies their fields use.",
      "name": "Catalog"
    },
    {
      "description": "Markets and events that are no longer in the catalog, and the caller's positions. These records have no age limit.",
      "name": "History"
    },
    {
      "description": "Signature debugging: the string the server built from your request.",
      "name": "Authentication"
    },
    {
      "description": "Subaccounts and their ledgers.",
      "name": "Accounts"
    },
    {
      "description": "Orders, and the caller's own trading state.",
      "name": "Execution"
    },
    {
      "description": "The websocket entry point.",
      "name": "Streaming"
    },
    {
      "description": "The rate-limit schedule, read at runtime.",
      "name": "Throttle"
    }
  ]
}
