Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/usdc-payment-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@hbar-kit/payments": minor
"@hbar-kit/core": minor
---

Add USDC payment verification helpers for Hedera.

Introduces `verifyUsdcPayment` and `waitForUsdcPayment` as convenience wrappers over HTS payment
verification, with 6-decimal amount parsing, optional token ID override, docs, examples, and tests.
Adds a verified USDC token registry (`USDC_TOKEN_IDS`, `getUsdcTokenId`) with mainnet/testnet token
ids confirmed against the Mirror Node and Circle's official docs, plus `isUsdcPaymentResult` and an
optional `symbol` field on `PaymentAsset` (additive, non-breaking). Adds a new `UnsupportedAssetError`
to `@hbar-kit/core` for networks without a verified USDC token id (e.g. previewnet).
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ docs/.vitepress/cache
next-env.d.ts
.claude
.claude/
.claude.local.md
.idea/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ if (result.matched) {
No private keys, no signing, no funds held — `verifyHbarPayment` only **reads** public Mirror Node
data and tells you whether the payment you expected actually arrived.

### Verify a USDC payment

USDC on Hedera is an HTS token. hbar-kit includes a USDC convenience helper for common invoice,
checkout, and payment-link flows — it fills in the verified USDC token id and 6-decimal parsing.

```ts
import { verifyUsdcPayment } from "@hbar-kit/payments"

const result = await verifyUsdcPayment({
network: "mainnet",
receiver: "0.0.12345",
amount: "25.00",
memo: "invoice_123",
after: new Date(Date.now() - 30 * 60 * 1000),
})
```

See [Verify a USDC payment](https://devwhodevs.github.io/hbar-kit/guide/verify-usdc-payment).

## Use cases

- Hedera payment links
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default defineConfig({
items: [
{ text: "Verify an HBAR payment", link: "/guide/verify-hbar" },
{ text: "Verify an HTS token payment", link: "/guide/verify-hts" },
{ text: "Verify a USDC payment", link: "/guide/verify-usdc-payment" },
{ text: "Wait for a payment", link: "/guide/wait-for-payment" },
{ text: "Use a custom Mirror Node", link: "/guide/custom-mirror-node" },
{ text: "Partial & duplicate payments", link: "/guide/partial-and-duplicate" },
Expand Down
4 changes: 4 additions & 0 deletions docs/guide/amounts-and-decimals.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ formatUnits(2_889_029n, 6) // "2.889029"
```

Parsing is **strict**: a value with more decimals than allowed throws `InvalidAmountError`.

USDC on Hedera uses **6 decimals**. [`verifyUsdcPayment`](/guide/verify-usdc-payment) always parses
amounts at 6 decimals, so `"25.001234"` is fine but `"25.0012345"` (7 dp) throws
`InvalidAmountError`.
3 changes: 3 additions & 0 deletions docs/guide/verify-hts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ const result = await verifyHtsPayment({
```

If you omit `decimals`, hbar-kit fetches it from the token info endpoint and caches it.

> Paying in **USDC**? Use [`verifyUsdcPayment`](/guide/verify-usdc-payment) — a thin wrapper that
> fills in the verified USDC token id and 6-decimal parsing for you.
150 changes: 150 additions & 0 deletions docs/guide/verify-usdc-payment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Verify a USDC payment on Hedera

USDC on Hedera is a normal **HTS token**, so hbar-kit can already verify it with
[`verifyHtsPayment`](/guide/verify-hts). `verifyUsdcPayment` is a thin convenience wrapper that
fills in the **verified USDC token id** for the network and parses amounts at **6 decimals**, so you
don't rebuild that boilerplate for every invoice, checkout, or payment-link flow.

It is the same read-only, non-custodial verification as the rest of hbar-kit: no private keys, no
funds moved — it only reads public Mirror Node data and tells you whether the expected payment
arrived.

```ts
import { verifyUsdcPayment } from "@hbar-kit/payments"

const result = await verifyUsdcPayment({
network: "mainnet",
receiver: "0.0.12345",
amount: "25.00", // 25 USDC — a decimal string, never a float
memo: "invoice_123", // your order / invoice id
after: new Date(Date.now() - 30 * 60 * 1000),
})

if (result.matched) {
// Confirmed. Mark the invoice paid using result.transactionId as your idempotency key.
}
```

## What it does under the hood

`verifyUsdcPayment` calls `verifyHtsPayment` with:

- the canonical USDC token id for the selected `network`,
- `decimals = 6`,
- the same `receiver` / `amount` / `memo` / time-window rules,
- the same [payment status model](/guide/concepts).

The returned `PaymentResult` is identical to the HTS one, except its `asset` is tagged as USDC:

```ts
result.asset // { tokenId: "0.0.456858", decimals: 6, symbol: "USDC" }
```

Use `isUsdcPaymentResult(result)` (or inspect `result.asset.tokenId`) to detect USDC results in a
mixed pipeline.

## Verified token ids

USDC token ids are **network-specific**, so `network` is **required** — the USDC helper never
assumes mainnet. The ids are verified against the live Hedera Mirror Node and Circle's official
[USDC contract addresses](https://developers.circle.com/stablecoins/usdc-contract-addresses):

| Network | USDC token id |
| ---------- | --------------------------------------------------- |
| mainnet | `0.0.456858` |
| testnet | `0.0.429274` |
| previewnet | _not issued by Circle_ → throws `UnsupportedAssetError` |

```ts
import { getUsdcTokenId, USDC_TOKEN_IDS } from "@hbar-kit/payments"

getUsdcTokenId("mainnet") // "0.0.456858"
USDC_TOKEN_IDS.testnet // "0.0.429274"
```

## Custom / mock tokens (testnet & dev)

For local testing against a mock HTS token, pass an explicit `tokenId`. It is still parsed at 6
decimals and tagged as USDC:

```ts
const result = await verifyUsdcPayment({
network: "testnet",
tokenId: process.env.TESTNET_USDC_TOKEN_ID!, // your dev/mock token
receiver: "0.0.12345",
amount: "10.00",
memo: "test_invoice_1",
})
```

Production mainnet flows should omit `tokenId` and use the verified canonical id.

## Amount precision

Amounts are **decimal strings**, never floats, and USDC is always **6 decimals**:

```ts
"25.00" // ✅ 25 USDC
"25.001234" // ✅ 6 decimals
"25.0012345" // ❌ 7 decimals → throws InvalidAmountError
```

## Production checklist

- **Always verify server-side.** Never trust a client-supplied amount, receiver, token id, or memo
— look them up from your own records, keyed by an opaque order id.
- **Always use a time window** (`after`/`before`) to bound the search.
- **Use a unique memo per payment request** so duplicates are detectable.
- **Handle every status**, not just `confirmed`: `underpaid`, `overpaid`, `duplicate`, `mismatch`,
`pending`, `expired`.
- **Use `comparison: "atLeast"` only if you accept overpayment**; the default is exact-match.
- **Use `transactionId` for idempotency** in your own database so each payment is processed once.

```ts
const result = await verifyUsdcPayment({
network: "mainnet",
receiver: "0.0.12345",
amount: "25.00",
memo: "invoice_123",
after: new Date(Date.now() - 30 * 60 * 1000),
})

switch (result.status) {
case "confirmed":
// result.matched === true — settle the order, store result.transactionId
break
case "underpaid":
case "overpaid":
case "duplicate":
case "mismatch":
case "pending":
// branch on each explicitly
break
}
```

## Wait for a USDC payment

`waitForUsdcPayment` polls until the payment is confirmed (or `overpaid`/`duplicate`), or until
`timeoutMs` elapses (then `status: "expired"`). It mirrors `waitForHbarPayment` /
`waitForHtsPayment` and accepts `timeoutMs`, `pollIntervalMs`, and an `AbortSignal`:

```ts
import { waitForUsdcPayment } from "@hbar-kit/payments"

const result = await waitForUsdcPayment({
network: "mainnet",
receiver: "0.0.12345",
amount: "25.00",
memo: "invoice_123",
timeoutMs: 5 * 60 * 1000,
pollIntervalMs: 3000,
})
```

## When to use which

- **`verifyUsdcPayment`** — you're accepting USDC and want the token id + 6-decimal parsing handled
for you. The common case.
- **`verifyHtsPayment`** — any other HTS token, or when you need full control over `tokenId` and
`decimals`.
4 changes: 4 additions & 0 deletions docs/guide/wait-for-payment.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ const result = await waitForHbarPayment({

Pass an `AbortSignal` as `signal` to cancel early. Polling stops immediately on a confirmed,
duplicate, or overpaid result.

The same polling exists for tokens — `waitForHtsPayment` and
[`waitForUsdcPayment`](/guide/verify-usdc-payment) — with identical `timeoutMs` / `pollIntervalMs` /
`signal` options.
15 changes: 13 additions & 2 deletions docs/reference/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface PaymentResult {
| "expired"
| "failed"
receiver: string
asset: "HBAR" | { tokenId: string; decimals: number }
asset: "HBAR" | { tokenId: string; decimals: number; symbol?: string }
transactionId?: string
payer?: string
amount?: string
Expand All @@ -43,6 +43,17 @@ interface PaymentResult {

Adds `tokenId: string` and optional `decimals?: number` (auto-fetched when omitted).

## waitForHbarPayment / waitForHtsPayment
## verifyUsdcPayment(params)

Wrapper over `verifyHtsPayment` that resolves the verified USDC token id for `network` (**required**;
mainnet `0.0.456858`, testnet `0.0.429274`, previewnet throws `UnsupportedAssetError`) and forces
`decimals = 6`. Accepts an optional `tokenId` override (dev/mock tokens, still 6 decimals). The
result `asset` is tagged `{ tokenId, decimals: 6, symbol: "USDC" }`. See
[Verify a USDC payment](/guide/verify-usdc-payment).

Also exported: `getUsdcTokenId(network)`, `USDC_TOKEN_IDS`, `USDC_DECIMALS` (= 6), and
`isUsdcPaymentResult(result)`.

## waitForHbarPayment / waitForHtsPayment / waitForUsdcPayment

Adds `timeoutMs?`, `pollIntervalMs?`, `signal?`. Resolves `confirmed` or `expired`.
8 changes: 8 additions & 0 deletions examples/node-payment-verifier/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# Used by verify:hbar and verify:hts (network is hardcoded to testnet in those scripts)
HBARKIT_RECEIVER=0.0.12345
HBARKIT_AMOUNT=25
HBARKIT_MEMO=order_6471727153206
HBARKIT_TOKEN=0.0.456858

# Used by verify:usdc
HEDERA_NETWORK=mainnet
RECEIVER=0.0.12345
AMOUNT=25.00
MEMO=invoice_123
# USDC_TOKEN_ID=0.0.429274 # optional override for a dev/testnet mock token
12 changes: 10 additions & 2 deletions examples/node-payment-verifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ pnpm --filter @hbar-kit/example-node-payment-verifier verify:hbar

# Verify an HTS token payment
pnpm --filter @hbar-kit/example-node-payment-verifier verify:hts

# Verify a USDC payment
pnpm --filter @hbar-kit/example-node-payment-verifier verify:usdc
```

Each script prints `PAID` / `NOT PAID` plus the full `PaymentResult` (status, amount, payer), and —
when matched — a HashScan explorer deep-link.
`verify:hbar` / `verify:hts` print `PAID` / `NOT PAID` plus the full `PaymentResult` (status,
amount, payer), and — when matched — a HashScan explorer deep-link.

`verify:usdc` reads `HEDERA_NETWORK`, `RECEIVER`, `AMOUNT`, `MEMO`, and an optional `USDC_TOKEN_ID`
override (for a dev/testnet mock token), then prints `PAID` / `NOT PAID`, the status, transaction id,
payer, amount, and a HashScan link when matched. With no `USDC_TOKEN_ID` it uses the verified
canonical USDC token id for the network.

## Key idea: server-side verification

Expand Down
3 changes: 2 additions & 1 deletion examples/node-payment-verifier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"verify:hbar": "tsx src/verify-hbar.ts",
"verify:hts": "tsx src/verify-hts.ts"
"verify:hts": "tsx src/verify-hts.ts",
"verify:usdc": "tsx src/verify-usdc.ts"
},
"dependencies": {
"@hbar-kit/payments": "workspace:*"
Expand Down
23 changes: 23 additions & 0 deletions examples/node-payment-verifier/src/verify-usdc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { verifyUsdcPayment } from "@hbar-kit/payments"

const network = (process.env.HEDERA_NETWORK ?? "mainnet") as "mainnet" | "testnet" | "previewnet"
const receiver = process.env.RECEIVER ?? "0.0.12345"
const amount = process.env.AMOUNT ?? "25.00"
const memo = process.env.MEMO
const tokenId = process.env.USDC_TOKEN_ID // optional override (dev/testnet mock token)

const result = await verifyUsdcPayment({
network,
receiver,
amount,
memo,
tokenId, // undefined → uses the verified canonical USDC token id for `network`
after: new Date(Date.now() - 30 * 60 * 1000),
})

console.log(result.matched ? "PAID" : "NOT PAID")
console.log("status: ", result.status)
console.log("transaction id:", result.transactionId ?? "—")
console.log("payer: ", result.payer ?? "—")
console.log("amount: ", result.amount ? `${result.amount} USDC` : "—")
if (result.matched) console.log("hashscan: ", result.explorerUrl)
15 changes: 13 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,23 @@ Business-level verification on top of `@hbar-kit/mirror`. Exports:
- `verifyHbarPayment(params): Promise<PaymentResult>` — verify an HBAR payment.
- `verifyHtsPayment(params): Promise<PaymentResult>` — verify an HTS token payment (auto-resolves
token decimals from the Mirror Node if `decimals` is omitted, and caches them).
- `verifyUsdcPayment(params): Promise<PaymentResult>` — verify a USDC payment. Convenience wrapper
over `verifyHtsPayment` that resolves the verified USDC token id for `network` (required;
mainnet `0.0.456858`, testnet `0.0.429274`, previewnet throws) and forces `decimals = 6`. Accepts
an optional `tokenId` override for dev/mock tokens; tags the result `asset` as
`{ tokenId, decimals: 6, symbol: "USDC" }`. USDC is verified through the Mirror Node as an HTS
token — no custody, no Circle API, no fiat conversion.
- `waitForHbarPayment(params & WaitOptions): Promise<PaymentResult>` — poll until the payment is
confirmed or the timeout elapses.
- `waitForHtsPayment(params & WaitOptions): Promise<PaymentResult>` — same, for HTS.
- `waitForUsdcPayment(params & WaitOptions): Promise<PaymentResult>` — same, for USDC.
- `getUsdcTokenId(network)`, `USDC_TOKEN_IDS`, `USDC_DECIMALS` (= 6), `isUsdcPaymentResult(result)`
— USDC registry/helpers.
- `hashscanTxUrl(network, consensusTimestamp, transactionId): string` — build a HashScan explorer
link.
- Lower-level helpers: `netToReceiver`, `memoMatches`, `classifyAmount`.
- Types: `PaymentResult`, `PaymentStatus`, `PaymentMatch`, `PaymentAsset`, `MemoComparison`,
`VerifyHbarParams`, `VerifyHtsParams`, `VerifyBaseParams`, `WaitOptions`.
`VerifyHbarParams`, `VerifyHtsParams`, `VerifyUsdcParams`, `VerifyBaseParams`, `WaitOptions`.

`verify*` params (`VerifyBaseParams`):

Expand All @@ -57,6 +66,8 @@ Business-level verification on top of `@hbar-kit/mirror`. Exports:
- `after?: Date | string`, `before?: Date | string` — restrict the time window.
- `client?: MirrorClient` — reuse a pre-built Mirror client.
- HTS only (`VerifyHtsParams`): `tokenId: string`, `decimals?: number`.
- USDC only (`VerifyUsdcParams`): `network` required; `tokenId?` optional override (decimals are
always 6).

`WaitOptions`: `timeoutMs?` (default 10 min), `pollIntervalMs?` (default 3000 ms), `signal?`
(AbortSignal).
Expand Down Expand Up @@ -89,7 +100,7 @@ interface PaymentResult {
matched: boolean // true ONLY when the expected payment is satisfied by exactly one tx
status: PaymentStatus
receiver: string
asset: "HBAR" | { tokenId: string; decimals: number }
asset: "HBAR" | { tokenId: string; decimals: number; symbol?: string } // symbol:"USDC" via verifyUsdcPayment
transactionId?: string
payer?: string
amount?: string // human-readable, e.g. "25"
Expand Down
7 changes: 5 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ reports whether the expected payment arrived.
## Packages

- `@hbar-kit/payments` — payment verification. Functions: `verifyHbarPayment`, `verifyHtsPayment`,
`waitForHbarPayment`, `waitForHtsPayment`. Returns a typed `PaymentResult` (discriminated by
`status`).
`verifyUsdcPayment`, `waitForHbarPayment`, `waitForHtsPayment`, `waitForUsdcPayment`. Returns a
typed `PaymentResult` (discriminated by `status`). USDC on Hedera is verified through the Mirror
Node as an HTS token (6 decimals); `verifyUsdcPayment` is a convenience wrapper that fills in the
verified USDC token id — no custody, no Circle API, no fiat conversion.
- `@hbar-kit/mirror` — typed Hedera Mirror Node REST client: `createMirrorClient` (transactions,
tokens, balances) with cursor pagination and a pluggable transport.
- `@hbar-kit/core` — zero-dependency Hedera primitives: network config (`NETWORKS`,
Expand Down Expand Up @@ -56,6 +58,7 @@ if (result.matched) {
- verify an HBAR payment in TypeScript
- verify a Hedera payment by memo / order id
- verify an HTS token payment
- verify a USDC payment on Hedera (HTS token, 6 decimals)
- accept HBAR payments without custody
- build a Hedera payment link, invoice, or checkout flow (incl. Next.js)
- backend / server-side Hedera payment verification API
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,10 @@ export class PaymentVerificationError extends HbarKitError {
this.name = "PaymentVerificationError"
}
}
/** A requested asset is not available on the selected network (e.g. USDC on previewnet). */
export class UnsupportedAssetError extends HbarKitError {
constructor(message: string, opts?: HbarKitErrorOptions) {
super(message, opts)
this.name = "UnsupportedAssetError"
}
}
Loading
Loading