Skip to content

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.

An agent's virtual Visa card in the app

The client ​

All Reap traffic goes through ReapClient (src/reap/client.ts) — a small, framework-free REST client. The contract it enforces:

ConcernHow ReapClient handles it
API versionPinned header Reap-Version: 2025-02-14 on every request
AuthStatic Authorization: Bearer <REAP_API_KEY> — no token mint step
IdempotencyAuto-attaches an Idempotency-Key to money-creating POSTs (account/card/posting); omitting it returns 422
Timeouts15s per request budget (the real-time authorization path is separate and tighter)
ErrorsNon-2xx throws a typed ReapError(status, message, code, body)
ResponsesReturns 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:

OperationNotes
createCardVIRTUAL or PHYSICAL, with a 3DS challenge method
freeze / unfreezeReversible pause — the kill-switch and admin freeze route through this
blockTerminal block
revealPanReturns the full PAN as an encrypted blob
revealCardHosted iframe reveal (no plaintext touches us)
3DSget / 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 swipeWe do — Reap calls our authorization endpoint liveReap does — internally, against the user's balance
CollateralOur treasury holds pooled collateralReap holds each user's balance
Balance of recordOur ledger_agents tableReap (we sync)
Our endpointPOST /providers/reap/authorize must answer in real timeNot needed
FlagREAP_USER_FUNDED = offREAP_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:

EventAction
CARD_3DS_CHALLENGE_CREATEDAuto-approve when it matches a held reservation
CARD_TRANSACTION_CREATED / _UPDATEDPer-event: CLEARING → settleClearing, REVERSAL → reverse, REFUND → refund; appends to the transactions ledger. User-Funded forces syncReapBalance.
USER_APPLICATION_STATUS_UPDATEDManaged-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:

SecretEndpoint
REAP_AUTH_SECRETAUTHORIZATION endpoint (real-time approve/decline)
REAP_WEBHOOK_SECRETNOTIFICATION 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)
          → openAccount

The 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 chainReap familyFundable under User-Funded?
SolanaSOLANAYes
BaseBASEYes
PolygonPOLYGONYes
EthereumETHEREUMYes
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 ​

VariablePurpose
REAP_API_KEYBearer key; reapConfigured is true when set
REAP_API_URLBase URL; defaults to the Singapore sandbox. reapSandbox is derived by testing the URL for /sandbox/i and gates the X-Simulate-Checkout behavior
REAP_VERSIONAPI version header (2025-02-14)
REAP_AUTH_SECRETSigning secret for the authorization endpoint
REAP_WEBHOOK_SECRETSigning secret for the notification endpoint
REAP_REVEAL_PRIVATE_KEYRSA private key (PEM or base64 PEM) for PAN decryption; revealKeyConfigured gates reveal
REAP_USER_FUNDEDSwitches to User-Funded + Managed auth; effective only with a real key (reapUserFunded)

be everywhere — live here.