# Merchant API Example

This Node 22 / TypeScript example uses `PayArriveServerClient` and a durable
local event consumer. The default `demo.ts` transport is synthetic: it sends no
HTTP, RPC, payments or signing requests and does not authenticate a real customer.
Every default user, session, event and address is a fixture. Never send funds to
the displayed source-code addresses. The separate `live.ts` entry requires an
explicit `--live` flag and calls the five real merchant API operations.

The example's ledger writes are to schema `merchant_demo` in a local PostgreSQL
database literally named `payarrive_merchant_demo`. The URL must explicitly include
its dedicated username, password and port. The connection guard rejects other
database names, non-loopback hosts and URL query overrides. The process refuses
nonempty `PG*` environment settings and `NODE_PG_FORCE_NATIVE`; connection settings
come from `MERCHANT_DEMO_DATABASE_URL`. Default synthetic mode reads no PayArrive
credential environment variables. Live mode also creates or recovers a session
through the configured API, which can persist records in that API's own database.

The console gives each Project a public Project ID and one or more Project Server
Keys (`pa_sk_...`). The ID is metadata only; the Server Key is the backend Bearer
credential and must stay in the merchant server or an authorized MCP host. This
example never accepts a key in source code or model arguments, and it does not use
the platform's internal Privy App Secret.

## Standalone Download

The public documentation exposes seven raw files:

- [README.md](https://payarrive.com/docs/example/README.md)
- [merchant.ts](https://payarrive.com/docs/example/merchant.ts)
- [demo.ts](https://payarrive.com/docs/example/demo.ts)
- [live.ts](https://payarrive.com/docs/example/live.ts)
- [headless.ts](https://payarrive.com/docs/example/headless.ts)
- [tsconfig.json](https://payarrive.com/docs/example/tsconfig.json)
- [package.json](https://payarrive.com/docs/example/package.json)

Place these seven files in one directory. The documentation build derives the SDK
file and local imports from repository sources; the standalone package needs no
private repository checkout.

```sh
npm install
npm run build
export MERCHANT_DEMO_DATABASE_URL='postgresql://demo:demo@127.0.0.1:5432/payarrive_merchant_demo'
npm start
```

This uses the same isolated local database prerequisites below. The downloaded
package does not include the repository's test suite.

## Synthetic Mode

Use Node `22.22.3` and the repository's installed dependencies. Create an empty
local database named `payarrive_merchant_demo`, then run:

```sh
export MERCHANT_DEMO_DATABASE_URL='postgresql://demo:demo@127.0.0.1:5432/payarrive_merchant_demo'
npx tsc -p examples/merchant-node/tsconfig.json
node .example-dist/examples/merchant-node/demo.js
```

The URL is an example with synthetic local credentials. Use your dedicated local
demo account. Running twice keeps one credit, two inbox events, cursor `2` and
`balance_micro_usdt: "1000000"`. The balance is a synthetic USDT counter, not a
customer's currency, exchange rate or production balance adapter.

## Real API Mode

Use an authorized development API, its existing Server Key, and a trusted test
operator's existing merchant user reference. This mode requires service activation
and the same network admission as direct REST. A valid Key alone is insufficient.
It does not authenticate a C-end customer or add a production credit integration.

Set these environment variables outside source code and model tool arguments:

| Variable | Meaning |
|---|---|
| `MERCHANT_DEMO_DATABASE_URL` | The same isolated local database described above |
| `PAYARRIVE_BASE_URL` | Trusted HTTPS API origin; loopback HTTP is allowed for local integration |
| `PAYARRIVE_SERVER_KEY` | Existing authorized test merchant Server Key; never print it |
| `MERCHANT_PROJECT_REF` | Expected Project ID copied from the console; must match the Key's integration context before any session or local ledger write |
| `MERCHANT_EXTERNAL_USER_REF` | Stable existing test user selected by an authorized merchant operator |
| `MERCHANT_IDEMPOTENCY_KEY` | Original persisted logical request key; reuse after an unknown result |

With all six variables configured, the downloaded example runs with:

```sh
npm run build
npm run live
```

From the repository, use `npm run example:merchant:live`. Both scripts already
include the CLI's required `--live` flag; do not append it again. Direct execution
without that flag does not start real API calls. Live mode first verifies the Key's
Project through integration context, then uses SDK session creation,
server session lookup, deposit history and event polling, then the existing durable
example consumer. API reads do not themselves advance the local cursor or credit.

Each run reads at most ten event pages before processing pending credits. If
`caught_up` is `false`, the collected pages remain durable and no pending credit
is processed in that run; rerun with the same configuration and request identity
to continue. The local ledger is partitioned by the verified remote Project ID;
a mismatched Key and `MERCHANT_PROJECT_REF` fails before any session or ledger write.

The local balance remains an illustrative USDT counter even when events come from
a real API. No RPC, signing or balance-service call is part of this example.
Do not run it as a second credit consumer alongside CloudAIKey's existing Sidecar.
Do not log complete create responses: `client_token` and the token fragment in
`checkout_url` are short-lived capabilities. Server Keys never belong in output.

The SDK defaults to a ten-second request timeout, rejects redirects and never
retries automatically. `PayArriveApiError` exposes a safe `code`, HTTP `status`
(`null` when no response was received), `retryable` and optional `retryAfterMs`.
An unknown create result must keep the original user, amount and idempotency key.
The error's retry hint does not prove that the original request had no effect.

## Durable Boundaries

- `createSession` normalizes the user and idempotency key before persisting the
  original request and invoking the SDK. A lost response is retried with that
  same identity; changing the user for
  the stored key fails. No customer login or production credential flow is provided.
- `saveEventPage` validates the entire page, takes the Project cursor row lock,
  inserts its inbox records and advances the cursor in one transaction. A stale
  concurrent page returns `false`; its caller reloads the stored cursor. Events
  deduplicate by `(project_id, event_id)` and conflicting Deposit identities fail.
- `processPending` takes the same Project lock. Credit uniqueness is
  `(project_id, deposit_id)`, even when different event IDs refer to one Deposit.
  The credit record, exact synthetic balance change and processed marker commit
  together. Reconnect and read the original Deposit after an unknown commit;
  replay cannot create another positive credit.
- Already-received later `deposit.reverted` suppresses an earlier pending
  confirmation. A later restored confirmation can credit once if never credited.
  Reversal after accepted credit records reconciliation and preserves the credit
  and balance. `deposit.settled` never credits; `settlement.reverted` records
  reconciliation without undoing inbound credit. This only covers events already
  received; future corrections require the same reconciliation path.
- Amounts use `BigInt` and PostgreSQL exact numeric values. Each Deposit is
  independently floored to six decimals; its discarded atomic tail is retained.
  A zero quantum records no positive balance change.

## External Ledger Boundary

An external balance service cannot join this PostgreSQL transaction. This
example has no adapter for one. Production integration needs its own persistent
operation identity, original request and `unknown` state before the call, plus
lookup of the original result after a timeout. Never invent a new key or amount
to retry an unknown credit. A received reversal permits querying an existing
operation but must not trigger a new credit submission. If the external service
cannot prove the result, retain reconciliation work for operator resolution.

The public protocol and current activation rules belong to
[the merchant guide](https://payarrive.com/docs/merchant-guide.md) and
[merchant OpenAPI](https://payarrive.com/docs/merchant-openapi.yaml).
This example creates no new public API. These URLs describe the release artifact;
availability requires an actual HTTP check after installation.

For application integration, the same SDK and the five-operation MCP command are
also available in the [installable merchant package](https://payarrive.com/downloads/payarrive-merchant-0.1.0.tgz).
See the guide's [SDK quickstart](https://payarrive.com/docs/#sdk-quickstart) and
[Codex MCP configuration](https://payarrive.com/docs/#merchant-mcp). Public
[documentation MCP](https://payarrive.com/docs/#documentation-mcp) needs no Key
and remains separate from the authenticated business tools.

## Verification

```sh
npx vitest run test/merchant-example.test.ts
RUN_REAL_POSTGRES_TESTS=1 npx vitest run test/merchant-example-postgres.test.ts
```

The real PostgreSQL suite creates an isolated Testcontainers database and removes
only its own container. It covers rollback, lost commit responses, replay,
concurrency, restart, corrections and precision. Skipping that suite is not
evidence that database recovery works. No Docker requirement applies to parsing
and transport tests.
