{"_id":"@bsv/eqc","_rev":"3-ebba98145c72134dc923a91f19643e37","name":"@bsv/eqc","dist-tags":{"latest":"0.1.0"},"versions":{"0.0.0-stage":{"name":"@bsv/eqc","version":"0.0.0-stage","_id":"@bsv/eqc@0.0.0-stage","stub":true,"maintainers":[{"name":"tyeverett","email":"p2ppsr@tyweb.us"}],"dist":{"shasum":"2d34ef471be31ca068c598126eef9161e76ab820","tarball":"https://registry.npmjs.org/@bsv/eqc/-/eqc-0.0.0-stage.tgz","fileCount":2,"integrity":"sha512-sxCza4Nr9U5lJJtCUopLT8ULgibhDQu1g8tN0HbnPkRQF33PhcUldpMfl4ZKDkETe2KUK9KYLRF5jIEvX8XkBA==","signatures":[{"sig":"MEUCIE3be7c2+8NSMFgiEgOnFPBtVPYgVh61ptufXqvXH9pgAiEAw1WLp3cZK3beWfHdaLR57VBVtnjvaL1PWOpxBKlBFZ0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":330},"_npmUser":{"name":"tyeverett","email":"p2ppsr@tyweb.us"},"description":"Temporary package placeholder for staged publishing","directories":{}},"0.1.0":{"_id":"@bsv/eqc@0.1.0","bugs":{"url":"https://github.com/bsv-blockchain/ts-stack/issues"},"dist":{"shasum":"77ca963d62bfb2da6802ef8930e40bc82d265f5e","tarball":"https://registry.npmjs.org/@bsv/eqc/-/eqc-0.1.0.tgz","fileCount":21,"integrity":"sha512-86J302K2sQXrhyWLEFnctZGmCWRQGtQmvtCqc/VqgRSyXFvi87zPBziK1FQ9HuZDiTJd6G4rcjKGzo3GOVYdJg==","signatures":[{"sig":"MEUCIH7vy5/y4d9M2LKGtKnmUgVBHOvFT2HltpKXRNO65rIMAiEAi/0qTvTHsZYkkbWmpEATybRKfunv5N06tYdkF+a2A5Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDsAVBWi5VybgxmdSQW7ERxbkMAjHzH5BfCWwRO2nlIeAiBH/HmQ9CJqZ9ba4s85+TETvU6NkqNFUAjuqcJVbmm44Q=="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bsv%2feqc@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":678580},"main":"./dist/index.cjs","name":"@bsv/eqc","type":"commonjs","_from":"file:/home/runner/work/ts-stack/ts-stack/release-artifacts/packages/bsv-eqc-0.1.0.tgz","types":"./dist/index.d.cts","author":{"name":"BSV Association"},"module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./host":{"import":{"types":"./dist/host.d.mts","default":"./dist/host.mjs"},"require":{"types":"./dist/host.d.cts","default":"./dist/host.cjs"}}},"license":"SEE LICENSE IN LICENSE.txt","scripts":{"doc":"ts2md --inputFilename=src/index.ts --outputFilename=docs/client.md --filenameSubString=client --firstHeadingLevel=1 && ts2md --inputFilename=src/host.ts --outputFilename=docs/host.md --filenameSubString=host --firstHeadingLevel=1","lint":"oxlint src test vitest.config.ts --deny-warnings","test":"vitest run","build":"tsdown src/index.ts src/host.ts --format cjs,esm --dts --sourcemap --clean --out-dir dist --tsconfig tsconfig.build.json","typecheck":"tsc --project tsconfig.typecheck.json","pack:check":"node ../../../scripts/check-package-artifact.mjs . --exports EQC,EQCError,ECONOMIC_PATHS,DEFAULTS,computeQueryId,computePayouts,canonicalJson --entry-exports \"./host=createEconomicQueryHost|overlayLookupProvider|messageListProvider|bytesProvider\"","test:watch":"vitest","format:check":"pnpm --workspace-root exec prettier --check \"packages/overlays/eqc/src/**/*.ts\" \"packages/overlays/eqc/test/**/*.ts\" \"packages/overlays/eqc/*.{json,ts,md}\"","test:browser":"pnpm build && node ../../../scripts/check-browser-package.mjs .","test:coverage":"vitest run --coverage","test:property":"vitest run src/protocol/protocol.property.test.ts"},"version":"0.1.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"4f6409db-ff22-4ba0-95bf-03af48c61b84"}},"homepage":"https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/eqc#readme","keywords":["bsv","brc-178","overlay","message-box","micropayments","eqc"],"_resolved":"/home/runner/work/ts-stack/ts-stack/release-artifacts/packages/bsv-eqc-0.1.0.tgz","_integrity":"sha512-86J302K2sQXrhyWLEFnctZGmCWRQGtQmvtCqc/VqgRSyXFvi87zPBziK1FQ9HuZDiTJd6G4rcjKGzo3GOVYdJg==","repository":{"url":"git+https://github.com/bsv-blockchain/ts-stack.git","type":"git","directory":"packages/overlays/eqc"},"_npmVersion":"11.19.1","description":"Economic Query Client (EQC) and host handlers for BRC-178 race-settled collection markets over BSV overlay lookups and message boxes","directories":{},"maintainers":[{"name":"johngalt5","email":"dylan@murraydt.com"},{"name":"loftsteinn","email":"oli@oskarsson.nl"},{"name":"dkellen","email":"darrkellen@gmail.com"},{"name":"braydude","email":"brayden167@gmail.com"},{"name":"tyeverett","email":"p2ppsr@tyweb.us"}],"sideEffects":false,"_nodeVersion":"24.22.1","dependencies":{"express-rate-limit":"8.7.0"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"host":["dist/host.d.cts"]}},"_hasShrinkwrap":false,"devDependencies":{"ts2md":"^0.2.8","oxlint":"^1.83.0","tsdown":"0.23.0","vitest":"^5.0.1","express":"^5.2.1","@bsv/sdk":"^4.0.1","fast-check":"^4.9.0","typescript":"npm:@typescript/typescript6@6.0.2","@types/node":"^26.6.1","@types/express":"^5.0.6","@typescript/native":"npm:typescript@7.0.2","@vitest/coverage-v8":"5.0.1","@bsv/auth-express-middleware":"^2.3.0"},"peerDependencies":{"@bsv/sdk":"^2.8.0 || ^3.0.0 || ^4.0.1"},"peerDependenciesMeta":{"@bsv/sdk":{"optional":false}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/eqc_0.1.0_1793150570971_0.35337750054033323"}}},"time":{"created":"2026-10-27T17:29:54.068Z","modified":"2026-10-28T01:22:51.500Z","0.0.0-stage":"2026-10-27T17:29:54.068Z","0.1.0":"2026-10-28T01:22:51.068Z"},"description":"Economic Query Client (EQC) and host handlers for BRC-178 race-settled collection markets over BSV overlay lookups and message boxes","maintainers":[{"name":"johngalt5","email":"dylan@murraydt.com"},{"name":"loftsteinn","email":"oli@oskarsson.nl"},{"name":"dkellen","email":"darrkellen@gmail.com"},{"name":"braydude","email":"brayden167@gmail.com"},{"name":"tyeverett","email":"p2ppsr@tyweb.us"}],"readme":"# @bsv/eqc\n\nEconomic Query Client for [BRC-178](https://bsv.brc.dev/overlays/0178) race-settled collection\nmarkets. Overlay nodes and message box servers answer the same query. The client\nranks them by the time their answers actually arrive and pays the fastest hosts that agree on\nthe answer, so propagating data to peers is how a host becomes eligible to be paid.\n\n## Install\n\n```bash\nnpm install @bsv/eqc @bsv/sdk\n```\n\n`@bsv/sdk` is a peer dependency. The package has no other runtime dependency.\n\n## Usage\n\n### Client\n\n```ts\nimport { EQC } from '@bsv/eqc'\n\nconst eqc = new EQC(wallet)\n\nconst answer = await eqc.lookup({ service: 'ls_example', query: { key: 'value' } })\nconst messages = await eqc.listMessages({ messageBox: 'payment_inbox' })\n\nconst result = await eqc.query({\n  type: 'overlay-lookup',\n  params: { service: 'ls_example', query: { key: 'value' } }\n})\nconsole.log(result.ranking, result.txid, result.consistency)\n```\n\nThe client takes no host list. Like `LookupResolver`, it bootstraps from `DEFAULT_SLAP_TRACKERS`\nand accepts the same network options: `networkPreset`, `slapTrackers`, `hostOverrides`, and\n`additionalHosts`. Every query begins with an `ls_slap` lookup on the free BRC-24 `/lookup`\nroute; the fee of the query that follows covers it. An `ls_slap` lookup issued as the target of\n`eqc.lookup` is raced and paid like any other.\n\n| Option                | Default | Meaning                                                                           |\n| --------------------- | ------- | --------------------------------------------------------------------------------- |\n| `threshold`           | 3       | Distinct host identity keys that must attest one content hash before any payment. |\n| `topK`                | 5       | Fastest agreeing hosts that share the fee; hosts advertising less are skipped.    |\n| `raceMs`              | 400     | Window after the first valid attestation.                                         |\n| `floorFeeSats`        | 1000    | Minimum total fee.                                                                |\n| `maxFeeSats`          | 2000    | Budget per query; hosts with a higher floor are skipped.                          |\n| `feeSats`             | —       | Optional fee above the floor, capped by `maxFeeSats`.                             |\n| `hostTimeoutMs`       | 5000    | Deadline for each authenticated call to a host.                                   |\n| `paramsTimeoutMs`     | 2000    | Deadline for each `/economic/params` probe.                                       |\n| `maxHosts`            | 16      | Hosts contacted per query, best reputation first, ties in random order.           |\n| `collectAttempts`     | 3       | Sends of a paid collect before the host is given up on.                           |\n| `collectRetryDelayMs` | 250     | Pause before a collect is sent again.                                             |\n\nAt the defaults, five hosts receive 269, 238, 206, 168, and 119 satoshis from one transaction. A\nranked host whose share is below the `minPayoutSats` it advertises gets no output and no collect\nrequest, because it would refuse the output; it appears in `result.rejected` with\n`share below host minPayoutSats`, and `result.feeSats` is the total actually paid.\n\nA host's `/economic/params` is cached for five minutes, and so is a 4xx answer, which means the\nhost runs no market. A timeout, a network or parse failure, or a 5xx answer is remembered for at\nmost 15 seconds, so a host that stalls its probe costs one `paramsTimeoutMs` wait per interval\nrather than one per query.\n\nReputation is local to the client. A host that serves wrong bytes, signs with the wrong key, or\nfails a collect (every collect follows a dispatched payout) is excluded for ten minutes. Timeouts,\nlate or minority answers, and a session key that no SLAP advertisement names only lower its rank.\n\nA collect that fails in transit, or that finds the host still settling an earlier collect for\nthe same query (409 `ERR_QUERY_SETTLING`), is sent again with the same payment, up to\n`collectAttempts` times. The host answers a repeat with the delivery it already recorded and\nnever internalizes a second payment, so a lost response costs a retry, not a second fee.\n\n`query()` resolves as soon as one paid host delivers bytes whose `SHA-256` equals the attested\ncontent hash. Collects to the other paid hosts continue in the background; `result.completion`\nresolves when they finish, and `result.rejected` lists every host that failed and why.\n\nFailures are `EQCError` with a `code`: `ERR_EQC_NO_HOSTS`, `ERR_EQC_BUDGET`,\n`ERR_EQC_NO_ATTESTATION`, `ERR_EQC_THRESHOLD` (nothing was paid), `ERR_EQC_PAYMENT`, and\n`ERR_EQC_UNDELIVERED` (carries the payout `txid`).\n\nQuery parameters are hashed as canonical JSON, which allows integers but not fractional numbers.\n\n### Host\n\n```ts\nimport { createEconomicQueryHost, overlayLookupProvider } from '@bsv/eqc/host'\n\nconst host = createEconomicQueryHost({\n  wallet,\n  providers: [overlayLookupProvider({ engine, anchorTopics: { ls_example: ['tm_example'] } })]\n})\nhost.mount(router) // GET /economic/params, POST /economic/query, POST /economic/collect\n```\n\nThe handlers are typed structurally, so an express `Router` or `Application` satisfies them and\nthis package does not depend on express. Mount them behind `express.json()` and\n`@bsv/auth-express-middleware`.\n\n`wallet` signs attestations and deliveries and internalizes payouts, so it must have a storage\nprovider. Its identity key must be the key the server authenticates with over BRC-103 and the key\nits SLAP token advertises.\n\nWith `@bsv/overlay-express`, do not pass the `wallet` the router factory receives. That wallet is\nthe BRC-103 authentication wallet: it has no storage provider and cannot create or internalize\nactions, so the host would attest, the client would pay, and every collect would fail. Build a\nstorage-backed wallet from the same root key as the node instead:\n\n```ts\nconst hostWallet = await buildStorageBackedWallet(serverPrivateKey)\n\nserver.registerRouter('/', ({ engine }) => {\n  const router = express.Router()\n  createEconomicQueryHost({\n    wallet: hostWallet,\n    providers: [overlayLookupProvider({ engine })]\n  }).mount(router)\n  return router\n})\n```\n\n`buildStorageBackedWallet` is yours to supply: it returns a wallet with an active storage\nprovider, built from the private key the overlay server was configured with.\n\nIf the wallet throws while internalizing, the host logs the query ID, rank, client key, and payout\n`txid` through `logger.error` and answers 500, never 402: the payout is already on the network and\nthe log line is what lets the operator claim it later.\n\n| Option            | Default | Meaning                                                                  |\n| ----------------- | ------- | ------------------------------------------------------------------------ |\n| `threshold`       | 3       | Advertised default threshold.                                            |\n| `topK`            | 5       | Advertised, and the largest `topK` accepted; more is answered 400.       |\n| `floorFeeSats`    | 1000    | Minimum total fee, a number or a function of the canonical payload size. |\n| `minPayoutSats`   | 1       | Smallest output a collect is served for.                                 |\n| `maxQueryTtlMs`   | 60000   | Furthest `expires` accepted.                                             |\n| `maxPayloadBytes` | 2 MiB   | Largest estimated encoded delivery attested.                             |\n| `rateLimit`       | 240/min | Requests per client across the mounted routes; `{ windowMs, limit }`.    |\n\n`floorFeeSats` is a floor on the fee of a query, not on what this host earns from a delivery. The\nhost is held to the fee it quoted in its signed attestation: at rank `i` of `k` it requires\n`floor(quotedFeeSats × weight(i) / S)`, without the rounding remainder rank 1 is paid, so any fee\nat or above the quote is served at every rank. The client chooses `k` up to the host's `topK`, so\nthe enforceable minimum per delivery is `max(minPayoutSats, share at rank K of K)`: 119 satoshis at\nthe defaults. Raise `minPayoutSats` or lower `topK` to raise it; clients skip a host whose\n`topK` is too small for them and send no output below its `minPayoutSats`.\n\n`maxPayloadBytes` is compared against the estimated size of the collect response: base64 of the\npayload and of the supplement plus 1024 bytes of envelope. Keep it below the response limit of\nthe server the routes are mounted on, because a larger delivery is replaced by a 413 only after\nthe payment was internalized. The 2 MiB default fits the smallest `@bsv/overlay-express` response\nprofile (`MAX_RESPONSE_BYTES` is 4 MiB on `small`, 8 MiB on `standard`).\n\nThe pending store holds each attested answer until it is collected or expires. Its byte budget\ncounts the cached payload, the stored query, and retained deliveries, and an unsettled record\ngives up its bytes and its per-client slot the moment it expires.\n\nA paid collect records the signed delivery and the payout `txid` before it answers. If that\nanswer is lost, the same client collects again and receives the same delivery, with or without a\npayment attached; the wallet is not called again. Deliveries are kept for `deliveryRetentionMs`\nafter settlement (default ten minutes), then a repeat answers 409 `ERR_QUERY_SETTLED`. A\nconcurrent collect that arrives mid-settlement answers 409 `ERR_QUERY_SETTLING`, which the client\nretries. A settle claim lasts `settleLeaseMs` (default two minutes): if the host stops between\ninternalizing a payout and recording the delivery, a later collect takes the claim over and\ninternalizes the same transaction again, which a wallet-toolbox wallet merges, so the host wallet\nmust accept a transaction it already holds.\n\n`InMemoryPendingStore` lives in process memory, so a restart forgets pending queries and retained\ndeliveries alike. To replay paid deliveries across restarts, or across several host instances,\npass a durable `PendingStore` that persists every record field before each call resolves.\n\n`mount()` puts an [`express-rate-limit`](https://github.com/express-rate-limit/express-rate-limit)\nlimiter in front of all three routes, shared between them. An authenticated client is counted by\nits BRC-103 identity key, and anything else by address (IPv6 by `/56`); past the limit a route\nanswers 429 `ERR_RATE_LIMITED`. Behind a proxy, set express `trust proxy` to the real hop count so\nunauthenticated requests are counted by client address rather than the proxy's.\n\nProviders: `overlayLookupProvider` (BRC-24 lookups, with BRC-136 topic anchors),\n`messageListProvider` (BRC-33 listings, served only to the authenticated recipient), and\n`bytesProvider` for any read that returns a byte string.\n\n## Security notes\n\n- Arrival time is measured by the client. Host-supplied timestamps are never read.\n- The attestation signer and the BRC-103 session identity must be the same key, and that key must\n  be one a SLAP token advertises for the host's URL. SLAP is permissionless, so every key\n  advertised for a URL is kept: a third party's token cannot evict an honest host.\n- The threshold counts identity keys, not operators. Identity keys are free: a tracker, or anyone\n  who publishes SLAP tokens, can mint `threshold` keys and domains that agree on a fabricated\n  answer. Loss stays bounded by `maxFeeSats` per query and overlay outputs remain SPV-checkable,\n  but omissions and stale answers are not detectable. For reads that carry value, pin the hosts\n  you trust with `hostOverrides`.\n- The client creates outputs only for hosts that attested the winning hash, and makes no wallet\n  call when the threshold is not met.\n- Hosts are untrusted, and `AuthFetch` would pay any well-formed HTTP 402 challenge and answer a\n  BRC-103 certificate request. The transport therefore gives it a wallet facade that forwards only\n  `getPublicKey`, `createSignature`, `verifySignature`, `createHmac`, and `verifyHmac`. It lists\n  no certificates and rejects every other call. Only the payout transaction spends.\n- `/economic/params` is read with a 64 KiB cap that stops the download. Authenticated responses\n  are capped at 16 MiB inside `AuthFetch`, which stops reading once the cap is crossed. The\n  deadline rejects a slow call but does not cancel its transfer; `maxHosts` and `hostTimeoutMs`\n  bound that exposure.\n- SLAP advertisements name canonical `https`, `wss`, or `js8c+bsvauth+smf` URIs from\n  `@bsv/sdk` 2.8.0. Plain `http` hosts, accepted only under the `local` preset, therefore come\n  from `hostOverrides`, `additionalHosts`, or `slapTrackers`, never from a SLAP token.\n- An overlay lookup's content hash covers the sorted outpoint list. BEEF travels beside it and\n  must be verified by SPV like any other BEEF.\n\n## Specification\n\nBRC-178 leaves several encodings open. The choices made here, including the `/economic/*`\npaths and the 1000-satoshi default floor, are recorded in\n[the design document](../../../docs/superpowers/specs/2026-09-18-eqc-brc178-design.md).\n\n## API reference\n\nGenerated with `pnpm doc` (ts2md): [client entry](./docs/client.md) (`@bsv/eqc`) and\n[host entry](./docs/host.md) (`@bsv/eqc/host`).\n\n## License\n\nOpen BSV License — see [LICENSE.txt](./LICENSE.txt).\n\n## SDK4 hotfix peer requirement\n\nThe SDK4 peer alternative starts at `@bsv/sdk ^4.0.1`, which restores numeric\nwallet results and legacy wire compatibility. Existing SDK2/3 alternatives and\nlower bounds remain unchanged. SDK4 users should install the 4.0.1 hotfix; no\napplication-wallet wire or data migration is required.\n","readmeFilename":"README.md","homepage":"https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/eqc#readme","keywords":["bsv","brc-178","overlay","message-box","micropayments","eqc"],"repository":{"url":"git+https://github.com/bsv-blockchain/ts-stack.git","type":"git","directory":"packages/overlays/eqc"},"author":{"name":"BSV Association"},"bugs":{"url":"https://github.com/bsv-blockchain/ts-stack/issues"},"license":"SEE LICENSE IN LICENSE.txt"}