Appearance
Reap integration
Every pocket agent card is a real Visa card issued through Reap (Reap Global). Reap is the regulated card-issuing and crypto-custody layer underneath the product: it onboards users, holds balances, issues virtual and physical cards, decides whether a swipe is approved, and tells us when a transaction clears. Our job is to wire that cleanly into the app's ledger, the agent runtime, and the money-safety invariants — without ever losing track of a cent.
This page is the map of that integration: the client, card issuing, the two authorization models, how settlement webhooks are reconciled exactly once, and how KYC runs through SumSub.

The client
All Reap traffic goes through ReapClient (src/reap/client.ts) — a small, framework-free REST client. The contract it enforces:
| Concern | How ReapClient handles it |
|---|---|
| API version | Pinned header Reap-Version: 2025-02-14 on every request |
| Auth | Static Authorization: Bearer <REAP_API_KEY> — no token mint step |
| Idempotency | Auto-attaches an Idempotency-Key to money-creating POSTs (account/card/posting); omitting it returns 422 |
| Timeouts | 15s per request budget (the real-time authorization path is separate and tighter) |
| Errors | Non-2xx throws a typed ReapError(status, message, code, body) |
| Responses | Returns the already-unwrapped JSON body |
The surface is broad: Users/KYC, Accounts, crypto funding (deposit addresses + signed withdrawals), Cards, 3DS challenges, Transactions/activities, Webhook endpoint management, Policies, Agentic Commerce (search → quote → checkout → enrollment/mandate), and a sandbox-only Simulation API. The sections below cover the parts that carry money.
Card issuing
A card is created under a user's Reap user + account (provisioning is covered in KYC below). The client supports:
| Operation | Notes |
|---|---|
createCard | VIRTUAL or PHYSICAL, with a 3DS challenge method |
freeze / unfreeze | Reversible pause — the kill-switch and admin freeze route through this |
block | Terminal block |
revealPan | Returns the full PAN as an encrypted blob |
revealCard | Hosted iframe reveal (no plaintext touches us) |
| 3DS | get / respond to challenges |
PAN reveal
When an agent needs the real card number for a checkout, Reap returns it as an RSA-OAEP-SHA1 encrypted blob, which src/reap/reveal.ts decrypts with the registered private key (REAP_REVEAL_PRIVATE_KEY, PEM or base64 PEM). The plaintext is handed to the agent for one checkout and is never stored or logged. revealKeyConfigured gates the path; offline it falls back to a mock PAN. See the Cards page for the agent-facing side of this.
Authorization: Managed vs External
When a card is swiped, someone has to decide in real time whether to approve or decline. Reap supports two models, and which one is in play depends on the card-custody mode (see the non-custodial model, Track B).
| External authorization (Program-Funded, default) | Managed authorization (User-Funded) | |
|---|---|---|
| Who decides a swipe | We do — Reap calls our authorization endpoint live | Reap does — internally, against the user's balance |
| Collateral | Our treasury holds pooled collateral | Reap holds each user's balance |
| Balance of record | Our ledger_agents table | Reap (we sync) |
| Our endpoint | POST /providers/reap/authorize must answer in real time | Not needed |
| Flag | REAP_USER_FUNDED = off | REAP_USER_FUNDED = on |
External authorization (the live path today)
In External mode, Reap sends a CARD_AUTHORIZATION_REQUEST to our endpoint and we must answer APPROVE or DECLINE against our own ledger, in real time. The decision runs through decideAuthorization → ledger.matchAuthorization, and it is fail-closed: anything unmatched declines.
The matcher is strict. An agent's card_pay tool places a policy hold (a reservation) before the swipe; the live authorization then has to match that reservation:
matchAuthorization takes a row-level lock (SELECT … FOR UPDATE) on the agent's ledger row and then:
- expires stale holds first;
- matches a reservation by currency + merchant (substring) + amount within a ±15% (or $1 floor) tolerance;
- declines if zero candidates, more than one candidate (ambiguous), or if the balance — excluding the amount reserved by other holds — is insufficient;
- on a clean match, approves and consumes the reservation at the actual cleared amount.
A frozen card (the kill-switch / admin freeze) declines immediately with TRANSACTION_NOT_ALLOWED.
Managed authorization (User-Funded)
In Managed mode we are out of the live decision entirely. Reap holds the user's balance at per-chain deposit addresses and authorizes + settles the swipe internally; there is no external-auth webhook. We learn about the transaction through the notification webhook and syncReapBalance to keep our view current. This is the mode Track B switches on, and it is gated on Reap provisioning the User-Funded + Managed-Auth project.
Clearing & settlement webhooks
Authorization is only the first half. The money actually settles later, asynchronously, via the notification webhook at POST /webhooks/reap. These events are the statement of record, so they are processed exactly once — even across Redis-dedup outages, cross-replica redelivery, or Reap re-sending.
The events the handler acts on:
| Event | Action |
|---|---|
CARD_3DS_CHALLENGE_CREATED | Auto-approve when it matches a held reservation |
CARD_TRANSACTION_CREATED / _UPDATED | Per-event: CLEARING → settleClearing, REVERSAL → reverse, REFUND → refund; appends to the transactions ledger. User-Funded forces syncReapBalance. |
USER_APPLICATION_STATUS_UPDATED | Managed-KYC completion (gated on reapUserFunded): maps externalId → app user, flips kycStatus, never downgrades an already-APPROVED user off a stale IN_REVIEW |
The real exactly-once guarantee is not the Redis fast-path dedup — that's just an optimization. It's the processed_events row inserted ON CONFLICT DO NOTHING in the same transaction as the balance mutation. If the row is new, we settle; if it conflicts, the whole transaction rolls back and nothing double-settles.
Signature verification (two secrets)
The two webhook endpoints have two distinct signing secrets — never mix them:
| Secret | Endpoint |
|---|---|
REAP_AUTH_SECRET | AUTHORIZATION endpoint (real-time approve/decline) |
REAP_WEBHOOK_SECRET | NOTIFICATION endpoint (async events) |
verifyReapWebhook (src/reap/webhook.ts) implements a Stripe-style scheme:
text
X-Reap-Webhook-Signature: t=<unixSeconds>,v1=<hexHmacSha256>
signed payload = `${t}.${rawBody}`It verifies an HMAC-SHA256 over the exact raw request bytes (never a re-serialized JSON string), rejects timestamps outside a 300-second tolerance (replay guard), and compares with timingSafeEqual. Only after the signature passes is the body parsed as JSON.
KYC via SumSub
Before a user can hold a card, Reap must run them through KYC. The integration (src/provider/provisioning.ts) has two paths depending on the environment, both driven through SumSub (Reap's Managed-KYC provider).
Sandbox: the simulate recipe
In sandbox there is no real identity check. createReapUserWithKyc drives the application to approval synchronously:
text
createUser → advanceApplication(MANAGED_KYC)
→ simulate(IN_REVIEW) → poll
→ simulate(APPROVED) → poll (up to 4 rounds)
→ openAccountThe whole thing is idempotent — it reuses an existing Reap user/account for the same externalId, so a restart or retry doesn't create duplicates.
Production: real SumSub
startManagedKyc kicks off the real flow: createUser → advanceApplication(MANAGED_KYC) returns a SumSub sdkToken. The client opens the SumSub WebSDK — the backend serves a self-contained bridge page at /kyc/verify that loads static.sumsub.com's sns-websdk-builder.js under a scoped CSP. Approval arrives asynchronously via the USER_APPLICATION_STATUS_UPDATED webhook; the flow does not block waiting for it.
The orchestration routes: POST /api/kyc drives it; /api/reap/account/signer-message + /api/reap/account do the User-Funded client-signed account handshake; /api/reap/deposit returns the per-chain deposit addresses.
Known gap: the PII is a stub
The production SumSub path is built but not yet production-ready. createUser currently hardcodes firstName: 'Pocket', lastName: 'Agent', and a random generated phone number (looksValidE164 rejects 555 numbers, so it synthesizes a +1408… number). There is no real name, DOB, 18+ gate, address, or ID capture, and the SumSub path has not been run against production Reap. Collecting and forwarding real applicant PII is required before any real-money KYC — this is tracked as a launch blocker.
Chain mapping
Under User-Funded, funding is a plain stablecoin transfer to a Reap-provisioned deposit address, which exists only on the chains Reap supports. src/reap/chains.ts maps app chain names to Reap's chain families:
| App chain | Reap family | Fundable under User-Funded? |
|---|---|---|
| Solana | SOLANA | Yes |
| Base | BASE | Yes |
| Polygon | POLYGON | Yes |
| Ethereum | ETHEREUM | Yes |
| BNB / BSC | — | No — not in Reap's chain set |
REAP_FUND_CHAINS orders these cheapest-gas-first (['Solana','Base','Polygon','Ethereum']). Matching is by family prefix, so the same app chain ('Base') resolves whichever network the Reap project runs on — BASE_SEPOLIA in sandbox, BASE_MAINNET in production — without hardcoding testnet vs mainnet. The exact chainId, deposit address, and accepted-asset IDs are always read back from the account's chainAddresses[], never guessed.
BNB is the one gotcha
Because Reap's chain set has no BNB/BSC, a top-up on BNB has no User-Funded deposit address — depositAddressFor() returns undefined and the fund flow skips it. BNB is still a supported chain elsewhere in the app (balances, gasless sends, 4337), just not as a Reap deposit chain.
Agentic Commerce
Reap also powers the agent's catalog shopping — the "search → buy" path an agent uses instead of browser checkout. The client exposes searchProducts, getProductDetails, resolveVariant, createQuote, selectShippingOption, createCheckout, and the enrollment/mandate lifecycle (createEnrollment / revoke, mandate pause / resume / cancel). In sandbox, createCheckout carries X-Simulate-Checkout: COMPLETED so an order can complete without a real merchant. Enrollment source is REAP_CARD for an agent's own card or EXTERNAL for a user's own linked card. The agent-facing behavior is covered on the Agents page.
Configuration
| Variable | Purpose |
|---|---|
REAP_API_KEY | Bearer key; reapConfigured is true when set |
REAP_API_URL | Base URL; defaults to the Singapore sandbox. reapSandbox is derived by testing the URL for /sandbox/i and gates the X-Simulate-Checkout behavior |
REAP_VERSION | API version header (2025-02-14) |
REAP_AUTH_SECRET | Signing secret for the authorization endpoint |
REAP_WEBHOOK_SECRET | Signing secret for the notification endpoint |
REAP_REVEAL_PRIVATE_KEY | RSA private key (PEM or base64 PEM) for PAN decryption; revealKeyConfigured gates reveal |
REAP_USER_FUNDED | Switches to User-Funded + Managed auth; effective only with a real key (reapUserFunded) |
Related reading
- The no-license custody model — how Reap User-Funded (Track B) fits the non-custodial plan.
- Cards — the card experience from the user's and agent's side.
- Security & control — the approval gate that wraps every card purchase.