npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usethrottle/quotes

v0.3.0

Published

Buyer-facing React components, hooks and server proxy for Throttle B2B quotes — hosted quote links and RFQ request forms.

Readme

@usethrottle/quotes

Buyer-facing React components, hooks and a same-origin server proxy for Throttle B2B quotes — the hosted quote link and the RFQ request form.

This is the buyer half of quotes. Merchant-side operations (create, price, issue, revise, record acceptance) live in @usethrottle/api-client's QuotesService, which needs a secret key and must stay server-side.

npm install @usethrottle/quotes

The buyer quote page

A quote link is /(your route)/:token, where the token is the qtl_… value from the buyer's email. The token is the only credential — there is no API key in the browser.

import { QuoteView } from '@usethrottle/quotes';

export default function QuotePage({ params }: { params: { token: string } }) {
  return <QuoteView token={params.token} />;
}

QuoteView renders line items with optional toggles and editable quantities, live totals, revision history, the comment thread, and the accept / request-changes / decline actions. On acceptance it sends the buyer to Throttle hosted checkout.

The default markup is intentionally plain. For your own presentation, use the hook and render whatever you like:

import { useQuote, formatMoney } from '@usethrottle/quotes';

function MyQuote({ token }: { token: string }) {
  const { quote, total, selections, setQuantity, setSelected, accept, error } = useQuote(token);
  if (!quote) return null;

  return (
    <>
      {quote.currentRevision?.items.map((item) => (
        <Row
          key={item.id}
          item={item}
          state={selections[item.id]}
          onQty={(q) => setQuantity(item.id, q)}
          onToggle={(on) => setSelected(item.id, on)}
        />
      ))}
      <strong>{formatMoney(total, quote.currency)}</strong>
      <button onClick={() => accept({ acceptedByName: 'Ada', acceptedByEmail: '[email protected]' })}>
        Accept
      </button>
    </>
  );
}

useQuote fires the view beacon once per mount, seeds selections from the revision's own defaults, and computes total with the same rule the server applies at acceptance — including scaling a line's tax and discount pro-rata when the buyer changes its quantity. Keep using total rather than summing the lines yourself, or the number you show may not be the number charged.

Errors worth handling

Everything throws ThrottleQuotesError carrying the API's own code:

| code | meaning | | --- | --- | | revision_superseded | A newer revision was issued. error.currentRevisionId names it; useQuote auto-refreshes. | | quote_expired | The offer lapsed. Show "request updated pricing". | | invalid_item_selection | A locked line's quantity was changed, a required line deselected, or an unknown item sent. | | accept_in_progress | A concurrent accept won the race; retry shortly. | | not_found | Unknown or rotated token, or a draft/archived quote. |

import { ThrottleQuotesError } from '@usethrottle/quotes';

try {
  await accept({ acceptedByName, acceptedByEmail });
} catch (e) {
  if (e instanceof ThrottleQuotesError && e.isExpired) showExpiredNotice();
}

RFQ request form

import { QuoteRequestForm } from '@usethrottle/quotes';

<QuoteRequestForm formToken={params.formToken} />;

Bot protection is wired in: the honeypot field and the mount-time anchor that feeds the API's minimum-fill-time check. A honeypot hit returns a success-shaped response and creates nothing — by design, so a bot cannot learn it was caught. Treat submission success as "accepted", not "created".

If the form requires Cloudflare Turnstile, render the widget yourself and pass its token — this package pulls in no third-party script:

<QuoteRequestForm formToken={token} turnstileToken={turnstileToken} />

Add to Quote (quote cart)

Buyers who want several things quoted can collect them while they browse. The cart persists across page navigation and merges quantities when the same SKU is added twice. Throttle stores no catalog, so lines come from your own product data — the same arrangement as the shopping cart.

import { useQuoteCart, QuoteRequestForm } from '@usethrottle/quotes';

function AddToQuote({ product }) {
  const { add, count } = useQuoteCart();
  return (
    <button
      onClick={() =>
        add({ name: product.title, sku: product.sku, referenceId: product.id, quantity: 1 })
      }
    >
      Add to quote ({count})
    </button>
  );
}

// On your request page: submit everything they collected as one RFQ.
function RequestPage({ formToken }) {
  const { lines } = useQuoteCart();
  return <QuoteRequestForm formToken={formToken} items={lines} />;
}

A request needs items, a written message, or both. Lines arrive on the merchant's working revision unpriced: targetUnitPrice is recorded as the buyer's ask and shown to the rep, and never becomes the quoted price — nothing sent from a browser can set what a merchant charges.

Outside React, createQuoteCart() exposes the same add/update/remove/ subscribe/toRequestItems surface for a plain-JS storefront.

Same-origin proxy (optional)

Quote links work fine calling Throttle directly. Proxy through your own origin if you'd rather not have a third-party host in the address bar or CSP:

// app/api/throttle/[...path]/route.ts
import { createQuoteProxyHandler } from '@usethrottle/quotes/server';

const handler = createQuoteProxyHandler();
export { handler as GET, handler as POST };
<QuoteView token={token} options={{ baseUrl: 'https://yourshop.com/api/throttle' }} />

The proxy forwards no API key — the buyer's token is the only credential. It allow-lists the public quote paths, so it cannot be pointed at a merchant endpoint, and forwards x-forwarded-for so acceptance evidence and per-IP limits attribute to the real buyer.

Payment terms

A quote carries one of three terms, on currentRevision.paymentTerms:

  • pay_in_full — card at checkout
  • net_terms — invoice due in netN days
  • deposit_balancedepositAmount charged today by card, the remainder invoiced balanceNetN days out

depositAmount is frozen when the rep issues the quote, so it does not move when the buyer edits quantities. Selections that would drop the total to or below the deposit are rejected with invalid_item_selection.

License

MIT