+ * sdk.auth('myApiKey');
+ *
+ * @see {@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}
+ * @see {@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22}
+ * @param values Your auth credentials for the API; can specify up to two strings or numbers.
+ */
+ auth(...values: string[] | number[]) {
+ this.core.setAuth(...values);
+ return this;
+ }
+
+ /**
+ * If the API you're using offers alternate server URLs, and server variables, you can tell
+ * the SDK which one to use with this method. To use it you can supply either one of the
+ * server URLs that are contained within the OpenAPI definition (along with any server
+ * variables), or you can pass it a fully qualified URL to use (that may or may not exist
+ * within the OpenAPI definition).
+ *
+ * @example
+ * sdk.server('https://eu.api.example.com/v14');
+ *
+ * @param url Server URL
+ * @param variables An object of variables to replace into the server URL.
+ */
+ server(url: string, variables = {}) {
+ this.core.setServer(url, variables);
+ }
+
+ /**
+ * Search and filter all news articles available via the Perigon API. The result includes a
+ * list of individual articles that were matched to your specific criteria.
+ *
+ * @summary All Articles
+ * @throws FetchError<400, types.AllNewsResponse400> 400
+ * @throws FetchError<401, types.AllNewsResponse401> 401
+ * @throws FetchError<403, types.AllNewsResponse403> 403
+ * @throws FetchError<404, types.AllNewsResponse404> 404
+ * @throws FetchError<500, types.AllNewsResponse500> 500
+ */
+ allNews(metadata: types.AllNewsMetadataParam): Promise> {
+ return this.core.fetch('/v1/all', 'get', metadata);
+ }
+
+ /**
+ * Stories
+ *
+ * @throws FetchError<400, types.Stories1Response400> 400
+ */
+ stories1(metadata: types.Stories1MetadataParam): Promise> {
+ return this.core.fetch('/v1/stories/all', 'get', metadata);
+ }
+}
+
+const createSDK = (() => { return new SDK(); })()
+;
+
+export default createSDK;
+
+export type { AllNewsMetadataParam, AllNewsResponse200, AllNewsResponse400, AllNewsResponse401, AllNewsResponse403, AllNewsResponse404, AllNewsResponse500, Stories1MetadataParam, Stories1Response200, Stories1Response400 } from './types';
diff --git a/packages/perigon/openapi.json b/packages/perigon/openapi.json
new file mode 100644
index 0000000..1e9ab15
--- /dev/null
+++ b/packages/perigon/openapi.json
@@ -0,0 +1,1629 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "News & Stories",
+ "version": "unknown"
+ },
+ "servers": [
+ {
+ "url": "https://api.goperigon.com"
+ }
+ ],
+ "components": {
+ "securitySchemes": {
+ "sec0": {
+ "type": "apiKey",
+ "in": "query",
+ "name": "apiKey",
+ "x-default": ""
+ }
+ }
+ },
+ "security": [
+ {
+ "sec0": []
+ }
+ ],
+ "paths": {
+ "/v1/all": {
+ "get": {
+ "summary": "All Articles",
+ "description": "Search and filter all news articles available via the Perigon API. The result includes a list of individual articles that were matched to your specific criteria.",
+ "operationId": "all-news",
+ "parameters": [
+ {
+ "name": "apiKey",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "q",
+ "in": "query",
+ "description": "Search query, each article will be scored and ranked against it. Articles are searched on the title, description, and content fields. More ➜",
+ "schema": {
+ "type": "string",
+ "default": "recall OR \"safety concern*\""
+ }
+ },
+ {
+ "name": "title",
+ "in": "query",
+ "description": "Search article headlines/title field. Semantic similar to q parameter.",
+ "schema": {
+ "type": "string",
+ "default": "tesla OR TSLA OR \"General Motors\" OR GM"
+ }
+ },
+ {
+ "name": "desc",
+ "in": "query",
+ "description": "Search query on the description field. Semantic similar to q parameter.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "content",
+ "in": "query",
+ "description": "Search query on the article's body of content field. Semantic similar to q parameter.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "description": "Search query on the url field. Semantic similar to q parameter. E.g. could be used for querying certain website sections, e.g. source=cnn.com&url=travel.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "articleId",
+ "in": "query",
+ "description": "Article ID will search for a news article by the ID of the article. If several parameters are passed, all matched articles will be returned.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "clusterId",
+ "in": "query",
+ "description": "Search for related content using a cluster ID. Passing a cluster ID will filter results to only the content found within the cluster.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "from",
+ "in": "query",
+ "description": "'from' filter, will search articles published after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2023-03-01T00:00:00",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "to",
+ "in": "query",
+ "description": "'to' filter, will search articles published before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T23:59:59",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "addDateFrom",
+ "in": "query",
+ "description": "'addDateFrom' filter, will search articles added after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T00:00:00",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "addDateTo",
+ "in": "query",
+ "description": "'addDateTo' filter, will search articles added before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T23:59:59",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "refreshDateFrom",
+ "in": "query",
+ "description": "Will search articles that were refreshed after the specified date. The date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T00:00:00",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "refreshDateTo",
+ "in": "query",
+ "description": "Will search articles that were refreshed before the specified date. The date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T23:59:59",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "medium",
+ "in": "query",
+ "description": "Medium will filter out news articles medium, which could be 'Video' or 'Article'. If several parameters are passed, all matched articles will be returned.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "Article",
+ "Video"
+ ]
+ }
+ },
+ {
+ "name": "source",
+ "in": "query",
+ "description": "Publisher's domain can include a subdomain. If multiple parameters are passed, they will be applied as OR operations. Wildcards (\\* and ?) are suported (e.g. \\*.cnn.com). More ➜",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "sourceGroup",
+ "in": "query",
+ "description": "One of the supported source groups. Find Source Groups in the guided part of our documentation... More ➜",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "top10",
+ "top100",
+ "top25crypto",
+ "top25finance",
+ "top50tech",
+ "top100sports",
+ "top100leftUS",
+ "top100rightUS",
+ "top100centerUS"
+ ]
+ }
+ },
+ {
+ "name": "excludeSource",
+ "in": "query",
+ "description": "The domain of the website, which should be excluded from the search. Multiple parameters could be provided. Wildcards (\\* and ?) are suported (e.g. \\*.cnn.com).",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "paywall",
+ "in": "query",
+ "description": "Filter to show only results where the source has a paywall (true) or does not have a paywall (false).",
+ "schema": {
+ "type": "boolean"
+ }
+ },
+ {
+ "name": "byline",
+ "in": "query",
+ "description": "Author names to filter by. Article author bylines are used as a source field. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "journalistId",
+ "in": "query",
+ "description": "Filter by journalist ID. Journalist IDs are unique journalist identifiers which can be found through the Journalist API, or in the matchedAuthors field. More ➜",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "description": "Language code to filter by language. If multiple parameters are passed, they will be applied as OR operations. More ➜",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "da",
+ "de",
+ "en",
+ "es",
+ "fi",
+ "fr",
+ "hu",
+ "it",
+ "nl",
+ "no",
+ "pl",
+ "pt",
+ "ru",
+ "sv",
+ "uk"
+ ]
+ }
+ },
+ {
+ "name": "searchTranslation",
+ "in": "query",
+ "description": "Expand a query to search the translation, translatedTitle, and translatedDescription fields for non-English articles.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "name": "label",
+ "in": "query",
+ "description": "Labels to filter by, could be 'Opinion', 'Paid-news', 'Non-news', etc. If multiple parameters are passed, they will be applied as OR operations. More ➜",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "Opinion",
+ "Non-news",
+ "Paid News",
+ "Fact Check",
+ "Pop Culture",
+ "Roundup",
+ "Press Release",
+ "Low Content"
+ ]
+ }
+ },
+ {
+ "name": "excludeLabel",
+ "in": "query",
+ "description": "Exclude results that include specific labels (Opinion, Non-news, Paid News, etc.). You can filter multiple by repeating the parameter.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "Opinion",
+ "Non-news",
+ "Paid News",
+ "Fact Check",
+ "Pop Culture",
+ "Roundup",
+ "Press Release",
+ "Low Content"
+ ]
+ }
+ },
+ {
+ "name": "category",
+ "in": "query",
+ "description": "Filter by categories. Categories are general themes that the article is about. Examples of categories: Tech, Politics, etc. If multiple parameters are passed, they will be applied as OR operations. Use 'none' to search uncategorized articles. More ➜",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "Politics",
+ "Tech",
+ "Sports",
+ "Business",
+ "Finance",
+ "Entertainment",
+ "Health",
+ "Weather",
+ "Lifestyle",
+ "Auto",
+ "Science",
+ "Travel",
+ "Environment",
+ "World",
+ "General",
+ "none"
+ ]
+ }
+ },
+ {
+ "name": "topic",
+ "in": "query",
+ "description": "Filter by topics. Each topic is some kind of entity that the article is about. Examples of topics: Markets, Joe Biden, Green Energy, Climate Change, Cryptocurrency, etc. If multiple parameters are passed, they will be applied as OR operations. More ➜",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "excludeTopic",
+ "in": "query",
+ "description": "Filter by excluding topics. Each topic is some kind of entity that the article is about. Examples of topics: Markets, Joe Biden, Green Energy, Climate Change, Cryptocurrency, etc. If multiple parameters are passed, they will be applied as OR operations. More ➜",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "linkTo",
+ "in": "query",
+ "description": "Returns only articles that point to specified links (as determined by the 'links' field in the article response).",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "showReprints",
+ "in": "query",
+ "description": "Whether to return reprints in the response or not. Reprints are usually wired articles from sources like AP or Reuters that are reprinted in multiple sources at the same time. By default, this parameter is 'true'.",
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ {
+ "name": "reprintGroupId",
+ "in": "query",
+ "description": "Shows all articles belonging to the same reprint group. A reprint group includes one original article (the first one processed by the API) and all its known reprints.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "city",
+ "in": "query",
+ "description": "Filters articles where a specified city plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the urban area in question. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "area",
+ "in": "query",
+ "description": "Filters articles where a specified area, such as a neighborhood, borough, or district, plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the area in question. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "state",
+ "in": "query",
+ "description": "Filters articles where a specified state plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the state in question. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "locationsCountry",
+ "in": "query",
+ "description": "Filters articles where a specified country plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the country in question. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "excludeLocationsCountry",
+ "in": "query",
+ "description": "Excludes articles where a specified country plays a central role in the content, ensuring results are not deeply relevant to the country in question. If multiple parameters are passed, they will be applied as AND operations, excluding articles relevant to any of the specified countries.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "description": "Return all articles that have the specified location. Location attributes are delimited by ':' between key and value, and '::' between attributes. Example: 'city:New York::state:NY'.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "lat",
+ "in": "query",
+ "description": "Latitude of the center point to search places",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "lon",
+ "in": "query",
+ "description": "Longitude of the center point to search places",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "maxDistance",
+ "in": "query",
+ "description": "Maximum distance (in km) from starting point to search articles by tagged places",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ {
+ "name": "sourceCity",
+ "in": "query",
+ "description": "Find articles published by sources that are located within a given city.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "sourceCounty",
+ "in": "query",
+ "description": "Find articles published by sources that are located within a given county.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "sourceCountry",
+ "in": "query",
+ "description": "Find articles published by sources that are located within a given country. Must be 2 character country code (i.e. us, gb, etc).",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "country",
+ "in": "query",
+ "description": "Country code to filter by country. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "us",
+ "gb",
+ "de",
+ "it",
+ "fr",
+ "nl",
+ "se",
+ "dk",
+ "fi",
+ "hu",
+ "no",
+ "pl",
+ "pt",
+ "ru",
+ "ua",
+ "ch",
+ "br",
+ "nz",
+ "mx",
+ "au"
+ ]
+ }
+ },
+ {
+ "name": "sourceState",
+ "in": "query",
+ "description": "Find articles published by sources that are located within a given state.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "sourceLon",
+ "in": "query",
+ "description": "Latitude of the center point to search articles created by local publications.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "sourceLat",
+ "in": "query",
+ "description": "Latitude of the center point to search articles created by local publications.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "sourceMaxDistance",
+ "in": "query",
+ "description": "Maximum distance from starting point to search articles created by local publications.",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ {
+ "name": "personWikidataId",
+ "in": "query",
+ "description": "List of person Wikidata IDs for filtering.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "personName",
+ "in": "query",
+ "description": "List of person names for exact matches. Boolean and complex logic is not supported on this paramter.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companyId",
+ "in": "query",
+ "description": "List of company IDs to filter by.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companyName",
+ "in": "query",
+ "description": "Search by company name.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "companyDomain",
+ "in": "query",
+ "description": "Search by company domains for filtering. E.g. companyDomain=apple.com.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companySymbol",
+ "in": "query",
+ "description": "Search by company symbols.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "Zero-based page number. From 0 to 10000. See the Pagination section for limitations.",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ {
+ "name": "size",
+ "in": "query",
+ "description": "Number of articles returned per page, from 0 to 100.",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ {
+ "name": "sortBy",
+ "in": "query",
+ "description": "'relevance' to sort by relevance to the query, 'date' to sort by the publication date (desc), 'pubDate' is a synonym to 'date', 'addDate' to sort by 'addDate' field (desc), 'refreshDate' to sort by 'refreshDate' field (desc).",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "date",
+ "relevance",
+ "addDate",
+ "pubDate",
+ "refreshDate"
+ ],
+ "default": "relevance"
+ }
+ },
+ {
+ "name": "showNumResults",
+ "in": "query",
+ "description": "Whether to show the total number of all matched articles. Default value is false which makes queries a bit more efficient but also counts up to 10000 articles.",
+ "schema": {
+ "type": "boolean"
+ }
+ },
+ {
+ "name": "positiveSentimentFrom",
+ "in": "query",
+ "description": "Filters results with a sentiment score greater than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "positiveSentimentTo",
+ "in": "query",
+ "description": "Filters results with a sentiment score less than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "neutralSentimentFrom",
+ "in": "query",
+ "description": "Filters results with a sentiment score greater than or equal to the specified value, indicating neutral sentiment. Explanation of sentimental values can be found in Docs under the Article Data section.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "neutralSentimentTo",
+ "in": "query",
+ "description": "Filters results with a sentiment score less than or equal to the specified value, indicating neutral sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "negativeSentimentFrom",
+ "in": "query",
+ "description": "Filters results with a sentiment score greater than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "negativeSentimentTo",
+ "in": "query",
+ "description": "Filters results with a sentiment score less than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "taxonomy",
+ "in": "query",
+ "description": "Filters by Google Content Categories. This field will accept 1 or more categories, must pass the full name of the category. Example: taxonomy=/Finance/Banking/Other, /Finance/Investing/Funds",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "prefixTaxonomy",
+ "in": "query",
+ "description": "Filters by Google Content Categories. This field will filter by the category prefix only. Example: prefixTaxonomy=/Finance",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "200",
+ "content": {
+ "application/json": {
+ "examples": {
+ "Result": {
+ "value": "{\n \"status\": 200,\n \"numResults\": 1,\n \"articles\": [\n {\n \"url\": \"https://decrypt.co/301053/nft-market-hits-three-year-low-in-trading-and-sales-report\",\n \"authorsByline\": \"Vismaya V\",\n \"articleId\": \"680d7893185c4357a9047332cf38206c\",\n \"clusterId\": \"37426b0543cb45e3a547695fd63e9919\",\n \"source\": {\n \"domain\": \"decrypt.co\",\n \"paywall\": false,\n \"location\": {\n \"country\": \"us\",\n \"state\": \"NY\",\n \"city\": \"New York\",\n \"coordinates\": {\n \"lat\": 40.7127281,\n \"lon\": -74.0060152\n }\n }\n },\n \"imageUrl\": \"https://cdn.decrypt.co/resize/1024/height/512/wp-content/uploads/2021/07/The-Bored-Ape-Yacht-Club-gID_7.jpeg\",\n \"country\": \"us\",\n \"language\": \"en\",\n \"pubDate\": \"2025-01-15T09:20:34+00:00\",\n \"addDate\": \"2025-01-15T09:27:57.156032+00:00\",\n \"refreshDate\": \"2025-01-15T09:27:57.156034+00:00\",\n \"score\": 52.79721,\n \"title\": \"NFT Market Hits Three-Year Low in Trading and Sales: Report\",\n \"description\": \"Annual NFT trading volumes fell by 19%, while sales counts dipped by 18% compared to 2023, according to DappRadar.\",\n \"content\": \"The NFT market suffered a dismal 2024, with trading volumes and sales counts dropping to their weakest levels since 2020.\\n\\nAnnual trading volumes fell by 19%, while sales counts dipped by 18% compared to 2023, according to a report by blockchain analytics platform.\\n\\nDespite a surge in crypto market activity, driven by Bitcoin’s all-time highs and booming DeFi growth, NFTs appeared to struggle under the weight of their own inflated valuations.\\n\\nEarly in the year, NFT trading volumes reached $5.3 billion in Q1, a modest 4% increase compared to the same period in 2023.\\n\\nHowever, this momentum proved fleeting, as volumes plummeted to $1.5 billion in Q3 before recovering slightly to $2.6 billion in Q4.\\n\\nEven with these fluctuations, annual sales counts fell sharply, pointing to a broader trend: while individual NFTs became more expensive in line with rising crypto token prices, overall market engagement dwindled.\\n\\nYuga Labs’ flagship collections Bored Ape Yacht Club (BAYC) and Mutant Ape Yacht Club (MAYC) hit historic lows, with floor prices dropping to 15 ETH and 2.4 ETH, respectively.\\n\\nEven Otherdeeds for Yuga Labs' Otherside metaverse plummeted to 0.23 ETH, a far cry from their initial minting price, exposing cracks in Yuga’s high-priced, membership-driven model.\\n\\nThis coincided with DappRadar’s observation that “Perhaps 2024 helped us realize that NFTs don’t need to be expensive to prove their importance in the broader Web3 ecosystem,” a critique of the market’s reliance on exclusivity and inflated pricing.\\n\\nAmid this downturn, the NFT market witnessed a paradox in November when CryptoPunk #8348, a rare seven-trait collectible from the NFT collection, was collateralized for a $2.75 million loan via the NFT lending platform GONDI.\\n\\nTouted as a milestone for NFTs as financial assets, this event showed speculative excess when juxtaposed with DappRadar’s insights about affordability and utility.\\n\\nWhile high-profile transactions like this aim to affirm NFTs’ value, they also highlight a market still driven by exclusivity and inflated pricing, even as wider participation wanes.\\n\\nEven within the struggling sector, blue-chip collections like CryptoPunks defied trends, nearly doubling in USD value in 2024 with notable sales driving brief recovery periods.\\n\\nNFT platforms like Blur dominated marketplace activity, leveraging zero-fee trading and aggressive airdrop campaigns to capture the largest share of trading volumes.\\n\\nIn contrast, rival marketplace OpenSea struggled with regulatory headwinds and declining market sentiment, forcing significant layoffs by year-end.\\n\\nBy Q4, Blur and OpenSea were neck-and-neck in market share, but Blur’s ability to generate high activity from a smaller, more active user base gave it the edge, as per the report.\\n\\nWhile trading volumes in late 2024 hinted at a potential recovery—November sales hit $562 million, the highest since May—the overall trajectory suggests that affordability, accessibility, and utility will be critical for sustained growth in 2025.\",\n \"medium\": \"Article\",\n \"links\": [\n \"https://dappradar.com/blog/dapp-industry-report-2024-overview\",\n \"https://decrypt.co/resources/what-is-the-metaverse-immersive-nft-virtual-world\",\n \"https://decrypt.co/resources/non-fungible-tokens-nfts-explained-guide-learn-blockchain\",\n \"https://decrypt.co/247932/sec-coming-after-opensea-nfts-could-be-in-trouble\",\n \"https://share.flipboard.com/bookmarklet/popout?v=2&url=https://decrypt.co/301053/nft-market-hits-three-year-low-in-trading-and-sales-report\",\n \"https://decrypt.co/resources/what-are-cryptopunks-ethereum-nft-avatars\",\n \"https://decrypt.co/291710/cryptopunk-prices-surge-bitcoin-gains\",\n \"https://decrypt.co/scene\",\n \"https://decrypt.co/resources/what-is-bored-ape-yacht-club-the-celebrity-nft-of-choice\",\n \"https://decrypt.co/247930/if-you-bought-bored-ape-nfts-at-the-peak-youve-lost-93-of-your-investment\",\n \"https://decrypt.co/293754/someone-just-took-out-a-2-75-million-loan-against-this-one-of-one-cryptopunk-nft\"\n ],\n \"labels\": [\n {\n \"name\": \"Roundup\"\n }\n ],\n \"matchedAuthors\": [\n {\n \"id\": null,\n \"name\": \"Vismaya V\"\n }\n ],\n \"claim\": \"\",\n \"verdict\": \"\",\n \"keywords\": [\n {\n \"name\": \"NFT trading volumes\",\n \"weight\": 0.095650345\n },\n {\n \"name\": \"NFT Market\",\n \"weight\": 0.09479949\n },\n {\n \"name\": \"crypto market activity\",\n \"weight\": 0.09068515\n },\n {\n \"name\": \"Annual trading volumes\",\n \"weight\": 0.08782178\n },\n {\n \"name\": \"trading volumes\",\n \"weight\": 0.087688774\n },\n {\n \"name\": \"market share\",\n \"weight\": 0.08508381\n },\n {\n \"name\": \"overall market engagement\",\n \"weight\": 0.08292204\n },\n {\n \"name\": \"declining market sentiment\",\n \"weight\": 0.080258906\n },\n {\n \"name\": \"annual sales counts\",\n \"weight\": 0.07653589\n },\n {\n \"name\": \"sales counts\",\n \"weight\": 0.07410056\n }\n ],\n \"topics\": [],\n \"categories\": [],\n \"taxonomies\": [\n {\n \"name\": \"/Finance/Investing/Currencies & Foreign Exchange\"\n },\n {\n \"name\": \"/News/Business News/Financial Markets News\"\n }\n ],\n \"entities\": [\n {\n \"data\": \"Yuga Labs’\",\n \"type\": \"ORG\",\n \"mentions\": 3\n },\n {\n \"data\": \"Bored Ape Yacht Club\",\n \"type\": \"ORG\",\n \"mentions\": 1\n },\n {\n \"data\": \"BAYC\",\n \"type\": \"ORG\",\n \"mentions\": 1\n },\n {\n \"data\": \"Mutant Ape Yacht Club\",\n \"type\": \"ORG\",\n \"mentions\": 1\n },\n {\n \"data\": \"MAYC\",\n \"type\": \"ORG\",\n \"mentions\": 1\n },\n {\n \"data\": \"DappRadar\",\n \"type\": \"ORG\",\n \"mentions\": 2\n },\n {\n \"data\": \"NFT\",\n \"type\": \"ORG\",\n \"mentions\": 4\n },\n {\n \"data\": \"GONDI\",\n \"type\": \"ORG\",\n \"mentions\": 1\n },\n {\n \"data\": \"Blur\",\n \"type\": \"ORG\",\n \"mentions\": 3\n },\n {\n \"data\": \"OpenSea\",\n \"type\": \"ORG\",\n \"mentions\": 2\n },\n {\n \"data\": \"Otherside\",\n \"type\": \"PRODUCT\",\n \"mentions\": 1\n },\n {\n \"data\": \"CryptoPunks\",\n \"type\": \"PRODUCT\",\n \"mentions\": 1\n }\n ],\n \"companies\": [\n {\n \"id\": \"f0886b1323a142df88f521ba9a0bbedf\",\n \"name\": \"OpenSea Ventures\",\n \"domains\": [\n \"opensea.io\"\n ],\n \"symbols\": []\n },\n {\n \"id\": \"bee2238a04fe4f228252ea641c3403a7\",\n \"name\": \"NFT Labs\",\n \"domains\": [\n \"nftlabs.to\"\n ],\n \"symbols\": []\n }\n ],\n \"sentiment\": {\n \"positive\": 0.033333376,\n \"negative\": 0.89130986,\n \"neutral\": 0.07535672\n },\n \"summary\": \"The National Finance Finance (NFT) market experienced a three-year low in 2024, with annual trading volumes and sales counts dropping to their lowest levels since 2020. The report by blockchain analytics platform DappRadar suggests that despite a surge in crypto market activity driven by Bitcoin's all-time highs and DeFi growth, NFTs struggled under the weight of their inflated valuations. Despite early Q1 trading volumes reaching $5.3 billion, a 4% increase compared to the same period in 2023, volumes dropped to $1.5 billion in Q3 before recovering to $2.6 billion by Q4. Despite these fluctuations, overall market engagement decreased. Despite this downturn, high-profile transactions like CryptoPunk #8348, a rare seven-trait collectible from the NFT collection, were seen as a milestone for NFT's value and highlighted a market still driven by exclusivity and inflated pricing.\",\n \"translation\": \"\",\n \"translatedTitle\": \"\",\n \"translatedDescription\": \"\",\n \"translatedSummary\": \"\",\n \"locations\": [],\n \"reprint\": false,\n \"reprintGroupId\": \"3d67e5bf389b40b3895ae0708f4dd208\",\n \"places\": [],\n \"people\": []\n }\n ]\n}"
+ }
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200,
+ "default": 0
+ },
+ "numResults": {
+ "type": "integer",
+ "example": 1,
+ "default": 0
+ },
+ "articles": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string",
+ "example": "https://decrypt.co/301053/nft-market-hits-three-year-low-in-trading-and-sales-report"
+ },
+ "authorsByline": {
+ "type": "string",
+ "example": "Vismaya V"
+ },
+ "articleId": {
+ "type": "string",
+ "example": "680d7893185c4357a9047332cf38206c"
+ },
+ "clusterId": {
+ "type": "string",
+ "example": "37426b0543cb45e3a547695fd63e9919"
+ },
+ "source": {
+ "type": "object",
+ "properties": {
+ "domain": {
+ "type": "string",
+ "example": "decrypt.co"
+ },
+ "paywall": {
+ "type": "boolean",
+ "example": false,
+ "default": true
+ },
+ "location": {
+ "type": "object",
+ "properties": {
+ "country": {
+ "type": "string",
+ "example": "us"
+ },
+ "state": {
+ "type": "string",
+ "example": "NY"
+ },
+ "city": {
+ "type": "string",
+ "example": "New York"
+ },
+ "coordinates": {
+ "type": "object",
+ "properties": {
+ "lat": {
+ "type": "number",
+ "example": 40.7127281,
+ "default": 0
+ },
+ "lon": {
+ "type": "number",
+ "example": -74.0060152,
+ "default": 0
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "imageUrl": {
+ "type": "string",
+ "example": "https://cdn.decrypt.co/resize/1024/height/512/wp-content/uploads/2021/07/The-Bored-Ape-Yacht-Club-gID_7.jpeg"
+ },
+ "country": {
+ "type": "string",
+ "example": "us"
+ },
+ "language": {
+ "type": "string",
+ "example": "en"
+ },
+ "pubDate": {
+ "type": "string",
+ "example": "2025-01-15T09:20:34+00:00"
+ },
+ "addDate": {
+ "type": "string",
+ "example": "2025-01-15T09:27:57.156032+00:00"
+ },
+ "refreshDate": {
+ "type": "string",
+ "example": "2025-01-15T09:27:57.156034+00:00"
+ },
+ "score": {
+ "type": "number",
+ "example": 52.79721,
+ "default": 0
+ },
+ "title": {
+ "type": "string",
+ "example": "NFT Market Hits Three-Year Low in Trading and Sales: Report"
+ },
+ "description": {
+ "type": "string",
+ "example": "Annual NFT trading volumes fell by 19%, while sales counts dipped by 18% compared to 2023, according to DappRadar."
+ },
+ "content": {
+ "type": "string",
+ "example": "The NFT market suffered a dismal 2024, with trading volumes and sales counts dropping to their weakest levels since 2020.\n\nAnnual trading volumes fell by 19%, while sales counts dipped by 18% compared to 2023, according to a report by blockchain analytics platform.\n\nDespite a surge in crypto market activity, driven by Bitcoin’s all-time highs and booming DeFi growth, NFTs appeared to struggle under the weight of their own inflated valuations.\n\nEarly in the year, NFT trading volumes reached $5.3 billion in Q1, a modest 4% increase compared to the same period in 2023.\n\nHowever, this momentum proved fleeting, as volumes plummeted to $1.5 billion in Q3 before recovering slightly to $2.6 billion in Q4.\n\nEven with these fluctuations, annual sales counts fell sharply, pointing to a broader trend: while individual NFTs became more expensive in line with rising crypto token prices, overall market engagement dwindled.\n\nYuga Labs’ flagship collections Bored Ape Yacht Club (BAYC) and Mutant Ape Yacht Club (MAYC) hit historic lows, with floor prices dropping to 15 ETH and 2.4 ETH, respectively.\n\nEven Otherdeeds for Yuga Labs' Otherside metaverse plummeted to 0.23 ETH, a far cry from their initial minting price, exposing cracks in Yuga’s high-priced, membership-driven model.\n\nThis coincided with DappRadar’s observation that “Perhaps 2024 helped us realize that NFTs don’t need to be expensive to prove their importance in the broader Web3 ecosystem,” a critique of the market’s reliance on exclusivity and inflated pricing.\n\nAmid this downturn, the NFT market witnessed a paradox in November when CryptoPunk #8348, a rare seven-trait collectible from the NFT collection, was collateralized for a $2.75 million loan via the NFT lending platform GONDI.\n\nTouted as a milestone for NFTs as financial assets, this event showed speculative excess when juxtaposed with DappRadar’s insights about affordability and utility.\n\nWhile high-profile transactions like this aim to affirm NFTs’ value, they also highlight a market still driven by exclusivity and inflated pricing, even as wider participation wanes.\n\nEven within the struggling sector, blue-chip collections like CryptoPunks defied trends, nearly doubling in USD value in 2024 with notable sales driving brief recovery periods.\n\nNFT platforms like Blur dominated marketplace activity, leveraging zero-fee trading and aggressive airdrop campaigns to capture the largest share of trading volumes.\n\nIn contrast, rival marketplace OpenSea struggled with regulatory headwinds and declining market sentiment, forcing significant layoffs by year-end.\n\nBy Q4, Blur and OpenSea were neck-and-neck in market share, but Blur’s ability to generate high activity from a smaller, more active user base gave it the edge, as per the report.\n\nWhile trading volumes in late 2024 hinted at a potential recovery—November sales hit $562 million, the highest since May—the overall trajectory suggests that affordability, accessibility, and utility will be critical for sustained growth in 2025."
+ },
+ "medium": {
+ "type": "string",
+ "example": "Article"
+ },
+ "links": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "example": "https://dappradar.com/blog/dapp-industry-report-2024-overview"
+ }
+ },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "Roundup"
+ }
+ }
+ }
+ },
+ "matchedAuthors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {},
+ "name": {
+ "type": "string",
+ "example": "Vismaya V"
+ }
+ }
+ }
+ },
+ "claim": {
+ "type": "string",
+ "example": ""
+ },
+ "verdict": {
+ "type": "string",
+ "example": ""
+ },
+ "keywords": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "NFT trading volumes"
+ },
+ "weight": {
+ "type": "number",
+ "example": 0.095650345,
+ "default": 0
+ }
+ }
+ }
+ },
+ "topics": {
+ "type": "array"
+ },
+ "categories": {
+ "type": "array"
+ },
+ "taxonomies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "/Finance/Investing/Currencies & Foreign Exchange"
+ }
+ }
+ }
+ },
+ "entities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "string",
+ "example": "Yuga Labs’"
+ },
+ "type": {
+ "type": "string",
+ "example": "ORG"
+ },
+ "mentions": {
+ "type": "integer",
+ "example": 3,
+ "default": 0
+ }
+ }
+ }
+ },
+ "companies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "f0886b1323a142df88f521ba9a0bbedf"
+ },
+ "name": {
+ "type": "string",
+ "example": "OpenSea Ventures"
+ },
+ "domains": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "example": "opensea.io"
+ }
+ },
+ "symbols": {
+ "type": "array"
+ }
+ }
+ }
+ },
+ "sentiment": {
+ "type": "object",
+ "properties": {
+ "positive": {
+ "type": "number",
+ "example": 0.033333376,
+ "default": 0
+ },
+ "negative": {
+ "type": "number",
+ "example": 0.89130986,
+ "default": 0
+ },
+ "neutral": {
+ "type": "number",
+ "example": 0.07535672,
+ "default": 0
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "example": "The National Finance Finance (NFT) market experienced a three-year low in 2024, with annual trading volumes and sales counts dropping to their lowest levels since 2020. The report by blockchain analytics platform DappRadar suggests that despite a surge in crypto market activity driven by Bitcoin's all-time highs and DeFi growth, NFTs struggled under the weight of their inflated valuations. Despite early Q1 trading volumes reaching $5.3 billion, a 4% increase compared to the same period in 2023, volumes dropped to $1.5 billion in Q3 before recovering to $2.6 billion by Q4. Despite these fluctuations, overall market engagement decreased. Despite this downturn, high-profile transactions like CryptoPunk #8348, a rare seven-trait collectible from the NFT collection, were seen as a milestone for NFT's value and highlighted a market still driven by exclusivity and inflated pricing."
+ },
+ "translation": {
+ "type": "string",
+ "example": ""
+ },
+ "translatedTitle": {
+ "type": "string",
+ "example": ""
+ },
+ "translatedDescription": {
+ "type": "string",
+ "example": ""
+ },
+ "translatedSummary": {
+ "type": "string",
+ "example": ""
+ },
+ "locations": {
+ "type": "array"
+ },
+ "reprint": {
+ "type": "boolean",
+ "example": false,
+ "default": true
+ },
+ "reprintGroupId": {
+ "type": "string",
+ "example": "3d67e5bf389b40b3895ae0708f4dd208"
+ },
+ "places": {
+ "type": "array"
+ },
+ "people": {
+ "type": "array"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "400",
+ "content": {
+ "text/plain": {
+ "examples": {
+ "Result": {
+ "value": "The request has bad formatting or missing parameters"
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "401",
+ "content": {
+ "text/plain": {
+ "examples": {
+ "Result": {
+ "value": "No API key was provided in the request"
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "403",
+ "content": {
+ "text/plain": {
+ "examples": {
+ "Result": {
+ "value": "Request was forbidden, likely due to plan restrictions or account status"
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "404",
+ "content": {
+ "text/plain": {
+ "examples": {
+ "Result": {
+ "value": "The requested resource was not found"
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "500",
+ "content": {
+ "text/plain": {
+ "examples": {
+ "Result": {
+ "value": "Some internal error has occurred, please contact support"
+ }
+ }
+ }
+ }
+ }
+ },
+ "deprecated": false,
+ "security": [],
+ "x-readme": {
+ "code-samples": [
+ {
+ "language": "curl",
+ "code": "$ curl 'https://api.goperigon.com/v1/all?category=Business&sourceGroup=top100&showReprints=false&apiKey=API_KEY'"
+ },
+ {
+ "language": "python",
+ "code": "import requests\n\nAPI_KEY = \"API_KEY\"\nurl = f\"https://api.goperigon.com/v1/all?category=Business&sourceGroup=top100&showReprints=false&apiKey={API_KEY}\"\n\nresp = requests.get(url)\n\nprint(resp.json())"
+ },
+ {
+ "language": "ruby",
+ "code": "require 'net/http'\n\napi_key = \"API_KEY\"\nurl = URI.parse(\"https://api.goperigon.com/v1/all?category=Business&sourceGroup=top100&showReprints=false&apiKey=#{api_key}\")\nres = Net::HTTP.get(URI.parse(url.to_s))\n\nputs res"
+ },
+ {
+ "language": "go",
+ "code": "package main\n\nimport (\n \"fmt\"\n \"net/http\"\n \"io/ioutil\"\n)\n\nfunc main() {\n key := \"API_KEY\"\n url := \"https://api.goperigon.com/v1/all?category=Business&sourceGroup=top100&showReprints=false&apiKey=\" + key \n res, err := http.Get(url)\n if err != nil {\n fmt.Println(err)\n }\n \n defer res.Body.Close()\n body, err := ioutil.ReadAll(res.Body)\n if err != nil {\n fmt.Println(err)\n }\n \n fmt.Println(string(body))\n}"
+ },
+ {
+ "language": "javascript",
+ "code": "const axios = require('axios');\nconst apiKey = \"API_KEY\"\nconst url = `https://api.goperigon.com/v1/all?category=Business&sourceGroup=top100&showReprints=false&apiKey=${apiKey}`\n\naxios.get(url)\n .then((response) => {\n console.log(response);\n })\n .catch((error) => {\n console.log(error);\n });"
+ }
+ ],
+ "samples-languages": [
+ "curl",
+ "python",
+ "ruby",
+ "go",
+ "javascript"
+ ]
+ }
+ }
+ },
+ "/v1/stories/all": {
+ "get": {
+ "summary": "Stories",
+ "description": "",
+ "operationId": "stories-1",
+ "parameters": [
+ {
+ "name": "apiKey",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "q",
+ "in": "query",
+ "description": "Search story by name, summary and key points. Preference is given to the name field. Supports complex query syntax, same way as **q** parameter from **/all** endpoint.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "name",
+ "in": "query",
+ "description": "Search story by name. Supports complex query syntax, same way as **q** parameter from **/all** endpoint.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "topic",
+ "in": "query",
+ "description": "Filter by topics. Each topic is some kind of entity that the article is about. Examples of topics: Markets, Joe Biden, Green Energy, Climate Change, Cryptocurrency, etc. If multiple parameters are passed, they will be applied as OR operations. More ➜",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "category",
+ "in": "query",
+ "description": "Filter by categories. Categories are general themes that the article is about. Examples of categories: Tech, Politics, etc. If multiple parameters are passed, they will be applied as OR operations. Use 'none' to search uncategorized articles. More ➜",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "Politics",
+ "Tech",
+ "Sports",
+ "Business",
+ "Finance",
+ "Entertainment",
+ "Health",
+ "Weather",
+ "Lifestyle",
+ "Auto",
+ "Science",
+ "Travel",
+ "Environment",
+ "World",
+ "General",
+ "none"
+ ]
+ }
+ },
+ {
+ "name": "clusterId",
+ "in": "query",
+ "description": "Filter to specific story. Passing a cluster ID will filter results to only the content found within the cluster. Multiple params could be passed.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "source",
+ "in": "query",
+ "description": "Filter stories by sources that wrote articles belonging to this story. At least 1 article is required for story to match. Multiple parameters could be passed.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "sourceGroup",
+ "in": "query",
+ "description": "Filter stories by sources that wrote articles belonging to this story. Source groups are expanded into a list of sources. At least 1 article by the source is required for story to match. Multiple params could be passed.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "personWikidataId",
+ "in": "query",
+ "description": "List of person Wikidata IDs for filtering. Filter is applied on topPeople field.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "personName",
+ "in": "query",
+ "description": "List of people names. Filtering is applied on topPeople field.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companyId",
+ "in": "query",
+ "description": "List of company IDs for filtering. Filtering is applied to topCompanies field.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companyName",
+ "in": "query",
+ "description": "List of company names for filtering. Filtering is applied on topCompanies field.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companyDomain",
+ "in": "query",
+ "description": "List of company domains for filtering. Filtering is applied on topCompanies field.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "companySymbol",
+ "in": "query",
+ "description": "List of company tickers for filtering. Filtering is applied on topCompanies field.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "nameExists",
+ "in": "query",
+ "description": "Returns stories with name assigned. Defaults to true.",
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ {
+ "name": "from",
+ "in": "query",
+ "description": "'from' filter, will search stories created after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2023-03-01T00:00:00",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "to",
+ "in": "query",
+ "description": "'to' filter, will search stories created before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2023-03-01T23:59:59",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ {
+ "name": "initializedFrom",
+ "in": "query",
+ "description": "'initializedFrom' filter, will search stories that became available after provided date",
+ "schema": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ {
+ "name": "initializedTo",
+ "in": "query",
+ "description": "'initializedTo' filter, will search stories that became available before provided date",
+ "schema": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ {
+ "name": "updatedFrom",
+ "in": "query",
+ "description": "Will return stories with 'updatedAt' >= 'updatedFrom'.",
+ "schema": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ {
+ "name": "updatedTo",
+ "in": "query",
+ "description": "Will return stories with 'updatedAt' <= 'updatedTo'.",
+ "schema": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ {
+ "name": "minClusterSize",
+ "in": "query",
+ "description": "Filter by minimum cluster size. Minimum cluster size filter applies to number of **unique** articles.",
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 5
+ }
+ },
+ {
+ "name": "maxClusterSize",
+ "in": "query",
+ "description": "Filter by maximum cluster size. Maximum cluster size filter applies to number of **unique** articles in the cluster.",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ {
+ "name": "state",
+ "in": "query",
+ "description": "Filter local news by state. Applies only to local news, when this param is passed non-local news will not be returned. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "city",
+ "in": "query",
+ "description": "Filter local news by city. Applies only to local news, when this param is passed non-local news will not be returned. If multiple parameters are passed, they will be applied as OR operations. More ➜",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "area",
+ "in": "query",
+ "description": "Filter local news by area. Applies only to local news, when this param is passed non-local news will not be returned. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "Zero-based page number. From 0 to 10000. See the Pagination section for limitations.",
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 0
+ }
+ },
+ {
+ "name": "size",
+ "in": "query",
+ "description": "Number of stories results per page, from 0 to 100.",
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 10
+ }
+ },
+ {
+ "name": "sortBy",
+ "in": "query",
+ "description": "Sort stories by count, creation date, last updated date. By default is sorted by created at.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "createdAt",
+ "updatedAt",
+ "count"
+ ],
+ "default": "createdAt"
+ }
+ },
+ {
+ "name": "showNumResults",
+ "in": "query",
+ "description": "Show total number of results. By default set to false, will cap result count at 10000.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "name": "showDuplicates",
+ "in": "query",
+ "description": "Stories are deduplicated by default. If a story is deduplicated, all future articles are merged into the original story. *duplicateOf* field contains the original cluster Id. When *showDuplicates=true*, all stories are shown.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "name": "positiveSentimentFrom",
+ "in": "query",
+ "description": "Filters results with a sentiment score greater than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "positiveSentimentTo",
+ "in": "query",
+ "description": "Filters results with a sentiment score less than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "neutralSentimentFrom",
+ "in": "query",
+ "description": "Filters results with a sentiment score greater than or equal to the specified value, indicating neutral sentiment. Explanation of sentimental values can be found in Docs under the Article Data section.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "neutralSentimentTo",
+ "in": "query",
+ "description": "Filters results with a sentiment score less than or equal to the specified value, indicating neutral sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "negativeSentimentFrom",
+ "in": "query",
+ "description": "Filters results with a sentiment score greater than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "negativeSentimentTo",
+ "in": "query",
+ "description": "Filters results with a sentiment score less than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores.",
+ "schema": {
+ "type": "number",
+ "format": "float"
+ }
+ },
+ {
+ "name": "country",
+ "in": "query",
+ "description": "Country code to filter by country. If multiple parameters are passed, they will be applied as OR operations.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "us",
+ "gb",
+ "de",
+ "it",
+ "fr",
+ "nl",
+ "se",
+ "dk",
+ "fi",
+ "hu",
+ "no",
+ "pl",
+ "pt",
+ "ru",
+ "ua",
+ "ch",
+ "br",
+ "nz",
+ "mx",
+ "au"
+ ]
+ }
+ },
+ {
+ "name": "taxonomy",
+ "in": "query",
+ "description": "Filters by Google Content Categories. This field will accept 1 or more categories, must pass the full name of the category. Example: taxonomy=/Finance/Banking/Other, /Finance/Investing/Funds",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "topTaxonomies",
+ "in": "query",
+ "description": "Filters by Google Content Categories. Highlights the top 3 categories in a cluster, ordered by count.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "200",
+ "content": {
+ "application/json": {
+ "examples": {
+ "Result": {
+ "value": "{\n \"status\": 200,\n \"numResults\": 18,\n \"results\": [\n {\n \"createdAt\": \"2025-01-29T08:11:03.602007+00:00\",\n \"updatedAt\": \"2025-01-29T14:46:26.744051+00:00\",\n \"initializedAt\": \"2025-01-29T08:42:16.214884+00:00\",\n \"id\": \"dc0612a3ff094e94be93f5713db2a940\",\n \"name\": \"Norway's Wealth Fund Reports Record $222 Billion Profit\",\n \"summary\": \"Norway's $1.8 trillion sovereign wealth fund announced a record annual profit of 2.51 trillion crowns ($222 billion) for 2024, primarily driven by a strong performance in the technology sector. CEO Nicolai Tangen noted that American tech giants like Apple, Microsoft, and Nvidia played a crucial role in this success, contributing to an overall return on investment of 13%. Despite this impressive profit, state inflows into the fund were lower than previous records, and returns from fixed income and real estate investments lagged. The fund, which averages 1.5% ownership of publicly listed stocks worldwide, continues to leverage revenues from Norway's oil and gas production. This milestone underscores the fund's strategic importance in bolstering Norway's economic position globally. Overall, the tech rally significantly enhanced the fund's financial standing in 2024.\",\n \"summaryReferences\": [\n \"5e5c0b136c214f588e4bcc18d9bcc1bc\",\n \"ffe6384f9f214f3782d99f06d56af619\",\n \"ac1002cc44f44632b4a29e2152e895cb\",\n \"f3dac6be5b5d42aa8a0c49f6c4fff237\",\n \"68c97189db94453597766a8b6bb6faf0\"\n ],\n \"keyPoints\": [\n {\n \"point\": \"Nicolai Tangen noted, 'Despite the impressive profit, the return on investment was slightly below our benchmark index,' indicating a need for improvement in future results.\",\n \"references\": [\n \"5e5c0b136c214f588e4bcc18d9bcc1bc\"\n ]\n },\n {\n \"point\": \"The fund's equity investments yielded an 18% return, while fixed income investments only saw a 1% increase, showcasing a stark contrast in performance across asset classes.\",\n \"references\": [\n \"ffe6384f9f214f3782d99f06d56af619\"\n ]\n },\n {\n \"point\": \"State inflows into the fund amounted to 402 billion crowns, highlighting a decrease compared to the previous record high of 2022, which affects future investment capabilities.\",\n \"references\": [\n \"ac1002cc44f44632b4a29e2152e895cb\"\n ]\n }\n ],\n \"sentiment\": {\n \"positive\": 0.7317738,\n \"negative\": 0.11641231,\n \"neutral\": 0.15181391\n },\n \"uniqueCount\": 15,\n \"reprintCount\": 14,\n \"totalCount\": 29,\n \"countries\": [\n {\n \"name\": \"in\",\n \"count\": 6\n },\n {\n \"name\": \"us\",\n \"count\": 14\n },\n {\n \"name\": \"ca\",\n \"count\": 3\n },\n {\n \"name\": \"ie\",\n \"count\": 1\n },\n {\n \"name\": \"am\",\n \"count\": 1\n },\n {\n \"name\": \"ae\",\n \"count\": 1\n },\n {\n \"name\": \"my\",\n \"count\": 1\n },\n {\n \"name\": \"ph\",\n \"count\": 1\n }\n ],\n \"topCountries\": [\n \"us\"\n ],\n \"topics\": [\n {\n \"name\": \"Odd News\",\n \"count\": 1\n },\n {\n \"name\": \"Markets\",\n \"count\": 5\n },\n {\n \"name\": \"Economy\",\n \"count\": 1\n }\n ],\n \"topTopics\": [\n {\n \"name\": \"Markets\"\n }\n ],\n \"categories\": [\n {\n \"name\": \"Business\",\n \"count\": 7\n },\n {\n \"name\": \"Finance\",\n \"count\": 7\n },\n {\n \"name\": \"World\",\n \"count\": 1\n }\n ],\n \"topCategories\": [\n {\n \"name\": \"Business\"\n },\n {\n \"name\": \"Finance\"\n }\n ],\n \"taxonomies\": [\n {\n \"name\": \"/News/Business News/Financial Markets News\",\n \"count\": 26\n },\n {\n \"name\": \"/Finance/Investing/Other\",\n \"count\": 26\n },\n {\n \"name\": \"/News/Business News/Other\",\n \"count\": 23\n },\n {\n \"name\": \"/News/Business News/Company News\",\n \"count\": 26\n },\n {\n \"name\": \"/Finance/Investing/Funds\",\n \"count\": 21\n },\n {\n \"name\": \"/News/Business News/Economy News\",\n \"count\": 7\n },\n {\n \"name\": \"/Finance/Investing/Stocks & Bonds\",\n \"count\": 25\n },\n {\n \"name\": \"/Finance/Financial Planning & Management/Asset & Portfolio Management\",\n \"count\": 1\n },\n {\n \"name\": \"/News/Technology News\",\n \"count\": 2\n }\n ],\n \"topTaxonomies\": [\n {\n \"name\": \"/News/Business News/Financial Markets News\"\n },\n {\n \"name\": \"/Finance/Investing/Other\"\n },\n {\n \"name\": \"/News/Business News/Company News\"\n },\n {\n \"name\": \"/Finance/Investing/Stocks & Bonds\"\n },\n {\n \"name\": \"/News/Business News/Other\"\n },\n {\n \"name\": \"/Finance/Investing/Funds\"\n }\n ],\n \"people\": [\n {\n \"wikidataId\": \"Q56408252\",\n \"name\": \"Nicolai Tangen\",\n \"count\": 24\n },\n {\n \"wikidataId\": \"Q22686\",\n \"name\": \"Donald Trump\",\n \"count\": 2\n }\n ],\n \"topPeople\": [\n {\n \"wikidataId\": \"Q56408252\",\n \"name\": \"Nicolai Tangen\"\n }\n ],\n \"companies\": [\n {\n \"id\": \"4de51cf8472b4ce7a5aecdf52a1de4c0\",\n \"name\": \"Microsoft Corporation\",\n \"domains\": [\n \"microsoft.com\"\n ],\n \"symbols\": [\n \"4338.HK\",\n \"MSF.BR\",\n \"MSF.DE\",\n \"MSFT\"\n ],\n \"count\": 2\n },\n {\n \"id\": \"806457af1fd9418db78388760f52c06f\",\n \"name\": \"Alphabet Inc.\",\n \"domains\": [\n \"abc.xyz\"\n ],\n \"symbols\": [\n \"ABEA.DE\",\n \"ABEC.DE\",\n \"GOOG\",\n \"GOOGL\",\n \"GOOGL.SW\"\n ],\n \"count\": 1\n },\n {\n \"id\": \"94bbd0512cd04cc4b7041d78d9d4cf73\",\n \"name\": \"NVIDIA Corporation\",\n \"domains\": [\n \"nvidia.com\"\n ],\n \"symbols\": [\n \"NVD.DE\",\n \"NVDA\"\n ],\n \"count\": 3\n }\n ],\n \"topCompanies\": [],\n \"locations\": [\n {\n \"count\": 5\n },\n {\n \"state\": \"03\",\n \"city\": \"Oslo\",\n \"count\": 9\n }\n ],\n \"topLocations\": [\n {\n \"state\": \"03\",\n \"city\": \"Oslo\"\n },\n {}\n ]\n },"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "400",
+ "content": {
+ "application/json": {
+ "examples": {
+ "Result": {
+ "value": "{\n \"status\": 400,\n \"message\": \"Errors during parameter validation: \\nError on parameter 'updatedFrom', invalid value 'test': Failed to convert property value of type 'java.lang.String' to required type 'java.time.OffsetDateTime' for property 'updatedFrom'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.gawq.api.formatting.DateTimeRequestParam java.time.OffsetDateTime] for value [test]; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [test]\",\n \"timestamp\": 1730827027255\n}"
+ }
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400,
+ "default": 0
+ },
+ "message": {
+ "type": "string",
+ "example": "Errors during parameter validation: \nError on parameter 'updatedFrom', invalid value 'test': Failed to convert property value of type 'java.lang.String' to required type 'java.time.OffsetDateTime' for property 'updatedFrom'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.gawq.api.formatting.DateTimeRequestParam java.time.OffsetDateTime] for value [test]; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [test]"
+ },
+ "timestamp": {
+ "type": "integer",
+ "example": 1730827027255,
+ "default": 0
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "deprecated": false,
+ "security": [],
+ "x-readme": {
+ "code-samples": [
+ {
+ "language": "curl",
+ "code": "$ curl 'https://api.goperigon.com/v1/stories/all?category=Business&from=2023-03-01&apiKey=API_KEY",
+ "name": "curl"
+ },
+ {
+ "language": "python",
+ "code": "import requests\n\nAPI_KEY = \"API_KEY\"\nurl = f\"https://api.goperigon.com/v1/all?category=Business&apiKey={API_KEY}\"\n\nresp = requests.get(url)\n\nprint(resp.json())",
+ "name": "python"
+ }
+ ],
+ "samples-languages": [
+ "curl",
+ "python"
+ ]
+ }
+ }
+ }
+ },
+ "x-readme": {
+ "headers": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ]
+ },
+ "x-readme-fauxas": true
+}
\ No newline at end of file
diff --git a/packages/perigon/package.json b/packages/perigon/package.json
new file mode 100644
index 0000000..c4a7686
--- /dev/null
+++ b/packages/perigon/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@web-agent-rs/perigon",
+ "version": "0.0.0",
+ "main": "index.ts",
+ "types": "./index.d.ts",
+ "type": "module",
+ "dependencies": {
+ "api": "^6.1.3",
+ "json-schema-to-ts": "^2.8.0-beta.0",
+ "oas": "^20.11.0"
+ }
+}
diff --git a/packages/perigon/schemas.ts b/packages/perigon/schemas.ts
new file mode 100644
index 0000000..7a0dc4c
--- /dev/null
+++ b/packages/perigon/schemas.ts
@@ -0,0 +1,5 @@
+const AllNews = {"metadata":{"allOf":[{"type":"object","properties":{"apiKey":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#"},"q":{"type":"string","default":"recall OR \"safety concern*\"","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search query, each article will be scored and ranked against it. Articles are searched on the title, description, and content fields. More ➜"},"title":{"type":"string","default":"tesla OR TSLA OR \"General Motors\" OR GM","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search article headlines/title field. Semantic similar to q parameter."},"desc":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search query on the description field. Semantic similar to q parameter."},"content":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search query on the article's body of content field. Semantic similar to q parameter."},"url":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search query on the url field. Semantic similar to q parameter. E.g. could be used for querying certain website sections, e.g. source=cnn.com&url=travel."},"articleId":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Article ID will search for a news article by the ID of the article. If several parameters are passed, all matched articles will be returned."},"clusterId":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search for related content using a cluster ID. Passing a cluster ID will filter results to only the content found within the cluster."},"from":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'from' filter, will search articles published after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2023-03-01T00:00:00"},"to":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'to' filter, will search articles published before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T23:59:59"},"addDateFrom":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'addDateFrom' filter, will search articles added after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T00:00:00"},"addDateTo":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'addDateTo' filter, will search articles added before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T23:59:59"},"refreshDateFrom":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Will search articles that were refreshed after the specified date. The date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T00:00:00"},"refreshDateTo":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Will search articles that were refreshed before the specified date. The date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2022-02-01T23:59:59"},"medium":{"type":"string","enum":["Article","Video"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Medium will filter out news articles medium, which could be 'Video' or 'Article'. If several parameters are passed, all matched articles will be returned."},"source":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Publisher's domain can include a subdomain. If multiple parameters are passed, they will be applied as OR operations. Wildcards (\\* and ?) are suported (e.g. \\*.cnn.com). More ➜"},"sourceGroup":{"type":"string","enum":["top10","top100","top25crypto","top25finance","top50tech","top100sports","top100leftUS","top100rightUS","top100centerUS"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"One of the supported source groups. Find Source Groups in the guided part of our documentation... More ➜"},"excludeSource":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"The domain of the website, which should be excluded from the search. Multiple parameters could be provided. Wildcards (\\* and ?) are suported (e.g. \\*.cnn.com)."},"paywall":{"type":"boolean","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter to show only results where the source has a paywall (true) or does not have a paywall (false)."},"byline":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Author names to filter by. Article author bylines are used as a source field. If multiple parameters are passed, they will be applied as OR operations."},"journalistId":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by journalist ID. Journalist IDs are unique journalist identifiers which can be found through the Journalist API, or in the matchedAuthors field. More ➜"},"language":{"type":"string","enum":["da","de","en","es","fi","fr","hu","it","nl","no","pl","pt","ru","sv","uk"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Language code to filter by language. If multiple parameters are passed, they will be applied as OR operations. More ➜"},"searchTranslation":{"type":"boolean","default":false,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Expand a query to search the translation, translatedTitle, and translatedDescription fields for non-English articles."},"label":{"type":"string","enum":["Opinion","Non-news","Paid News","Fact Check","Pop Culture","Roundup","Press Release","Low Content"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Labels to filter by, could be 'Opinion', 'Paid-news', 'Non-news', etc. If multiple parameters are passed, they will be applied as OR operations. More ➜"},"excludeLabel":{"type":"string","enum":["Opinion","Non-news","Paid News","Fact Check","Pop Culture","Roundup","Press Release","Low Content"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Exclude results that include specific labels (Opinion, Non-news, Paid News, etc.). You can filter multiple by repeating the parameter."},"category":{"type":"string","enum":["Politics","Tech","Sports","Business","Finance","Entertainment","Health","Weather","Lifestyle","Auto","Science","Travel","Environment","World","General","none"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by categories. Categories are general themes that the article is about. Examples of categories: Tech, Politics, etc. If multiple parameters are passed, they will be applied as OR operations. Use 'none' to search uncategorized articles. More ➜"},"topic":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by topics. Each topic is some kind of entity that the article is about. Examples of topics: Markets, Joe Biden, Green Energy, Climate Change, Cryptocurrency, etc. If multiple parameters are passed, they will be applied as OR operations. More ➜"},"excludeTopic":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by excluding topics. Each topic is some kind of entity that the article is about. Examples of topics: Markets, Joe Biden, Green Energy, Climate Change, Cryptocurrency, etc. If multiple parameters are passed, they will be applied as OR operations. More ➜"},"linkTo":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Returns only articles that point to specified links (as determined by the 'links' field in the article response)."},"showReprints":{"type":"boolean","default":true,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Whether to return reprints in the response or not. Reprints are usually wired articles from sources like AP or Reuters that are reprinted in multiple sources at the same time. By default, this parameter is 'true'."},"reprintGroupId":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Shows all articles belonging to the same reprint group. A reprint group includes one original article (the first one processed by the API) and all its known reprints."},"city":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters articles where a specified city plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the urban area in question. If multiple parameters are passed, they will be applied as OR operations."},"area":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters articles where a specified area, such as a neighborhood, borough, or district, plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the area in question. If multiple parameters are passed, they will be applied as OR operations."},"state":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters articles where a specified state plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the state in question. If multiple parameters are passed, they will be applied as OR operations."},"locationsCountry":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters articles where a specified country plays a central role in the content, beyond mere mentions, to ensure the results are deeply relevant to the country in question. If multiple parameters are passed, they will be applied as OR operations."},"excludeLocationsCountry":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Excludes articles where a specified country plays a central role in the content, ensuring results are not deeply relevant to the country in question. If multiple parameters are passed, they will be applied as AND operations, excluding articles relevant to any of the specified countries."},"location":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Return all articles that have the specified location. Location attributes are delimited by ':' between key and value, and '::' between attributes. Example: 'city:New York::state:NY'."},"lat":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Latitude of the center point to search places"},"lon":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Longitude of the center point to search places"},"maxDistance":{"type":"integer","format":"int32","minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Maximum distance (in km) from starting point to search articles by tagged places"},"sourceCity":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Find articles published by sources that are located within a given city."},"sourceCounty":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Find articles published by sources that are located within a given county."},"sourceCountry":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Find articles published by sources that are located within a given country. Must be 2 character country code (i.e. us, gb, etc)."},"country":{"type":"string","enum":["us","gb","de","it","fr","nl","se","dk","fi","hu","no","pl","pt","ru","ua","ch","br","nz","mx","au"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Country code to filter by country. If multiple parameters are passed, they will be applied as OR operations."},"sourceState":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Find articles published by sources that are located within a given state."},"sourceLon":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Latitude of the center point to search articles created by local publications."},"sourceLat":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Latitude of the center point to search articles created by local publications."},"sourceMaxDistance":{"type":"integer","format":"int32","minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Maximum distance from starting point to search articles created by local publications."},"personWikidataId":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of person Wikidata IDs for filtering."},"personName":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of person names for exact matches. Boolean and complex logic is not supported on this paramter."},"companyId":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of company IDs to filter by."},"companyName":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search by company name."},"companyDomain":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search by company domains for filtering. E.g. companyDomain=apple.com."},"companySymbol":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search by company symbols."},"page":{"type":"integer","format":"int32","minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Zero-based page number. From 0 to 10000. See the Pagination section for limitations."},"size":{"type":"integer","format":"int32","minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Number of articles returned per page, from 0 to 100."},"sortBy":{"type":"string","enum":["date","relevance","addDate","pubDate","refreshDate"],"default":"relevance","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'relevance' to sort by relevance to the query, 'date' to sort by the publication date (desc), 'pubDate' is a synonym to 'date', 'addDate' to sort by 'addDate' field (desc), 'refreshDate' to sort by 'refreshDate' field (desc)."},"showNumResults":{"type":"boolean","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Whether to show the total number of all matched articles. Default value is false which makes queries a bit more efficient but also counts up to 10000 articles."},"positiveSentimentFrom":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score greater than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores."},"positiveSentimentTo":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score less than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores."},"neutralSentimentFrom":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score greater than or equal to the specified value, indicating neutral sentiment. Explanation of sentimental values can be found in Docs under the Article Data section."},"neutralSentimentTo":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score less than or equal to the specified value, indicating neutral sentiment. See the Article Data section in Docs for an explanation of scores."},"negativeSentimentFrom":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score greater than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores."},"negativeSentimentTo":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score less than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores."},"taxonomy":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters by Google Content Categories. This field will accept 1 or more categories, must pass the full name of the category. Example: taxonomy=/Finance/Banking/Other, /Finance/Investing/Funds"},"prefixTaxonomy":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters by Google Content Categories. This field will filter by the category prefix only. Example: prefixTaxonomy=/Finance"}},"required":["apiKey"]}]},"response":{"200":{"type":"object","properties":{"status":{"type":"integer","default":0,"examples":[200]},"numResults":{"type":"integer","default":0,"examples":[1]},"articles":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","examples":["https://decrypt.co/301053/nft-market-hits-three-year-low-in-trading-and-sales-report"]},"authorsByline":{"type":"string","examples":["Vismaya V"]},"articleId":{"type":"string","examples":["680d7893185c4357a9047332cf38206c"]},"clusterId":{"type":"string","examples":["37426b0543cb45e3a547695fd63e9919"]},"source":{"type":"object","properties":{"domain":{"type":"string","examples":["decrypt.co"]},"paywall":{"type":"boolean","default":true,"examples":[false]},"location":{"type":"object","properties":{"country":{"type":"string","examples":["us"]},"state":{"type":"string","examples":["NY"]},"city":{"type":"string","examples":["New York"]},"coordinates":{"type":"object","properties":{"lat":{"type":"number","default":0,"examples":[40.7127281]},"lon":{"type":"number","default":0,"examples":[-74.0060152]}}}}}}},"imageUrl":{"type":"string","examples":["https://cdn.decrypt.co/resize/1024/height/512/wp-content/uploads/2021/07/The-Bored-Ape-Yacht-Club-gID_7.jpeg"]},"country":{"type":"string","examples":["us"]},"language":{"type":"string","examples":["en"]},"pubDate":{"type":"string","examples":["2025-01-15T09:20:34+00:00"]},"addDate":{"type":"string","examples":["2025-01-15T09:27:57.156032+00:00"]},"refreshDate":{"type":"string","examples":["2025-01-15T09:27:57.156034+00:00"]},"score":{"type":"number","default":0,"examples":[52.79721]},"title":{"type":"string","examples":["NFT Market Hits Three-Year Low in Trading and Sales: Report"]},"description":{"type":"string","examples":["Annual NFT trading volumes fell by 19%, while sales counts dipped by 18% compared to 2023, according to DappRadar."]},"content":{"type":"string","examples":["The NFT market suffered a dismal 2024, with trading volumes and sales counts dropping to their weakest levels since 2020.\n\nAnnual trading volumes fell by 19%, while sales counts dipped by 18% compared to 2023, according to a report by blockchain analytics platform.\n\nDespite a surge in crypto market activity, driven by Bitcoin’s all-time highs and booming DeFi growth, NFTs appeared to struggle under the weight of their own inflated valuations.\n\nEarly in the year, NFT trading volumes reached $5.3 billion in Q1, a modest 4% increase compared to the same period in 2023.\n\nHowever, this momentum proved fleeting, as volumes plummeted to $1.5 billion in Q3 before recovering slightly to $2.6 billion in Q4.\n\nEven with these fluctuations, annual sales counts fell sharply, pointing to a broader trend: while individual NFTs became more expensive in line with rising crypto token prices, overall market engagement dwindled.\n\nYuga Labs’ flagship collections Bored Ape Yacht Club (BAYC) and Mutant Ape Yacht Club (MAYC) hit historic lows, with floor prices dropping to 15 ETH and 2.4 ETH, respectively.\n\nEven Otherdeeds for Yuga Labs' Otherside metaverse plummeted to 0.23 ETH, a far cry from their initial minting price, exposing cracks in Yuga’s high-priced, membership-driven model.\n\nThis coincided with DappRadar’s observation that “Perhaps 2024 helped us realize that NFTs don’t need to be expensive to prove their importance in the broader Web3 ecosystem,” a critique of the market’s reliance on exclusivity and inflated pricing.\n\nAmid this downturn, the NFT market witnessed a paradox in November when CryptoPunk #8348, a rare seven-trait collectible from the NFT collection, was collateralized for a $2.75 million loan via the NFT lending platform GONDI.\n\nTouted as a milestone for NFTs as financial assets, this event showed speculative excess when juxtaposed with DappRadar’s insights about affordability and utility.\n\nWhile high-profile transactions like this aim to affirm NFTs’ value, they also highlight a market still driven by exclusivity and inflated pricing, even as wider participation wanes.\n\nEven within the struggling sector, blue-chip collections like CryptoPunks defied trends, nearly doubling in USD value in 2024 with notable sales driving brief recovery periods.\n\nNFT platforms like Blur dominated marketplace activity, leveraging zero-fee trading and aggressive airdrop campaigns to capture the largest share of trading volumes.\n\nIn contrast, rival marketplace OpenSea struggled with regulatory headwinds and declining market sentiment, forcing significant layoffs by year-end.\n\nBy Q4, Blur and OpenSea were neck-and-neck in market share, but Blur’s ability to generate high activity from a smaller, more active user base gave it the edge, as per the report.\n\nWhile trading volumes in late 2024 hinted at a potential recovery—November sales hit $562 million, the highest since May—the overall trajectory suggests that affordability, accessibility, and utility will be critical for sustained growth in 2025."]},"medium":{"type":"string","examples":["Article"]},"links":{"type":"array","items":{"type":"string","examples":["https://dappradar.com/blog/dapp-industry-report-2024-overview"]}},"labels":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","examples":["Roundup"]}}}},"matchedAuthors":{"type":"array","items":{"type":"object","properties":{"id":{},"name":{"type":"string","examples":["Vismaya V"]}}}},"claim":{"type":"string","examples":[""]},"verdict":{"type":"string","examples":[""]},"keywords":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","examples":["NFT trading volumes"]},"weight":{"type":"number","default":0,"examples":[0.095650345]}}}},"topics":{"type":"array","items":{}},"categories":{"type":"array","items":{}},"taxonomies":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","examples":["/Finance/Investing/Currencies & Foreign Exchange"]}}}},"entities":{"type":"array","items":{"type":"object","properties":{"data":{"type":"string","examples":["Yuga Labs’"]},"type":{"type":"string","examples":["ORG"]},"mentions":{"type":"integer","default":0,"examples":[3]}}}},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","examples":["f0886b1323a142df88f521ba9a0bbedf"]},"name":{"type":"string","examples":["OpenSea Ventures"]},"domains":{"type":"array","items":{"type":"string","examples":["opensea.io"]}},"symbols":{"type":"array","items":{}}}}},"sentiment":{"type":"object","properties":{"positive":{"type":"number","default":0,"examples":[0.033333376]},"negative":{"type":"number","default":0,"examples":[0.89130986]},"neutral":{"type":"number","default":0,"examples":[0.07535672]}}},"summary":{"type":"string","examples":["The National Finance Finance (NFT) market experienced a three-year low in 2024, with annual trading volumes and sales counts dropping to their lowest levels since 2020. The report by blockchain analytics platform DappRadar suggests that despite a surge in crypto market activity driven by Bitcoin's all-time highs and DeFi growth, NFTs struggled under the weight of their inflated valuations. Despite early Q1 trading volumes reaching $5.3 billion, a 4% increase compared to the same period in 2023, volumes dropped to $1.5 billion in Q3 before recovering to $2.6 billion by Q4. Despite these fluctuations, overall market engagement decreased. Despite this downturn, high-profile transactions like CryptoPunk #8348, a rare seven-trait collectible from the NFT collection, were seen as a milestone for NFT's value and highlighted a market still driven by exclusivity and inflated pricing."]},"translation":{"type":"string","examples":[""]},"translatedTitle":{"type":"string","examples":[""]},"translatedDescription":{"type":"string","examples":[""]},"translatedSummary":{"type":"string","examples":[""]},"locations":{"type":"array","items":{}},"reprint":{"type":"boolean","default":true,"examples":[false]},"reprintGroupId":{"type":"string","examples":["3d67e5bf389b40b3895ae0708f4dd208"]},"places":{"type":"array","items":{}},"people":{"type":"array","items":{}}}}}},"$schema":"https://json-schema.org/draft/2020-12/schema#"},"400":{"$schema":"https://json-schema.org/draft/2020-12/schema#"},"401":{"$schema":"https://json-schema.org/draft/2020-12/schema#"},"403":{"$schema":"https://json-schema.org/draft/2020-12/schema#"},"404":{"$schema":"https://json-schema.org/draft/2020-12/schema#"},"500":{"$schema":"https://json-schema.org/draft/2020-12/schema#"}}} as const
+;
+const Stories1 = {"metadata":{"allOf":[{"type":"object","properties":{"apiKey":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#"},"q":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search story by name, summary and key points. Preference is given to the name field. Supports complex query syntax, same way as **q** parameter from **/all** endpoint."},"name":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Search story by name. Supports complex query syntax, same way as **q** parameter from **/all** endpoint."},"topic":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by topics. Each topic is some kind of entity that the article is about. Examples of topics: Markets, Joe Biden, Green Energy, Climate Change, Cryptocurrency, etc. If multiple parameters are passed, they will be applied as OR operations. More ➜"},"category":{"type":"string","enum":["Politics","Tech","Sports","Business","Finance","Entertainment","Health","Weather","Lifestyle","Auto","Science","Travel","Environment","World","General","none"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by categories. Categories are general themes that the article is about. Examples of categories: Tech, Politics, etc. If multiple parameters are passed, they will be applied as OR operations. Use 'none' to search uncategorized articles. More ➜"},"clusterId":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter to specific story. Passing a cluster ID will filter results to only the content found within the cluster. Multiple params could be passed."},"source":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter stories by sources that wrote articles belonging to this story. At least 1 article is required for story to match. Multiple parameters could be passed."},"sourceGroup":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter stories by sources that wrote articles belonging to this story. Source groups are expanded into a list of sources. At least 1 article by the source is required for story to match. Multiple params could be passed."},"personWikidataId":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of person Wikidata IDs for filtering. Filter is applied on topPeople field."},"personName":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of people names. Filtering is applied on topPeople field."},"companyId":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of company IDs for filtering. Filtering is applied to topCompanies field."},"companyName":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of company names for filtering. Filtering is applied on topCompanies field."},"companyDomain":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of company domains for filtering. Filtering is applied on topCompanies field."},"companySymbol":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"List of company tickers for filtering. Filtering is applied on topCompanies field."},"nameExists":{"type":"boolean","default":true,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Returns stories with name assigned. Defaults to true."},"from":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'from' filter, will search stories created after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2023-03-01T00:00:00"},"to":{"type":"string","format":"date-time","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'to' filter, will search stories created before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Add time in ISO format, ie. 2023-03-01T23:59:59"},"initializedFrom":{"type":"string","format":"date","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'initializedFrom' filter, will search stories that became available after provided date"},"initializedTo":{"type":"string","format":"date","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"'initializedTo' filter, will search stories that became available before provided date"},"updatedFrom":{"type":"string","format":"date","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Will return stories with 'updatedAt' >= 'updatedFrom'."},"updatedTo":{"type":"string","format":"date","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Will return stories with 'updatedAt' <= 'updatedTo'."},"minClusterSize":{"type":"integer","format":"int32","default":5,"minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by minimum cluster size. Minimum cluster size filter applies to number of **unique** articles."},"maxClusterSize":{"type":"integer","format":"int32","minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter by maximum cluster size. Maximum cluster size filter applies to number of **unique** articles in the cluster."},"state":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter local news by state. Applies only to local news, when this param is passed non-local news will not be returned. If multiple parameters are passed, they will be applied as OR operations."},"city":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter local news by city. Applies only to local news, when this param is passed non-local news will not be returned. If multiple parameters are passed, they will be applied as OR operations. More ➜"},"area":{"type":"array","items":{"type":"string"},"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filter local news by area. Applies only to local news, when this param is passed non-local news will not be returned. If multiple parameters are passed, they will be applied as OR operations."},"page":{"type":"integer","format":"int32","default":0,"minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Zero-based page number. From 0 to 10000. See the Pagination section for limitations."},"size":{"type":"integer","format":"int32","default":10,"minimum":-2147483648,"maximum":2147483647,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Number of stories results per page, from 0 to 100."},"sortBy":{"type":"string","enum":["createdAt","updatedAt","count"],"default":"createdAt","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Sort stories by count, creation date, last updated date. By default is sorted by created at."},"showNumResults":{"type":"boolean","default":false,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Show total number of results. By default set to false, will cap result count at 10000."},"showDuplicates":{"type":"boolean","default":false,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Stories are deduplicated by default. If a story is deduplicated, all future articles are merged into the original story. *duplicateOf* field contains the original cluster Id. When *showDuplicates=true*, all stories are shown."},"positiveSentimentFrom":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score greater than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores."},"positiveSentimentTo":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score less than or equal to the specified value, indicating positive sentiment. See the Article Data section in Docs for an explanation of scores."},"neutralSentimentFrom":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score greater than or equal to the specified value, indicating neutral sentiment. Explanation of sentimental values can be found in Docs under the Article Data section."},"neutralSentimentTo":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score less than or equal to the specified value, indicating neutral sentiment. See the Article Data section in Docs for an explanation of scores."},"negativeSentimentFrom":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score greater than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores."},"negativeSentimentTo":{"type":"number","format":"float","minimum":-3.402823669209385e+38,"maximum":3.402823669209385e+38,"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters results with a sentiment score less than or equal to the specified value, indicating negative sentiment. See the Article Data section in Docs for an explanation of scores."},"country":{"type":"string","enum":["us","gb","de","it","fr","nl","se","dk","fi","hu","no","pl","pt","ru","ua","ch","br","nz","mx","au"],"$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Country code to filter by country. If multiple parameters are passed, they will be applied as OR operations."},"taxonomy":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters by Google Content Categories. This field will accept 1 or more categories, must pass the full name of the category. Example: taxonomy=/Finance/Banking/Other, /Finance/Investing/Funds"},"topTaxonomies":{"type":"string","$schema":"https://json-schema.org/draft/2020-12/schema#","description":"Filters by Google Content Categories. Highlights the top 3 categories in a cluster, ordered by count."}},"required":["apiKey"]}]},"response":{"200":{"$schema":"https://json-schema.org/draft/2020-12/schema#"},"400":{"type":"object","properties":{"status":{"type":"integer","default":0,"examples":[400]},"message":{"type":"string","examples":["Errors during parameter validation: \nError on parameter 'updatedFrom', invalid value 'test': Failed to convert property value of type 'java.lang.String' to required type 'java.time.OffsetDateTime' for property 'updatedFrom'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.gawq.api.formatting.DateTimeRequestParam java.time.OffsetDateTime] for value [test]; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [test]"]},"timestamp":{"type":"integer","default":0,"examples":[1730827027255]}},"$schema":"https://json-schema.org/draft/2020-12/schema#"}}} as const
+;
+export { AllNews, Stories1 }
diff --git a/packages/perigon/types.ts b/packages/perigon/types.ts
new file mode 100644
index 0000000..83c1620
--- /dev/null
+++ b/packages/perigon/types.ts
@@ -0,0 +1,13 @@
+import type { FromSchema } from 'json-schema-to-ts';
+import * as schemas from './schemas';
+
+export type AllNewsMetadataParam = FromSchema;
+export type AllNewsResponse200 = FromSchema;
+export type AllNewsResponse400 = FromSchema;
+export type AllNewsResponse401 = FromSchema;
+export type AllNewsResponse403 = FromSchema;
+export type AllNewsResponse404 = FromSchema;
+export type AllNewsResponse500 = FromSchema;
+export type Stories1MetadataParam = FromSchema;
+export type Stories1Response200 = FromSchema;
+export type Stories1Response400 = FromSchema;
diff --git a/searxng_tester.mts b/searxng_tester.mts
new file mode 100644
index 0000000..a5553a3
--- /dev/null
+++ b/searxng_tester.mts
@@ -0,0 +1,29 @@
+import { SearxngService, type SearxngServiceConfig, type SearxngSearchResult } from 'searxng';
+
+const config: SearxngServiceConfig = {
+ baseURL: 'https://search-engine-gsio.fly.dev',
+ defaultSearchParams: {
+ format: 'json',
+ lang: 'auto',
+ },
+ defaultRequestHeaders: {
+ 'Content-Type': 'application/json',
+ },
+};
+
+const searxngService = new SearxngService(config);
+
+async function performSearch(query) {
+ try {
+ const results = await searxngService.search(query);
+ console.log(results);
+ return results;
+ } catch (error) {
+ console.error('Search failed:', error);
+ }
+}
+
+
+const results = await performSearch('dogs');
+
+console.log(JSON.stringify(results));
\ No newline at end of file
diff --git a/src/agents/crypto_market.rs b/src/agents/crypto_market.rs
new file mode 100644
index 0000000..989dfb6
--- /dev/null
+++ b/src/agents/crypto_market.rs
@@ -0,0 +1,28 @@
+use tokio::process::Child;
+use tracing;
+
+use crate::utils::utils::run_agent;
+
+pub async fn finance_query_agent(stream_id: &str, input: &str) -> Result {
+ run_agent(stream_id, input, "./packages/genaiscript/genaisrc/finance-query.genai.mts").await
+}
+
+
+// #[cfg(test)]
+// mod tests {
+// use std::fmt::Debug;
+// use crate::agents::search::search_agent;
+//
+// #[tokio::test] // Mark the test function as async
+// async fn test_search_execution() {
+// let input = "Who won the 2024 presidential election?";
+//
+// let mut command = search_agent("test-stream", input).await.unwrap();
+//
+// // command.stdout.take().unwrap().read_to_string(&mut String::new()).await.unwrap();
+// // Optionally, you can capture and inspect stdout if needed:
+// let output = command.wait_with_output().await.expect("Failed to wait for output");
+// println!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
+// println!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
+// }
+// }
diff --git a/src/agents/image_generator.rs b/src/agents/image_generator.rs
new file mode 100644
index 0000000..a3502fc
--- /dev/null
+++ b/src/agents/image_generator.rs
@@ -0,0 +1,10 @@
+use crate::utils::utils::run_agent;
+use tokio::process::Child;
+
+pub async fn image_generator(stream_id: &str, input: &str) -> Result {
+ tracing::debug!(
+ "Running image generator, \ninput: {}",
+ input
+ );
+ run_agent(stream_id, input, "./packages/genaiscript/genaisrc/image-generator.genai.mts").await
+}
diff --git a/src/agents/mod.rs b/src/agents/mod.rs
new file mode 100644
index 0000000..a7ff97d
--- /dev/null
+++ b/src/agents/mod.rs
@@ -0,0 +1,5 @@
+pub mod news;
+pub mod scrape;
+pub mod search;
+pub mod image_generator;
+pub mod crypto_market;
diff --git a/src/agents/news.rs b/src/agents/news.rs
new file mode 100644
index 0000000..74cb0cb
--- /dev/null
+++ b/src/agents/news.rs
@@ -0,0 +1,6 @@
+use crate::utils::utils::run_agent;
+use tokio::process::Child;
+
+pub async fn news_agent(stream_id: &str, input: &str) -> Result {
+ run_agent(stream_id, input, "./packages/genaiscript/genaisrc/news-search.genai.mts").await
+}
diff --git a/src/agents/scrape.rs b/src/agents/scrape.rs
new file mode 100644
index 0000000..ecd16ac
--- /dev/null
+++ b/src/agents/scrape.rs
@@ -0,0 +1,6 @@
+use crate::utils::utils::run_agent;
+use tokio::process::Child;
+
+pub async fn scrape_agent(stream_id: &str, input: &str) -> Result {
+ run_agent(stream_id, input, "./packages/genaiscript/genaisrc/web-scrape.genai.mts").await
+}
diff --git a/src/agents/search.rs b/src/agents/search.rs
new file mode 100644
index 0000000..de385c8
--- /dev/null
+++ b/src/agents/search.rs
@@ -0,0 +1,28 @@
+use tokio::process::Child;
+use tracing;
+
+use crate::utils::utils::run_agent;
+
+pub async fn search_agent(stream_id: &str, input: &str) -> Result {
+ run_agent(stream_id, input, "./packages/genaiscript/genaisrc/web-search.genai.mts").await
+}
+
+
+#[cfg(test)]
+mod tests {
+ use std::fmt::Debug;
+ use crate::agents::search::search_agent;
+
+ #[tokio::test]
+ async fn test_search_execution() {
+ let input = "Who won the 2024 presidential election?";
+
+ let mut command = search_agent("test-stream", input).await.unwrap();
+
+ // command.stdout.take().unwrap().read_to_string(&mut String::new()).await.unwrap();
+ // Optionally, you can capture and inspect stdout if needed:
+ let output = command.wait_with_output().await.expect("Failed to wait for output");
+ println!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
+ println!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..6349d06
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,30 @@
+// src/config.rs
+pub struct AppConfig {
+ pub env_vars: Vec,
+}
+
+
+impl AppConfig {
+ pub fn new() -> Self {
+ // Load .env file if it exists
+ match dotenv::dotenv() {
+ Ok(_) => tracing::debug!("Loaded .env file successfully"),
+ Err(e) => tracing::debug!("No .env file found or error loading it: {}", e),
+ }
+
+ Self {
+ env_vars: vec![
+ "OPENAI_API_KEY".to_string(),
+ "BING_SEARCH_API_KEY".to_string(),
+ "TAVILY_API_KEY".to_string(),
+ "GENAISCRIPT_MODEL_LARGE".to_string(),
+ "GENAISCRIPT_MODEL_SMALL".to_string(),
+ "SEARXNG_API_BASE_URL".to_string(),
+ ],
+ }
+ }
+
+ pub fn get_env_var(&self, key: &str) -> String {
+ std::env::var(key).unwrap_or_default()
+ }
+}
\ No newline at end of file
diff --git a/src/genaiscript.rs b/src/genaiscript.rs
new file mode 100644
index 0000000..dd9cdfa
--- /dev/null
+++ b/src/genaiscript.rs
@@ -0,0 +1,90 @@
+use std::collections::HashMap;
+use std::path::PathBuf;
+use tokio::process::{Child, Command};
+use tracing;
+
+const DEFAULT_ENV_VARS: [&str; 4] = [
+ "OPENAI_API_KEY",
+ "OPENAI_API_BASE",
+ "GENAISCRIPT_MODEL_LARGE",
+ "GENAISCRIPT_MODEL_SMALL",
+];
+
+pub struct GenAIScriptConfig {
+ script_path: PathBuf,
+ output_dir: PathBuf,
+ stream_id: String,
+ user_input: String,
+ retry_count: u32,
+ env_vars: HashMap,
+}
+
+impl GenAIScriptConfig {
+ pub fn new(script_path: impl Into, stream_id: impl Into, user_input: impl Into) -> Self {
+ let mut env_vars = HashMap::new();
+
+ // Initialize with default environment variables
+ for var in DEFAULT_ENV_VARS {
+ if let Ok(value) = std::env::var(var) {
+ env_vars.insert(var.to_string(), value);
+ }
+ }
+
+ Self {
+ script_path: script_path.into(),
+ output_dir: PathBuf::from("./web-agent-rs/output"),
+ stream_id: stream_id.into(),
+ user_input: user_input.into(),
+ retry_count: 0,
+ env_vars,
+ }
+ }
+
+ pub fn with_output_dir(mut self, dir: impl Into) -> Self {
+ self.output_dir = dir.into();
+ self
+ }
+
+ pub fn with_retry_count(mut self, count: u32) -> Self {
+ self.retry_count = count;
+ self
+ }
+
+ pub fn with_additional_env_vars(mut self, vars: HashMap) -> Self {
+ self.env_vars.extend(vars);
+ self
+ }
+}
+
+pub async fn run_genaiscript(config: GenAIScriptConfig) -> Result {
+ tracing::debug!("Initiating GenAIScript for stream {}", config.stream_id);
+
+ let output_path = config.output_dir.join(&config.stream_id);
+
+ let mut command = Command::new("bunx");
+ command
+ .arg("genaiscript")
+ .arg("run")
+ .arg(&config.script_path)
+ // .arg("--fail-on-errors")
+ .arg("—out-trace")
+ .arg(output_path)
+ .arg("--retry")
+ .arg(config.retry_count.to_string())
+ .arg("--vars")
+ .arg(format!("USER_INPUT='{}'", config.user_input));
+
+ // Add environment variables
+ for (key, value) in config.env_vars {
+ command.env(key, value);
+ }
+
+ command
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::inherit())
+ .spawn()
+ .map_err(|e| {
+ tracing::error!("Failed to spawn genaiscript process: {}", e);
+ e.to_string()
+ })
+}
\ No newline at end of file
diff --git a/src/handlers/error.rs b/src/handlers/error.rs
new file mode 100644
index 0000000..8b08792
--- /dev/null
+++ b/src/handlers/error.rs
@@ -0,0 +1,17 @@
+// src/handlers/error.rs
+use axum::{
+ http::StatusCode,
+ Json,
+ response::IntoResponse,
+};
+
+pub async fn handle_not_found() -> impl IntoResponse {
+ tracing::warn!("404 Not Found error occurred");
+
+ let error_response = serde_json::json!({
+ "error": "Route Not Found",
+ "status": 404
+ });
+
+ (StatusCode::NOT_FOUND, Json(error_response))
+}
\ No newline at end of file
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
new file mode 100644
index 0000000..506514d
--- /dev/null
+++ b/src/handlers/mod.rs
@@ -0,0 +1,7 @@
+
+// src/handlers/mod.rs
+pub mod error;
+pub mod status;
+pub mod stream;
+pub mod ui;
+pub mod webhooks;
\ No newline at end of file
diff --git a/src/handlers/status.rs b/src/handlers/status.rs
new file mode 100644
index 0000000..1bb1b56
--- /dev/null
+++ b/src/handlers/status.rs
@@ -0,0 +1,5 @@
+// src/handlers/status.rs
+pub async fn handle_status() -> &'static str {
+ tracing::debug!("Status check requested");
+ "Server is running"
+}
\ No newline at end of file
diff --git a/src/handlers/stream.rs b/src/handlers/stream.rs
new file mode 100644
index 0000000..c01cddd
--- /dev/null
+++ b/src/handlers/stream.rs
@@ -0,0 +1,82 @@
+use axum::{
+ body::Body,
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+use futures::StreamExt;
+use tokio_util::io::ReaderStream;
+
+pub async fn handle_stream() -> impl IntoResponse {
+ use tokio::process::Command;
+
+ let user_input = "Who won the 2024 election?";
+ tracing::debug!("Handling stream request with input: {}", user_input);
+
+ // Check environment variables
+ for env_var in ["OPENAI_API_KEY", "BING_SEARCH_API_KEY", "TAVILY_API_KEY"] {
+ if std::env::var(env_var).is_ok() {
+ tracing::debug!("{} is set", env_var);
+ } else {
+ tracing::warn!("{} is not set", env_var);
+ }
+ }
+
+ let mut cmd = match Command::new("genaiscript")
+ .arg("run")
+ .arg("genaisrc/web-search.genai.mts")
+ .arg("--vars")
+ .arg(format!("USER_INPUT='{}'", user_input))
+ .env("OPENAI_API_KEY", std::env::var("OPENAI_API_KEY").unwrap_or_default())
+ .env("BING_SEARCH_API_KEY", std::env::var("BING_SEARCH_API_KEY").unwrap_or_default())
+ .env("TAVILY_API_KEY", std::env::var("TAVILY_API_KEY").unwrap_or_default())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::null())
+ .spawn() {
+ Ok(cmd) => {
+ tracing::debug!("Successfully spawned genaiscript process");
+ cmd
+ }
+ Err(e) => {
+ tracing::error!("Failed to spawn genaiscript process: {}", e);
+ return Response::builder()
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
+ .body(Body::from("Failed to start process"))
+ .unwrap();
+ }
+ };
+
+ let stdout = match cmd.stdout.take() {
+ Some(stdout) => {
+ tracing::debug!("Successfully captured stdout from process");
+ stdout
+ }
+ None => {
+ tracing::error!("Failed to capture stdout from process");
+ return Response::builder()
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
+ .body(Body::from("Failed to capture process output"))
+ .unwrap();
+ }
+ };
+
+ let reader = tokio::io::BufReader::new(stdout);
+ let stream = ReaderStream::new(reader);
+ let mapped_stream = stream.map(|r| {
+ match r {
+ Ok(bytes) => {
+ tracing::trace!("Received {} bytes from stream", bytes.len());
+ Ok(bytes)
+ }
+ Err(e) => {
+ tracing::error!("Error reading from stream: {}", e);
+ Err(e)
+ }
+ }
+ });
+
+ tracing::debug!("Setting up SSE response");
+ Response::builder()
+ .header("Content-Type", "text/event-stream")
+ .body(Body::from_stream(mapped_stream))
+ .unwrap()
+}
\ No newline at end of file
diff --git a/src/handlers/ui.rs b/src/handlers/ui.rs
new file mode 100644
index 0000000..bcf5967
--- /dev/null
+++ b/src/handlers/ui.rs
@@ -0,0 +1,34 @@
+use axum::{
+ body::Body,
+ http::{StatusCode, header::CONTENT_TYPE},
+ response::{IntoResponse, Response},
+};
+use rust_embed::RustEmbed;
+use tracing::{debug, error};
+
+#[derive(RustEmbed)]
+#[folder = "assets/"]
+struct Asset;
+
+pub async fn serve_ui() -> impl IntoResponse {
+ debug!("Serving UI request");
+
+ // Attempt to retrieve the embedded "index.html"
+ match Asset::get("index.html") {
+ Some(content) => {
+ debug!("Successfully retrieved index.html");
+ Response::builder()
+ .status(StatusCode::OK)
+ .header(CONTENT_TYPE, "text/html")
+ .body(Body::from(content.data))
+ .unwrap()
+ }
+ None => {
+ error!("index.html not found in embedded assets");
+ Response::builder()
+ .status(StatusCode::NOT_FOUND)
+ .body(Body::from("404 Not Found"))
+ .unwrap()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/handlers/webhooks.rs b/src/handlers/webhooks.rs
new file mode 100644
index 0000000..88fbd91
--- /dev/null
+++ b/src/handlers/webhooks.rs
@@ -0,0 +1,261 @@
+use crate::agents;
+use crate::agents::news::news_agent;
+use crate::agents::scrape::scrape_agent;
+use crate::agents::search::search_agent;
+use axum::response::Response;
+use axum::{
+ body::Body, extract::Path, extract::Query, http::StatusCode, response::IntoResponse, Json,
+};
+use bytes::Bytes;
+use futures::stream::{Stream, StreamExt};
+use lazy_static::lazy_static;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use sled;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::time::Duration;
+use tokio::io::{AsyncBufReadExt, BufReader};
+use tokio::process::Command;
+use tokio::sync::Mutex;
+use crate::agents::crypto_market::finance_query_agent;
+use crate::agents::image_generator::image_generator;
+
+// init sled
+lazy_static! {
+ static ref DB: Arc> = Arc::new(Mutex::new(
+ sled::open("./web-agent-rs/db/stream_store").expect("Failed to open sled database")
+ ));
+}
+
+pub async fn handle_webhooks(Path(stream_id): Path) -> impl IntoResponse {
+ let db = DB.lock().await;
+ match db.get(&stream_id) {
+ Ok(Some(data)) => {
+
+ let mut info: StreamInfo = match serde_json::from_slice(&data) {
+ Ok(info) => info,
+ Err(e) => {
+ tracing::error!("Failed to deserialize StreamInfo: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+
+ // Increment the call_count in the database
+ info.call_count += 1;
+ let updated_info_bytes = match serde_json::to_vec(&info) {
+ Ok(data) => data,
+ Err(e) => {
+ tracing::error!("Failed to serialize updated StreamInfo: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+ match db.insert(&stream_id, updated_info_bytes) {
+ Ok(_) => {
+ if let Err(e) = db.flush_async().await {
+ tracing::error!("Failed to persist updated call_count to the database: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ }
+ Err(e) => {
+ tracing::error!("Failed to update call_count in the database: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+ let info: StreamInfo = match db.get(&stream_id) {
+ Ok(Some(updated_data)) => match serde_json::from_slice(&updated_data) {
+ Ok(info) => info,
+ Err(e) => {
+ tracing::error!("Failed to deserialize updated StreamInfo: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ },
+ Ok(None) => {
+ tracing::error!("Stream ID not found after update: {}", stream_id);
+ return StatusCode::NOT_FOUND.into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to fetch updated record from DB: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+ if(info.call_count > 1) {
+ return StatusCode::OK.into_response();
+ }
+
+ let resource = info.resource;
+ let input = serde_json::to_string(&info.payload.input).unwrap_or_default();
+
+ tracing::debug!(
+ "Processing webhook - Resource: {}, Stream ID: {}",
+ resource,
+ stream_id
+ );
+
+ let cmd = match resource.as_str() {
+ "web-search" => search_agent(stream_id.as_str(), &*input).await,
+ "news-search" => news_agent(stream_id.as_str(), &*input).await,
+ "image-generator" => image_generator(stream_id.as_str(), &*input).await,
+ "finance-query" => finance_query_agent(stream_id.as_str(), &*input).await,
+ "web-scrape" => scrape_agent(stream_id.as_str(), &*input).await,
+ _ => {
+ tracing::error!("Unsupported resource type: {}", resource);
+ return StatusCode::BAD_REQUEST.into_response();
+ }
+ };
+
+ let mut cmd = match cmd {
+ Ok(cmd) => cmd,
+ Err(e) => {
+ tracing::error!("Agent execution failed: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+ let stdout = match cmd.stdout.take() {
+ Some(stdout) => stdout,
+ None => {
+ tracing::error!("No stdout available for the command.");
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+ let reader = BufReader::new(stdout);
+ let sse_stream = reader_to_stream(reader, stream_id.clone());
+
+ return Response::builder()
+ .header("Content-Type", "text/event-stream")
+ .header("Cache-Control", "no-cache, no-transform")
+ .header("Connection", "keep-alive")
+ .header("X-Accel-Buffering", "yes")
+ .body(Body::from_stream(sse_stream))
+ .unwrap()
+ }
+ Ok(None) => {
+ tracing::error!("Stream ID not found: {}", stream_id);
+ StatusCode::NOT_FOUND.into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to fetch from DB: {}", e);
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ }
+}
+
+fn reader_to_stream(
+ reader: BufReader,
+ stream_id: String,
+) -> Pin> + Send>>
+where
+ R: tokio::io::AsyncRead + Unpin + Send + 'static,
+{
+ let stream = futures::stream::unfold(reader, move |mut reader| async move {
+ let mut line = String::new();
+ match reader.read_line(&mut line).await {
+ Ok(0) => None,
+ Ok(_) => Some((
+ Ok(Bytes::from(format!("data: {}\n\n", line.trim()))),
+ reader,
+ )),
+ Err(e) => Some((Err(e), reader)),
+ }
+ });
+
+ let stream_with_done = stream.chain(futures::stream::once(async {
+ Ok(Bytes::from("data: [DONE]\n\n"))
+ }));
+
+ Box::pin(stream_with_done)
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+struct Payload {
+ input: Value,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct StreamInfo {
+ resource: String,
+ payload: Payload,
+ parent: String,
+ call_count: i32,
+}
+
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct WebhookPostRequest {
+ id: String,
+ resource: String,
+ payload: Payload,
+ parent: String,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+struct WebhookPostResponse {
+ stream_url: String,
+}
+
+pub async fn handle_webhooks_post(Json(payload): Json) -> impl IntoResponse {
+ let db = DB.lock().await;
+
+ tracing::info!("Received webhook post request with ID: {}", payload.id);
+
+ let stream_id = payload.id.clone();
+ let info = StreamInfo {
+ resource: payload.resource.clone(),
+ payload: payload.payload,
+ parent: payload.parent.clone(),
+ call_count: 0
+ };
+
+ let info_bytes = match serde_json::to_vec(&info) {
+ Ok(data) => data,
+ Err(e) => {
+ tracing::error!("Failed to serialize StreamInfo: {}", e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+ }
+ };
+
+ // Use atomic compare_and_swap operation
+ match db.compare_and_swap(
+ &stream_id,
+ None as Option<&[u8]>,
+ Some(info_bytes.as_slice()),
+ ) {
+ Ok(_) => {
+ // Force an immediate sync to disk
+ match db.flush_async().await {
+ Ok(_) => {
+ // Verify the write by attempting to read it back
+ match db.get(&stream_id) {
+ Ok(Some(_)) => {
+ let stream_url = format!("/webhooks/{}", stream_id);
+ tracing::info!("Successfully created and verified stream URL: {}", stream_url);
+ Json(WebhookPostResponse { stream_url }).into_response()
+ },
+ Ok(None) => {
+ tracing::error!("Failed to verify stream creation: {}", stream_id);
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ },
+ Err(e) => {
+ tracing::error!("Error verifying stream creation: {}", e);
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ }
+ },
+ Err(e) => {
+ tracing::error!("Failed to flush DB: {}", e);
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ }
+ }
+ Err(e) => {
+ tracing::error!("Failed to insert stream info: {}", e);
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..18c3a5c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,43 @@
+// src/main.rs
+use crate::config::AppConfig;
+use crate::routes::create_router;
+use crate::setup::init_logging;
+
+mod config;
+mod routes;
+mod setup;
+mod handlers;
+mod agents;
+mod genaiscript;
+mod utils;
+mod session_identify;
+
+#[tokio::main]
+async fn main() {
+ // Initialize logging
+ init_logging();
+
+ // Load configuration
+ let config = AppConfig::new();
+
+ // Create router with all routes
+ let app = create_router();
+
+ // Start core
+ let addr = "0.0.0.0:3006";
+ tracing::info!("Attempting to bind core to {}", addr);
+
+ let listener = match tokio::net::TcpListener::bind(addr).await {
+ Ok(l) => {
+ tracing::info!("Successfully bound to {}", l.local_addr().unwrap());
+ l
+ }
+ Err(e) => {
+ tracing::error!("Failed to bind to {}: {}", addr, e);
+ panic!("Server failed to start");
+ }
+ };
+
+ tracing::info!("Server starting on {}", listener.local_addr().unwrap());
+ axum::serve(listener, app.into_make_service()).await.unwrap();
+}
diff --git a/src/routes.rs b/src/routes.rs
new file mode 100644
index 0000000..b6dc988
--- /dev/null
+++ b/src/routes.rs
@@ -0,0 +1,105 @@
+use crate::handlers::webhooks::handle_webhooks_post;
+use crate::handlers::{
+ error::handle_not_found,
+ ui::serve_ui
+ ,
+ webhooks::handle_webhooks,
+};
+use crate::session_identify::session_identify;
+use axum::extract::Request;
+use axum::response::Response;
+use axum::routing::post;
+// src/routes.rs
+use axum::routing::{get, Router};
+use http::header::AUTHORIZATION;
+use http::StatusCode;
+use serde::{Deserialize, Serialize};
+use serde_json::Number;
+use std::fmt;
+use tower_http::trace::{self, TraceLayer};
+use tracing::Level;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct CurrentUser {
+ pub(crate) sub: String,
+ pub name: String,
+ pub email: String,
+ pub exp: Number,
+ pub id: String,
+ pub aud: String,
+}
+
+impl fmt::Display for CurrentUser {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "CurrentUser {{ id: {}, name: {}, email: {}, sub: {}, aud: {}, exp: {} }}",
+ self.id, self.name, self.email, self.sub, self.aud, self.exp
+ )
+ }
+}
+
+pub fn create_router() -> Router {
+
+ Router::new()
+ .route("/", get(serve_ui))
+ // request a stream resource
+ .route("/api/webhooks", post(handle_webhooks_post))
+ // consume a stream resource
+ .route("/webhooks/:stream_id", get(handle_webhooks))
+ .route_layer(axum::middleware::from_fn(auth))
+ .route("/health", get(health))
+ .layer(
+ TraceLayer::new_for_http()
+ .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
+ .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
+ )
+ // left for smoke testing
+ // .route("/api/status", get(handle_status))
+ .fallback(handle_not_found)
+}
+
+async fn health() -> String {
+ return "ok".to_string();
+}
+
+async fn auth(mut req: Request, next: axum::middleware::Next) -> Result {
+ let session_token_header = req
+ .headers()
+ .get(AUTHORIZATION)
+ .and_then(|header_value| header_value.to_str().ok());
+
+ let session_token_parts= session_token_header.expect("No credentials").split(" ").collect::>();
+
+ let session_token = session_token_parts.get(1);
+
+
+ // log::info!("session_token: {:?}", session_token);
+
+ let session_token = session_token.expect("Unauthorized: No credentials supplied");
+
+ let result =
+ if let Some(current_user) = authorize_current_user(&*session_token).await {
+ // info!("current user: {}", current_user);
+ // insert the current user into a request extension so the handler can
+ // extract it
+ req.extensions_mut().insert(current_user);
+ Ok(next.run(req).await)
+ } else {
+ Err(StatusCode::UNAUTHORIZED)
+ };
+ result
+}
+
+
+async fn authorize_current_user(
+ session_token: &str,
+) -> Option {
+ let session_identity = session_identify(session_token)
+ .await
+ .unwrap();
+
+ // println!("current_user: {:?}", session_identity.user);
+
+ Some(serde_json::from_value::(session_identity.user).unwrap())
+}
\ No newline at end of file
diff --git a/src/session_identify.rs b/src/session_identify.rs
new file mode 100644
index 0000000..a70a8fd
--- /dev/null
+++ b/src/session_identify.rs
@@ -0,0 +1,55 @@
+use anyhow::Result;
+use serde_json::Value;
+use serde_json::json;
+use base64::Engine;
+use fips204::ml_dsa_44::{PrivateKey, PublicKey};
+use fips204::traits::{SerDes, Signer, Verifier};
+use crate::utils::base64::B64_ENCODER;
+
+pub struct SessionIdentity {
+ pub message: String,
+ pub signature: String,
+ pub target: String,
+ pub session_id: String,
+ pub user: Value
+}
+
+pub async fn session_identify(session_token: &str) -> Result {
+ let session_data_base64 = session_token.split('.').nth(0).ok_or_else(|| anyhow::anyhow!("Invalid session data format"))?;
+ // println!("session_data_base64: {}", session_data_base64);
+ let session_data: Value = serde_json::de::from_slice(&*B64_ENCODER.b64_decode_payload(session_data_base64).map_err(|e| anyhow::anyhow!("Failed to decode session data: {}", e))?).map_err(|e| anyhow::anyhow!("Failed to parse session data: {}", e))?;
+ // println!("session_data: {:?}", session_data);
+
+
+ let signature_base64 = session_token.split('.').nth(1).ok_or_else(|| anyhow::anyhow!("Invalid session token format"))?;
+ // println!("signature_base64: {}", signature_base64);
+
+ let target = session_data.get("aud")
+ .and_then(|e| e.as_str())
+ .ok_or_else(|| anyhow::anyhow!("Session data missing audience"))?;
+
+ let target = target.parse::().map_err(|e| anyhow::anyhow!("Failed to parse target to String: {}", e))?;
+
+ let session_id = session_data.get("id")
+ .and_then(|e| e.as_str())
+ .ok_or_else(|| anyhow::anyhow!("Session data missing id"))?;
+
+ let session_id = session_id.parse::().map_err(|e| anyhow::anyhow!("Failed to parse session_id to String: {}", e))?;
+
+ // let request_payload: Value = json!({
+ // "message": session_data_base64,
+ // "signature": signature_base64,
+ // "target": target,
+ // "session_id": session_id,
+ // });
+
+ let result = SessionIdentity {
+ message: session_data_base64.to_string(),
+ signature: signature_base64.to_string(),
+ target,
+ session_id,
+ user: session_data.clone()
+ };
+
+ Ok(result)
+}
\ No newline at end of file
diff --git a/src/setup.rs b/src/setup.rs
new file mode 100644
index 0000000..ddc7ad8
--- /dev/null
+++ b/src/setup.rs
@@ -0,0 +1,10 @@
+// src/setup.rs
+pub fn init_logging() {
+ tracing_subscriber::fmt()
+ .with_max_level(tracing::Level::DEBUG)
+ .with_target(true)
+ .with_thread_ids(true)
+ .with_file(true)
+ .with_line_number(true)
+ .init();
+}
\ No newline at end of file
diff --git a/src/utils/base64.rs b/src/utils/base64.rs
new file mode 100644
index 0000000..518db8a
--- /dev/null
+++ b/src/utils/base64.rs
@@ -0,0 +1,65 @@
+use base64::Engine;
+use base64::engine::GeneralPurpose;
+use base64::engine::general_purpose::STANDARD;
+use base64::engine::general_purpose::STANDARD_NO_PAD;
+
+pub struct Base64Encoder {
+ payload_engine: GeneralPurpose,
+ signature_engine: GeneralPurpose,
+ public_key_engine: GeneralPurpose,
+ secret_key_engine: GeneralPurpose,
+}
+
+impl Base64Encoder {
+ pub(crate) fn b64_encode(&self, p0: &[u8]) -> String {
+ self.payload_engine.encode(p0)
+ }
+ pub(crate) fn b64_decode(&self, p0: String) -> Result, base64::DecodeError> {
+ self.payload_engine.decode(p0)
+ }
+}
+
+pub const B64_ENCODER: &Base64Encoder = &Base64Encoder::new();
+
+impl Base64Encoder {
+ pub const fn new() -> Self { // Made new() a const fn
+ Base64Encoder {
+ payload_engine: STANDARD,
+ signature_engine: STANDARD,
+ public_key_engine: STANDARD,
+ secret_key_engine: STANDARD,
+ }
+ }
+
+ pub fn b64_encode_payload>(&self, input: T) -> String { // Added trait bound
+ self.payload_engine.encode(input)
+ }
+
+ pub fn b64_decode_payload>(&self, input: T) -> Result, base64::DecodeError> { // Added trait bound
+ self.payload_engine.decode(input)
+ }
+
+ pub fn b64_decode_signature>(&self, input: T) -> Result, base64::DecodeError> { // Added trait bound
+ self.signature_engine.decode(input)
+ }
+
+ pub fn b64_encode_signature>(&self, input: T) -> String { // Added trait bound
+ self.signature_engine.encode(input)
+ }
+
+ pub fn b64_encode_public_key>(&self, input: T) -> String { // Added trait bound
+ self.public_key_engine.encode(input)
+ }
+
+ pub fn b64_decode_public_key>(&self, input: T) -> Result, base64::DecodeError> { // Added trait bound
+ self.public_key_engine.decode(input)
+ }
+
+ pub fn b64_encode_secret_key>(&self, input: T) -> String { // Added trait bound
+ self.secret_key_engine.encode(input)
+ }
+
+ pub fn b64_decode_secret_key>(&self, input: T) -> Result, base64::DecodeError> { // Added trait bound
+ self.secret_key_engine.decode(input)
+ }
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
new file mode 100644
index 0000000..021524b
--- /dev/null
+++ b/src/utils/mod.rs
@@ -0,0 +1,2 @@
+pub mod utils;
+pub mod base64;
diff --git a/src/utils/utils.rs b/src/utils/utils.rs
new file mode 100644
index 0000000..5e2b2ab
--- /dev/null
+++ b/src/utils/utils.rs
@@ -0,0 +1,80 @@
+// utils.rs
+use tokio::process::{Child, Command}; // Use tokio::process::Child and Command
+use std::env;
+use tokio::time::{timeout, Duration};
+use tracing;
+
+
+pub struct ShimBinding {
+ user_input: String,
+ file_path: String, // Add new field for the file path
+ openai_api_key: String,
+ openai_api_base: String,
+ bing_search_api_key: String,
+ perigon_api_key: String,
+ tavily_api_key: String,
+ genaiscript_model_large: String,
+ genaiscript_model_small: String,
+ searxng_api_base_url: String,
+ searxng_password: String,
+}
+
+impl ShimBinding {
+ pub fn new(user_input: String, file_path: String) -> Self { // Update constructor to take file path
+ Self {
+ user_input,
+ file_path, // Initialize the new field
+ openai_api_key: env::var("OPENAI_API_KEY").unwrap_or_default(),
+ openai_api_base: env::var("OPENAI_API_BASE").unwrap_or_default(),
+ bing_search_api_key: env::var("BING_SEARCH_API_KEY").unwrap_or_default(),
+ tavily_api_key: env::var("TAVILY_API_KEY").unwrap_or_default(),
+ genaiscript_model_large: env::var("GENAISCRIPT_MODEL_LARGE").unwrap_or_default(),
+ genaiscript_model_small: env::var("GENAISCRIPT_MODEL_SMALL").unwrap_or_default(),
+ perigon_api_key: env::var("PERIGON_API_KEY").unwrap_or_default(),
+ searxng_api_base_url: env::var("SEARXNG_API_BASE_URL").unwrap_or_default(),
+ searxng_password: env::var("SEARXNG_PASSWORD").unwrap_or_default(),
+ }
+ }
+
+ pub fn execute(&self) -> std::io::Result {
+ let mut command = Command::new("./dist/genaiscript-rust-shim.js");
+ command
+ .arg("--file")
+ .arg(&self.file_path) // Use the file_path field instead of hardcoded value
+ .arg(format!("USER_INPUT={}", self.user_input))
+ .env("OPENAI_API_KEY", &self.openai_api_key)
+ .env("OPENAI_API_BASE", &self.openai_api_base)
+ .env("BING_SEARCH_API_KEY", &self.bing_search_api_key)
+ .env("TAVILY_API_KEY", &self.tavily_api_key)
+ .env("GENAISCRIPT_MODEL_LARGE", &self.genaiscript_model_large)
+ .env("GENAISCRIPT_MODEL_SMALL", &self.genaiscript_model_small)
+ .env("PERIGON_API_KEY", &self.perigon_api_key)
+ .env("SEARXNG_API_BASE_URL", &self.searxng_api_base_url)
+ .env("SEARXNG_PASSWORD", &self.searxng_password)
+ .stdout(std::process::Stdio::piped()) // Use tokio::io::Stdio::piped()
+ .stderr(std::process::Stdio::inherit()); // Use tokio::io::Stdio::piped()
+
+ command.spawn()
+ }
+}
+
+
+ /// Generic helper to execute a ShimBinding-based agent with a timeout
+pub async fn run_agent(stream_id: &str, input: &str, file_path: &str) -> Result {
+ tracing::debug!("Initiating agent for stream {} with file path {}", stream_id, file_path);
+
+ let shim_binding = ShimBinding::new(input.to_string(), file_path.to_string());
+ let spawn_future = async move {
+ match shim_binding.execute() {
+ Ok(child) => Ok(child),
+ Err(e) => {
+ tracing::error!("Failed to spawn shim process: {}", e);
+ Err(e.to_string())
+ }
+ }
+ };
+
+ timeout(Duration::from_secs(10), spawn_future)
+ .await
+ .unwrap_or_else(|_| Err("Command timed out after 10 seconds".to_string()))
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..54f4023
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+// "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ },
+ "include": [
+ "./packages/genaiscript/genaisrc/genaiscript.d.ts"
+ ]
+}