今日已更新 299 条资讯 | 累计 41519 条内容
关于我们

标签:#api

找到 571 篇相关文章

AI 资讯

Multi-Provider LLM Router, or How I Got Tired of Forgetting Which API Format I Had To Use

If you've ever built an application that integrates with multiple LLM providers (Anthropic, Google, OpenAI, DeepSeek), you already know the pain: Each provider has its own distinct Python SDK. Streaming responses using Server-Sent Events (SSE) requires divergent parser logic. Thinking / Reasoning blocks are formatted completely differently. I recently extracted the core streaming router from my platform into an open-source FastAPI template. Here is how it works. Objective A single asynchronous endpoint: POST /v1/chat/stream It accepts a unified request payload and returns a standardized SSE stream emitting four clean events: event: thinking — Internal model reasoning tokens (streamed in real-time). event: content — User-facing response text. event: tool_call — Function calling requests. event: done — Stream completion ( [DONE] ). Architecture Instead of pulling heavy wrapper frameworks, use direct asynchronous HTTP via httpx.AsyncClient and the official Google GenAI SDK: fastapi-multi-llm-starter/ ├── app/ │ ├── config.py # Pydantic Settings loading environment variables │ ├── main.py # FastAPI app with CORS, health check & test playground │ ├── models.json # Dynamic model catalog (Claude, Gemini, GPT) │ ├── router.py # Unified multi-provider async stream dispatcher │ └── schemas.py # Strict Pydantic v2 validation models ├── tests/ # Automated unit tests (pytest) ├── requirements.txt └── README.md Dynamic Model Catalog I disliked the idea of hardcoded models, so I decoupled them into a models.json file: { "models" : [ { "id" : "claude-sonnet-5" , "name" : "Claude Sonnet 5" , "provider" : "Anthropic" , "thinking" : true }, { "id" : "gemini-3.8-flash" , "name" : "Gemini 3.8 Flash" , "provider" : "Google" , "thinking" : true }, { "id" : "gpt-5.6-terra" , "name" : "GPT 5.6 Terra" , "provider" : "OpenAI" , "thinking" : true } ] } Now, if you want to add another model, you just edit the JSON. The backend and the embedded UI dynamically populate available models via GET /v

2026-09-10 原文 →
AI 资讯

Partition once versus filter twice for bulk email validation

The decision The partition function separates the array into the elements that satisfy the condition from those that do not docs . The filter function iterates over an array and applies an expression that returns matching values docs . Both scripts compute the task to split payload.records into accepted ids and rejected records with a reason, plus a retry count, by an email match. First approach The partition once approach uses the import line import * from dw::core::Arrays . %dw 2.0 import * from dw::core::Arrays output application/json var split = payload.records partition (r) -> (r.email default "") matches /.+@.+\..+/ --- { accepted: split.success map (r) -> r.id, rejected: split.failure map (r) -> { id: r.id, reason: "missing or invalid email" }, retryCount: sizeOf(split.failure) } Second approach The filter twice approach requires no import. %dw 2.0 output application/json var valid = payload.records filter ((r) -> (r.email default "") matches /.+@.+\..+/) var invalid = payload.records filter ((r) -> not ((r.email default "") matches /.+@.+\..+/)) --- { accepted: valid map (r) -> r.id, rejected: invalid map (r) -> { id: r.id, reason: "missing or invalid email" }, retryCount: sizeOf(invalid) } Same input, same output { "records" : [ { "id" : "ORD-1001" , "email" : "ana@example.com" }, { "id" : "ORD-1002" , "email" : "bad-address" }, { "id" : "ORD-1003" , "email" : "raj@example.org" }, { "id" : "ORD-1004" }, { "id" : "ORD-1005" , "email" : "mei@example.net" } ] } Both scripts print the same output for this input. { "accepted" : [ "ORD-1001" , "ORD-1003" , "ORD-1005" ], "rejected" : [ { "id" : "ORD-1002" , "reason" : "missing or invalid email" }, { "id" : "ORD-1004" , "reason" : "missing or invalid email" } ], "retryCount" : 2 } Measured Input Records Script Runs Min ms Median ms Max ms Source small 5 partition once 10 77 84 94 verified in sandbox large 50000 partition once 10 378 383 388 verified in sandbox small 5 filter twice 10 52 55.5 62 verified in sandbox

2026-09-10 原文 →
AI 资讯

Node.js API Boundaries for Realtime Quota Protection in Live Auction Dashboards

Short answer: put a small, explicit quota boundary in front of every realtime connection, make reconnect and backfill consume a separate budget, and measure freshness rather than WebSocket count. For a live auction dashboard, this keeps a reconnect storm from starving the bids, typing indicators, and read receipts that operators actually need. The page that wakes the on-call engineer is usually not the page they needed. It says realtime_connections > 50,000 or reports a spike of HTTP 429 responses. Meanwhile the auction view is quietly showing old bids because clients are retrying history reads and consuming the same API quota as the live stream. The first fix is to name the traffic classes: live fan-out, presence and typing signals, receipts, and backfill are different workloads with different loss tolerance. The alert is late: trace the quota failure backward Imagine a regional network flap drops 18% of browser connections in two minutes. Every tab reconnects with exponential backoff, then asks for the last 500 events. The backfill requests arrive together, compete with bid updates, and inflate p95 latency. A connection-count alert fires after the damage is visible, while the useful early signal was the ratio of reconnect attempts to successful session resumes. That sequence deserves a trace of its own. At 12:00:00, a viewer has acknowledged event 8,421. At 12:00:02, the socket closes and the browser schedules a resume. At 12:00:03, the resume is admitted, but the client also starts a second history request because its UI timer has not heard from the first one. At 12:00:04, both requests pass a connection-only limiter and read the same rows. At 12:00:05, a bid arrives behind those reads, so the dashboard is connected yet stale. A class-aware boundary would admit the resume, reject the duplicate backfill with Retry-After: 2 , and leave the bid lane untouched. The important detail is not the exact timestamps; it is that each transition is visible and attributable, s

2026-09-10 原文 →
开发者

nginx silently rejects the new HTTP QUERY method

RFC 10008 went to Proposed Standard in June. It adds QUERY, a new HTTP method. Safe and idempotent like GET, but it carries a body like POST. On paper, that's it. A new verb. I nearly didn't bother writing this up because of that. Then I read further into the RFC and found a line saying older proxies, frameworks and load balancer configs might not recognise the method yet. It doesn't say which ones. It doesn't say what "not recognise" even means in practice. Does it 404? 405? Does it just eat the body and treat it as GET? Nobody tells you, so I rented a box and found out myself. Most codebases I've touched have a POST /search somewhere, and it's always for the same reason: GET can't carry a real filter object, and nobody fancies fighting a URL length limit over it. QUERY fixes that, in theory. Whether it works in practice depends on every layer between the client and your view agreeing to let the method through. That's not something the RFC can tell you. Only running it can. So that's what I did. One backend, three reverse proxies in front of it, one separate Django app on the side, and a droplet I could throw away the second I had numbers. What I built A FastAPI backend on port 8001. nginx, Caddy and Traefik each fronting it on their own port. A separate Django project too, with two class-based views, because Django isn't built on Starlette and dispatches methods completely differently. All of it on one DigitalOcean Droplet in Frankfurt, fra1, s-2vcpu-4gb, Ubuntu 24.04. I killed the droplet the moment testing was done. Versions, in case you're checking this later: curl 8.5.0. FastAPI 0.141.1 on Starlette 1.6.0. Django 6.1.1. nginx 1.24.0. Caddy 2.11.4. Traefik 3.7.10. curl already does this properly First question, before anything else: can the tooling even send a QUERY request with a body? I pointed curl at a bare netcat listener to see the raw bytes. curl -s -m 2 -X QUERY -H "Content-Type: application/json" \ -d '{"q":"test"}' http://127.0.0.1:8000/search QUERY /

2026-09-10 原文 →
AI 资讯

The feed that looked filtered

agenticjobs 0.5.0 is out. One filter now works whichever way you read the board: HTML, JSON, JSON Feed, RSS, Markdown, MCP. /?workplace=remote&tags=javascript /api/v1/jobs?workplace=remote&tags=javascript /jobs.md?workplace=remote&tags=javascript /feed?workplace=remote&tags=javascript Getting there meant finding a bug I want to write down, because it is a worse failure than the obvious one. Two of the RSS feeds took a query and threw it away. Not "did not support filtering", which would be fine and visible. They accepted the parameters, ignored them, and answered with the whole board: const page = await searchJobs ( pool , { ... parseQuery ( new URLSearchParams ()), // an EMPTY one limit : 100 , }); Every other surface built its query from the actual request. These two built one from an empty parameter list, so whatever a reader put in the address was discarded before it reached the database. The test that found it is the useful part. Ask every surface for something no listing is, and require all of them to return nothing: workplace=onsite, on a board whose only job is remote /api/v1/jobs 0 correct /jobs.json 0 correct /feed 0 correct /jobs.rss 1 ignored the filter /feed.rss 3 ignored the filter A feed that returns nothing when you filter is either correct or obviously broken, and you can tell which in a second. A feed that returns everything looks like a working feed with a lot of results. Nobody subscribes to ?workplace=remote and then counts. The interesting question was what a filtered everything-feed should contain. /feed.rss carries jobs, employers and candidates. If you filter it by employmentType=contract , what happens to the people? The rule I settled on: tags are the only filter that means the same thing on both halves of the board. Workplace, employment type, seniority, agent policy, a salary floor and an employer are questions about a job, and a person cannot answer them. So a job-shaped filter drops the candidates, and employers drop out of any filtere

2026-09-09 原文 →
AI 资讯

The hold expired before we tried to capture it

Ran into this reconciling a subscription billing system: authorize now, capture in 48 hours after fraud review clears. Worked fine in testing. In production, roughly 2% of captures started failing with a decline code that looked like insufficient funds but wasn't. Turned out the issuer had already released the authorization hold. Visa's guideline is 7 days for most MCCs, but individual issuers set their own expiry, and we saw holds die anywhere from 3 to 10 days depending on the card's bank. Nothing in the original auth response tells you when that hold actually dies. You find out when the capture bounces. Our fraud review queue had a 72-hour SLA on paper. Average was fine. The tail wasn't. A subset of manual reviews sat for 4-6 days, long enough to cross into issuer-specific expiry windows we had no visibility into. Fix ended up being boring: track auth timestamp separately from order status, re-authorize automatically if capture attempt fails with that specific decline pattern and the auth is older than 3 days, and stop treating "authorized" as a stable state past 72 hours. Curious how other teams handle this. Do you re-auth automatically, or push the delay back to the review queue instead?

2026-09-09 原文 →
开发者

vim.async's Addition Modernizes Neovim’s Async Architecture for Better Stability

Neovim has introduced a structured concurrency library in its Lua standard library via the vim.async namespace. This framework provides a standardized method for managing async workflows, addressing issues related to task management and error propagation. It allows for cooperative scheduling and clear task hierarchies, improving plugin development and error handling in asynchronous operations. By Olimpiu Pop

2026-09-09 原文 →
AI 资讯

OpenSparrow v3.6 adds an External API module

OpenSparrow v3.6 adds an External API module that lets admins expose table data to external services through read-only, encrypted API keys. Each key is bound to a single table with a fixed column set, server-side filters and a row limit, and is served by a sessionless JSON endpoint that accepts nothing from the client except the key itself. Below is a breakdown of what changed and why it matters. External API module A new Admin → System → API tab lets admins define read-only API keys. Each definition is bound to one schema table, a chosen set of columns, fixed server-side filters and a row limit, so the data an external service can see is decided entirely by the admin — never by the caller. Keys are stored encrypted ( key_enc ) plus an HMAC key_hash for constant-time lookup, and are never returned to the browser after generation A new or regenerated key is generated server-side and shown exactly once in a modal Filter values are type-checked against the column's schema type on save, so a stored filter can't 500 at query time A sessionless JSON endpoint public/api/external.php is the single endpoint. External services authenticate with an Authorization: Bearer <key> header and receive JSON rows. The endpoint accepts no table, column or filter names from the client — everything comes from the config Hidden and system ( spw_ ) tables are refused, both on save and at request time Rate limiting is enforced per key and per IP, answering 429 with a Retry-After header Responses are 401 (missing/bad key), 403 (disabled), 404 (configured table gone) and 429 (rate limited) Usage statistics A new spw_external_api_log table records each successful request — API, table, rows returned and duration — with a stats/log view and a purge action in the admin module, so you can see exactly how your keys are being used. Changed includes/db.php gained sys_table_prefix() / is_system_table() helpers so the endpoint can refuse system tables. includes/crypto.php gained secret_hash() (HMAC-SHA2

2026-09-08 原文 →
AI 资讯

One question, 437,000 tokens: what real agents found in our MCP server

One question. 437,000 input tokens. Not a hard question either. An agent connected to our MCP server, asked something a support engineer answers in a sentence, and worked its way there through twenty tool calls, each one dragging every earlier answer along behind it. Nothing was broken while that happened. The server answered initialize correctly, spoke the 2025-03-26 revision, returned valid JSON-RPC to everything we threw at it. All of which turned out to be beside the point. So we pointed real agents at production and watched. 18 scenarios, two vendors, a 5 dollar budget that we topped up once. This is the long version with the traces in it. There is a shorter one on our blog if you only want the conclusions. What the server is Briefly, because it shapes everything below. FoxNose stores content as collections: schema-defined records with typed fields, some of them vector indexed. The MCP server is generated from that schema and served from the same URL prefix as the REST API . Fixed catalog of seven tools regardless of how many collections exist, five read and two optional write. Two of those properties matter below. Collections are what an agent chooses between, so a badly described collection is effectively invisible. And the agent inherits exactly the rights of the API key it connects with, so there is no second allowed-tools list drifting out of sync with the first. What the harness actually is A scenario is a question in plain English, a set of tools, and a check. The checks are where we made the most mistakes, so start there. They do not look at the answer text. Model output moves between runs, and a suite that asserts on wording is a suite you quietly stop trusting. They look at the trace: which tools ran, in what order, with what arguments, which errors came back, how many tokens the whole thing burned. A check is a small predicate over the run: any_of ( no_tool_errors (), recovered_after ( " unknown_resource " , then = " search_records " ), ) That second

2026-09-08 原文 →
AI 资讯

Craigslist's JSON-LD has no ID field — we join 290 of 325 listings by title alone

Quick answer Craigslist search pages ship two copies of every listing: a static HTML list, and a JSON-LD <script> block with images, currency, and geo-coordinates. The obvious move is to join them by ID. Don't — Craigslist's JSON-LD carries no shared identifier at all , not a bare post ID, not a URL, not a SKU. The only field both copies reliably share is the listing's title, and titles repeat. We joined by title through a per-title FIFO queue and measured it recovering 290 of 325 listings (89%) end-to-end on a captured 298-item page. That number is the ceiling of what a title-only join can do on this page shape — plan your field completeness around it, don't assume 100%. Why can't you just match the JSON-LD by ID? 🧩 When we built the Craigslist Multi-City Listings Scraper , the first design assumed what almost every JSON-LD block on almost every e-commerce-shaped site provides: a productID , a sku , a url , an @id — something that lines up a JSON entry with its DOM counterpart deterministically. Live inspection of a captured Craigslist search page found none of those. Each itemListElement entry has exactly name , image , offers , @type , and a position field that looks like it should solve the problem — until you check it against the static list past the first ~18 entries, where some static-list rows have no JSON-LD counterpart at all and the position numbering drifts out of alignment. So the join key that's actually usable, live, is title — and titles aren't unique. The fix is a FIFO queue per title: walk the static <li> list in document order, and for each title pop the next unconsumed JSON-LD entry with a matching name. # actors/craigslist-listings-scraper/src/search_parser.py def _parse_ld_json ( tree : HTMLParser ) -> dict [ str , deque [ _LdEntry ]]: by_title : dict [ str , deque [ _LdEntry ]] = defaultdict ( deque ) for list_item in data . get ( " itemListElement " , []): entry = _ld_entry_from_item ( list_item ) title = list_item . get ( " item " , {}). get

2026-09-08 原文 →
AI 资讯

BBB.org's business data isn't in the HTML — it's in an analytics script tag

Quick answer BBB.org doesn't render business data into the HTML table it looks like it does. Both the search-results page and every business profile page embed the real data as one inline JSON blob — webDigitalData — sitting inside a <script> tag meant for analytics, not for you. The visible <dt> / <dd> list underneath it only carries the fields the analytics layer left out (accreditation date, years in business). If you scrape the DOM table and ignore the script tag, you'll get a name and maybe a phone number and nothing else. If you scrape the script tag and ignore the DOM, you'll get ratings and IDs but no address, no years-in-business, no website. You need both, merged, per business. Why does the DOM only have half the data? 🕵️ When we built the BBB Business Leads Scraper , the first pass assumed BBB profile pages worked like most directory sites: a template with labeled fields you css_first() your way through. That assumption survives for exactly four fields — address, accreditation date, years in business, and website — which live nowhere except a <dt> / <dd> definition list further down the page. Everything that actually matters for lead scoring — business name, BBB letter grade, accreditation status, phone number, the internal IDs BBB uses to build its own canonical URL — comes from a different place entirely: a webDigitalData object, wired into the page for BBB's own analytics vendor, that happens to be complete, well-formed JSON: # actors/bbb-business-leads-scraper/src/parsers/common.py WEB_DIGITAL_DATA_MARKER = " webDigitalData " def extract_web_digital_data ( html : str ) -> dict | None : marker_pos = html . find ( WEB_DIGITAL_DATA_MARKER ) if marker_pos == - 1 : return None brace_start = html . find ( " { " , marker_pos ) raw = _extract_balanced_json ( html , brace_start ) ... return json . loads ( raw ) That is not a regex grabbing {.*} between two markers — a naive non-greedy match snaps shut on the first stray } inside a nested object, which this blo

2026-09-08 原文 →
AI 资讯

Why your Amazon payouts never match per order (and how to reconcile the FX gap)

If you sell on a marketplace whose currency differs from your bank's, you have probably tried to compute, per order, how much you will actually be paid — and found that the numbers never tie out. Here is why, and the reconciliation approach that does tie out to the cent. There is no per-order exchange rate. By design. The SP-API does not expose a per-order FX rate anywhere, and it is not an oversight: Orders API (getOrders/getOrder): amounts in the marketplace currency only. No FX, no converted amount. Finances API (listFinancialEvents / listTransactions): also marketplace/transaction currency only, and no applied FX rate. Transfers API: gives you the amount actually disbursed — but at the disbursement level, aggregated, not per order. The reason is that Amazon does not convert per order. When Amazon Currency Converter for Sellers (ACCS) is used, the conversion is applied once to the net disbursement — many orders, fees, refunds and reserves aggregated — at ACCS's own rate and timing. What this breaks If you build your books by applying a spot FX rate to each order, they will never reconcile against your bank deposits. The residual sum(converted_orders) - actual_transfer becomes a moving error you cannot close, because your per-order spot rates were never the rate Amazon used. The approach that reconciles Stop reconciling per order. Reconcile per disbursement: Group every financial event that belongs to a settlement/disbursement, in the marketplace currency, and sum to the net. Compare against the transferred amount in your bank currency. net_bank / net_marketplace is the effective blended FX rate for that disbursement. That single rate ties out exactly, because it is derived from what actually happened, not from an external rate feed. If you need a per-order figure (for COGS or margin), allocate the blended rate proportionally to each order's net. It is an allocation, not Amazon's per-order rate (which does not exist) — but every order rolls up to the deposit you a

2026-09-08 原文 →
AI 资讯

How to Send Email from Cloudflare Workers

There are two real ways to do this: Cloudflare's own native Email Service binding, or calling an external email API like Notify over fetch() . I'll walk through both, but I want to flag something about the native option up front that's easy to miss until you're actually setting it up: it currently requires the Workers Paid plan, not just a Cloudflare account. If you're on the free Workers tier and just want to send a password reset email, that's worth knowing before you spend time on it. Option 1: Cloudflare's Native Email Service Binding Cloudflare's Email Service (which covers both sending and receiving) lets a Worker send email through a binding, with no external API key. As of now it's still in beta, and there's a real gate on it: sending to arbitrary recipients requires the Workers Paid plan, and before you've fully onboarded a domain, the binding can only send to destination addresses you've explicitly verified. Setup looks like this: In the Cloudflare dashboard, go to Compute > Email Service > Email Sending , click Onboard Domain , and pick the domain you want to send from. Cloudflare adds the DNS records it needs automatically — an SPF record, a DKIM record, a DMARC record, and MX records on a cf-bounce subdomain. This usually finishes in minutes, though Cloudflare says it can take up to 24 hours. Add the binding to your Wrangler config: { "send_email" : [{ "name" : "EMAIL" }] } Send from your Worker: export default { async fetch ( request , env , ctx ) { await env . EMAIL . send ({ from : " noreply@yourdomain.com " , to : " user@example.com " , subject : " Welcome! " , html : " <h1>Thanks for signing up.</h1> " , }); return new Response ( " Email sent! " , { status : 200 }); }, }; A couple of things worth knowing before you build on this: by default, wrangler dev simulates the binding locally — emails are logged to your console, not actually sent — unless you set remote: true on the binding to send real mail during local development. And you can restrict wh

2026-09-08 原文 →
AI 资讯

Zone Redundancy Comes to API Management Standard v2

Microsoft has enabled zone redundancy on the Standard v2 tier of Azure API Management, following its arrival on Premium v2 in December. Standard v2 starts at $700 per month against $2,801 for Premium v2, but carries a 99.95% SLA rather than 99.99%. Zone redundancy can only be configured when creating an instance. By Steef-Jan Wiggers

2026-09-07 原文 →
AI 资讯

Building a Zero-Dependency Validation API on Cloudflare Workers

The idea I wanted a small side project that could actually run itself once shipped — no cron jobs to babysit, no upstream API to go down at 3am and take my uptime with it. That constraint led somewhere specific: an API that validates common business data formats — phone numbers, IBAN, VAT/tax IDs, BIC/SWIFT codes, credit card numbers, postal codes — using nothing but offline checksum and format rules. No third-party lookups. No API keys to rotate for an upstream provider. No rate limits inherited from someone else's infrastructure. If it's slow or wrong, it's my bug, not a dependency's outage. The stack Hono on Cloudflare Workers — TypeScript, no cold starts, runs on the free tier comfortably up to 100k requests/day libphonenumber-js , ibantools , jsvat , card-validator — all well-maintained, all pure computation, zero network calls Vitest for tests, run against real fixtures (not made-up test data — every "valid" example in my test suite is a real IBAN/VAT/card number pulled from each library's own published examples, verified against the actual library output before I trusted it) The whole thing is about 300 lines of TypeScript across the router and six validator modules. Small enough to actually reason about, which mattered more to me than feature breadth. app . post ( " /v1/iban/validate " , async ( c ) => { const body = await c . req . json < { iban ?: string } > (). catch (() => null ); if ( ! body ?. iban ) { return c . json ({ error : " missing required field: iban " }, 400 ); } return c . json ( validateIban ( body . iban )); }); The part that actually surprised me I expected the code to be the hard part. It wasn't. Deploying and listing it on RapidAPI was. Two things stood out: CORS mattered even though I "shouldn't" need it. Real production traffic through RapidAPI's gateway is server-to-server — CORS is a browser-enforced concept, so I assumed it was irrelevant. But RapidAPI's own in-dashboard request tester runs as a real browser fetch, and without an O

2026-09-07 原文 →
AI 资讯

How Freebuff, AgentRouter, OpenRouter, and Experiential Labs Give You Free AI Models (And the Business Tactics Behind It)

Frontier AI models are expensive to call directly. A single day of heavy Claude or GPT-5 usage in an agentic coding loop can rack up real money. But a small cluster of gateways and coding-agent products has figured out how to hand developers meaningful free access anyway. This post breaks down four of them — Freebuff, AgentRouter, OpenRouter, and Experiential Labs — and the actual tactics each one uses to keep the lights on while giving inference away. 1. OpenRouter — the "free router" and community-subsidized models OpenRouter is a unified, OpenAI-compatible API that sits in front of hundreds of models from dozens of providers. Its free tier isn't a special OpenRouter model — it's a curated set of models, mostly open-weight ones like DeepSeek R1, Llama variants, and Qwen releases, that carry a literal $0/M-token price tag because providers or OpenRouter itself are subsidizing the compute. The tactic: instead of making you pick a free model by hand, OpenRouter built openrouter/free , a router that automatically picks a working free model for each request, smart enough to filter for whatever the request needs — image understanding, tool calling, structured outputs, and so on. That's a neat trick: it turns "which free model works today" from a research chore into a solved problem, since free-model availability shifts constantly and the router absorbs that churn for you. To keep this sustainable, OpenRouter caps usage per key — community trackers put it at roughly 20 requests per minute and 200 requests per day on the free tier — and openly frames free access as ecosystem-building: it says free models help democratize access to AI and let large numbers of people experiment and learn, while it keeps expanding capacity by onboarding new providers and covering some costs directly. In plain terms, the free tier is marketing and community goodwill; paid usage across the rest of the catalog is the actual business. Using it is as simple as pointing any OpenAI-compatible SDK a

2026-09-07 原文 →
AI 资讯

How to fetch the RBA cash rate in Python (without parsing CSVs)

If you have ever tried to programmatically get the current RBA cash rate, you know the drill. You open the RBA F1 statistical table, download f01hist.xls, write a pandas.read_excel call, fight with the multi-row header (Series ID on row 11, units on row 6), filter, sort, take the last row. That is 30 lines of code to get a single number that changes 11 times a year. One line (MIT, no key) from rba_mcp import client print ( client . latest ( " F1_1 " , series = " cash_rate_target " ). records [ - 1 ]. value ) # live AU.CASHRATE as of 2026-09-03: 4.35 pip install rba-mcp No key. MIT-licensed. Attribution and source URL come back with the number. Hosted gateway Use GET /v1/series/AU.CASHRATE/latest on api.ausdata.io with a free key from ausdata.io (500 calls/mo). Live 2026-09-06: cash 4.35 percent, trimmed-mean CPI 3.6 percent (2026-Q2), real rate 0.75 percent. Why the hosted path exists The RBA publishes the nominal cash rate. The ABS publishes inflation. Neither publishes the real cash rate (nominal minus trimmed-mean CPI). Use /v1/real-rate-regime for that join. Same envelope across nine AU sources: source, source_url, attribution, retrieved_at. MCP Wire ausdata-mcp via npx in Claude Desktop or Cursor. Sisters on PyPI run fully local with no key. What this is not Suburb-level property prices Live KYC / company-officer lookup 5-minute wholesale electricity bid stacks AU macro public data, one envelope, citations done for you. R users readrba by Matt Cowgill is the R equivalent. This is the Python / JS / agent path. Links Canonical: https://ausdata.io/blog/rba-cash-rate-python-api/ Free key: https://ausdata.io Series: https://ausdata.io/series/AU.CASHRATE PyPI: rba-mcp

2026-09-06 原文 →
AI 资讯

Checking If a Business's Google Profile Actually Matches Its Own Website

If you do local SEO work, you've run into this: a client's Google Business Profile says one phone number, their website footer says another, and nobody noticed until a customer called the wrong number. Or the postal code on the GBP listing is a leftover from an old office. This kind of drift is called a NAP (Name, Address, Phone) inconsistency, and it's widely cited as a local search ranking factor. But checking it by hand means opening every listing and every website side by side. I built Google Maps NAP Consistency Checker , an Apify Actor that takes Google Maps scraper output, fetches each business's own website (lightly: homepage plus one likely subpage), and checks whether the name, postal code, and phone number on the Google Business Profile actually show up on the site. What it does, and what it doesn't This Actor checks one thing: does a business's own website agree with its Google Business Profile on name, postal code, and phone number. It does not check third-party directories (Yelp, Facebook, etc.). That's a different problem with a different competitor landscape. It does not crawl an entire website; it fetches at most two pages per business (homepage, plus a subpage if one with a keyword like "contact" or "about" is linked from it). It does not use an LLM. It's regex and string matching against fetched text, which makes it fast, cheap, and predictable. There's no model that can hallucinate a match that isn't there. Businesses with no independent website (only a social profile, or nothing) are skipped entirely, because there's nothing to fetch and compare against. How it works For each place with a real website, the Actor: Checks robots.txt for that domain before fetching anything, and skips the business if the checker's user agent isn't allowed. Fetches the homepage HTML (up to 3 MB), strips <script> , <style> , and comments before converting to text, so JavaScript variables and tracking IDs don't get misread as phone numbers. Looks for an internal link

2026-09-06 原文 →
AI 资讯

Returning RFC 9457 Problem Details from Go Validation Errors

How to turn struct-tag validation failures into a standard "application/problem+json" response — the RFC 9457 format — with one method call, no hand-rolled error envelope. Most Go APIs invent their own validation error shape. One team returns {"errors": [...]} , another {"field_errors": {...}} , a third just a flat {"error": "message"} and hopes the client parses it. Every one of those is a private contract the client has to learn from your docs, because there's no shared shape for "here's what's wrong with your request." RFC 9457 — Problem Details for HTTP APIs — is the IETF standard that fixes this: a application/problem+json body with type , title , status , and room for problem-specific extensions. Checker now builds one of these directly from a failed struct validation, via CheckErrors.ProblemDetails() . The shape RFC 9457 defines four base members — type , title , status , detail , instance — and lets a specific problem type add its own. For validation errors, RFC 9457 §3.1 sketches exactly this extension: an invalid-params array listing which fields failed and why. That's what Checker produces. From a failed struct to a problem+json body Take a struct with a missing required field: type Person struct { Name string `checkers:"required"` } person := & Person {} errs , ok := checker . CheckStruct ( person ) if ! ok { data , _ := json . Marshal ( errs . ProblemDetails ()) fmt . Println ( string ( data )) } { "type" : "about:blank" , "title" : "Your request parameters failed validation." , "status" : 400 , "invalid-params" : [ { "name" : "Name" , "reason" : "Required value is missing." , "code" : "REQUIRED" } ] } One method call — errs.ProblemDetails() — turns the same CheckErrors you'd otherwise call .JSON() on into a *ProblemDetails value, ready to marshal. type defaults to "about:blank" (RFC 9457's own default for "no more specific problem type registered"), status defaults to 400 , and each invalid-params entry carries the field name , a localized human-readab

2026-09-06 原文 →