SKILL.md
Open Money Stack (OMS)
The Open Money Stack is Polygon's platform for moving money between crypto and fiat. Its core is the OMS API: a REST interface that handles KYC'd customers, custodial wallets, pricing quotes, transactions, and cash-in deposits behind one bearer token. A neobank, a remittance app, an on-ramp, or a payments product is mostly a particular arrangement of the same handful of endpoints.
This skill exists to take a developer from "I want to build X" to a concrete, buildable plan. Do not dump the whole API on them. Instead: get them authenticated, ask a few questions about the product they have in mind, then hand back an implementation plan written against endpoints that actually work today.
The workflow
Follow these four steps in order. They mirror how a real integration starts.
- Orient. If the developer named a product ("build me a neobank"), map it
to an archetype below. If they were vague ("I want to move money on Polygon"), ask what they are building before anything else.
- Authenticate and onboard. Get them holding a bearer token (Step 1). Every
plan depends on this, so it comes first.
- Ask the discovery questions (Step 2). Five dimensions decide which
endpoints the plan uses and which KYC endorsements are required. Batch them into one message and skip any the developer already answered in their prompt.
- Produce the implementation plan (Step 3) using the template, grounded in
the API spine and the live-vs-early-access split.
The integrity rule that governs every plan: only present endpoints that are callable today as buildable steps. Anything in the early-access column is documented but not yet live in v0.12, so mark it plainly. A plan that promises a developer an endpoint that does not exist yet costs them a sprint and costs us their trust.
Step 1: Authenticate and onboard
A developer needs an OMS API key and secret before any call works. They request access and generate credentials from the dashboard; point them to /payments/get-started for the request-access flow.
With credentials in hand, exchange them for a bearer token. This is the only endpoint that takes no token.
curl -X POST https://sandbox-api.polygon.technology/v0.12/auth/token \
-H "Content-Type: application/json" \
-d '{"apiKey": "sk_sdbx_...", "apiSecret": "..."}'
{
"accessToken": "eyJhbGciOiJFZERTQSIs...",
"tokenType": "bearer",
"expiresIn": 86399,
"expiresAt": "2026-06-09T15:15:22Z"
}
The token is a JWT. Read expiresIn (seconds) and expiresAt from the response and re-exchange when it is close to expiring; do not hardcode a lifetime. Sandbox tokens are currently long-lived (observed around 24 hours), so let the response drive refresh logic rather than assuming a fixed window. Present the token as Authorization: Bearer <accessToken> on every other endpoint. Start in sandbox (sandbox-api); switch the host to api.polygon.technology for production once the flow works end to end.
Reference: /api-reference/auth/authorize.
Step 2: Discovery questions
Ask these before writing a plan. Each one changes the endpoints and the required KYC endorsements, so the answers are what make the plan specific rather than generic. Explain briefly why you are asking, and offer sensible defaults.
| Dimension | What to ask | Why it matters |
|---|---|---|
| Jurisdiction and currency | Which countries and currencies are your users in? | Drives which KYC endorsements are required (usd today) and which rails are available. |
| Custody | Should Polygon custody user funds, or should users hold their own keys? Ask this for every product, not just neobanks. | This is a genuine fork available for any app you build, so ask it explicitly rather than assuming. Custodial -> OMS wallets (POST /customers/{id}/wallets); the OMS holds funds and the user never touches keys. Non-custodial -> Polygon embedded wallets; users self-custody with email or social sign-in and no seed phrase, branded as your app. The two can coexist in one product. See the Custody section. |
| Funding (cash-in) | How do users add money: debit card, in-person cash, bank transfer, or incoming crypto? | Card, cash-in, and bank transfer are all live. For bank transfers, create a virtual account (POST /virtual-accounts); incoming ACH, wire, or SWIFT auto-converts to the configured destination. |
| Payout (cash-out) | How does money leave: crypto send, bank payout, or cash pickup? | Crypto send is live. Bank payout is live too: register the bank with POST /external-accounts, then quote to it by ext_ ID, or stand up a deposit address that routes incoming crypto straight to it. Cash pickup is early access. |
| Cadence | One-time transfers, or recurring deposits and balances? | Recurring inbound fiat (virtual accounts) and standing crypto-to-bank routes (deposit addresses) are live, alongside one-time card and cash-in funding. Deposit addresses must be enabled for your project. |
| Design and branding | Do you have brand guidelines or a design system (logo, colors, type, components)? | If yes, bring them so the UI is yours; the embedded wallet onboarding can carry your branding. If not, start from the example design system below and restyle it. |
If the developer's answers point mostly at early-access rails, say so directly and offer the live path that gets them closest (usually card or cash-in funding plus crypto send), so they can ship something real now and layer the rest later.
Step 3: The implementation plan
Produce the plan in this shape every time. It is the skill's main output. Keep it concrete: name the endpoint, the ID it returns, and the ID it consumes from the previous step, so the developer can see the chain.
# <Product> on Polygon OMS
## Summary
One paragraph: what the product does and which OMS primitives carry it.
## Custody
Custodial (OMS wallets) or non-custodial (Polygon embedded wallets), per the answers.
## Design
Bring-your-own brand, or start from the example design system (example-design.md), restyled.
For a mobile-first app (a neobank or most consumer money apps), render app screens inside an iPhone device frame by default; build web surfaces such as a dashboard as a standard browser layout.
## Required KYC endorsements
basic, cryptoCustody, usd (or the subset the flow needs).
## Build steps (live today)
1. <Endpoint> -> returns <id>. Consumes <id from prior step>. One line on purpose.
2. ...
Include the auth -> customer -> wallet -> fund -> move spine, trimmed to this product.
## Webhooks to handle
transaction.<type>.processing | completed | failed, plus cash_in.* if used.
## Early access (not callable yet, design for it)
List the steps that need rails still in early access, and the live fallback.
## Considerations
Idempotency-Key on every POST, sandbox testing, rate handling, balance reads.
Sign-in recovers identity; it never re-onboards. On every sign-in, resolve the existing wallet and customer before offering KYC. The embedded wallet is deterministic per identity (the same email or social account returns the same address), so the wallet recovers on its own; the OMS customer must be recovered to match: look it up by email/identity (GET /customers) and show the KYC form only when none exists. Treat the OMS customer and the wallet as the source of truth for identity, not local or session state. Signing out must not lose the account, and restoring must read the durable source first, then any local cache. If you cache account state locally, persist only after the recovery lookup has run: a cache write that fires before the lookup overwrites the saved record with empty post-sign-out state, the exact bug that forces re-onboarding.
Verify the full account lifecycle, not just first-run: sign up, sign out, sign back in with the same identity, and confirm the same wallet, the same customer, and the same balance, with no KYC re-prompt. Put this round-trip next to the funding and send flows in the plan's verification.
The OMS API spine
Almost every money-movement product is a trimmed version of this sequence. The IDs chain forward: customerId -> walletId -> quoteId -> transactionId. All examples assume Authorization: Bearer <token> and a base URL of https://sandbox-api.polygon.technology/v0.12. Put an Idempotency-Key header on every POST so retries never double-spend. Confirm exact field names against /api-reference before coding, since the spec is the source of truth.
1. Create a customer and run KYC. Only type: individual is supported today. Send a full identity on create: firstName, lastName, email, phone, birthDate, a structured residentialAddress, and identifyingInformation (for US users, an SSN). These fields are what enroll the customer with the underlying provider, and the provider only provisions a wallet and a balance once they are present. A name-only customer is accepted but silently left unenrolled, so its wallet provisioning and GET /balance calls fail later with provider account not provisioned for this customer. That silent gap is the most common sprint-costing trap, so collect the full identity in the onboarding form from the start.
Do not send an endorsements field. The provider assigns the default set (cryptoCustody and usd, which auto-includes basic). Endorsements track compliance status through INACTIVE -> PENDING -> ACTIVE, with ISSUES, REJECTED, REVOKED_ISSUES, and OFFBOARDED as the other states.
curl -X POST .../customers \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"individual","firstName":"Ada","lastName":"Lovelace",
"email":"[email protected]","phone":"+12125551234","birthDate":"1990-05-15",
"residentialAddress":{"line1":"123 Main St","city":"New York","state":"NY","country":"US","zipCode":"10001"},
"identifyingInformation":[{"type":"ssn","issuingCountry":"US","number":"123456789"}]}'
# -> { "id": "cst_...", "endorsements": [{ "name": "usd", "status": "PENDING" }, ...] }
Poll GET /customers/{customerId} (or handle the customer webhook) until the required endorsements reach ACTIVE before transacting. Note: the current sandbox is an alpha and may reject residentialAddress, identifyingInformation, or endorsements with ... is not yet supported. Confirm field support against /api-reference before building the onboarding form, because without the identity fields the customer cannot be enrolled and never gets a usable wallet.
2. Provision a custodial wallet. Wallet creation takes chain (not network); quotes and transactions take network for the same concept. Mixing up the two field names is a common error.
curl -X POST .../customers/cst_.../wallets \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-d '{"asset":"usdc","chain":"polygon"}'
# -> { "id": "wlt_...", "address": "0x..." }
3. Fund the wallet. Three live paths:
- Card (fiat to crypto): create a quote, then a transaction.
- Cash-in (in-person USD): create a deposit code the user takes to a retail
location (see the Cash services archetype).
- Bank transfer (virtual account): create a virtual account for the customer
(POST /virtual-accounts, destination walletCrypto); incoming ACH, wire, or SWIFT auto-converts and OMS auto-creates the transaction. See the deposit and payout resources section.
4. Move money. Quotes lock pricing; the transaction executes against the quote. The source is always an OMS wallet (walletId). The destination can be an external blockchainAddress, another OMS walletId (in-app peer transfer), or a saved external account. In the UI, let users enter a recipient as an email, a username, or a 0x address, then resolve it to one of those destination forms before quoting (email or username to the recipient's wallet; 0x to a blockchainAddress). Although USDC is the settlement asset, present balances and amounts to users as USD; USDC is the rail, not a user-facing label.
# Quote: dollars from a customer wallet to a recipient address (settled in USDC)
curl -X POST .../quotes \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"customerId":"cst_...",
"source":{"walletId":"wlt_...","asset":"usdc","network":"polygon"},
"destination":{"blockchainAddress":"0xRecipient","asset":"usdc","network":"polygon"},
"sponsorGas":true}'
# -> { "id": "qt_...", "exchangeRate": ..., "expiresAt": ... }
# Transaction: the quote is the contract
curl -X POST .../transactions \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-d '{"quoteId":"qt_..."}'
# -> { "id": "txn_...", "status": "processing" }
5. Track status. Status is webhook-driven. Register an endpoint with POST /webhooks and handle transaction.<type>.processing | completed | failed. Poll GET /transactions/{transactionId} as a fallback. Read balances with GET /customers/{customerId}/balance or GET /wallets/{walletId}/balance.
Deposit and payout resources (live in v0.12)
Four resources extend the spine with reusable funding and payout references. All four are callable today. Their list endpoints paginate with limit, startingAfter, and endingBefore and return { object, data, hasMore, nextCursor, previousCursor } envelopes.
External accounts (ext_...): saved off-platform payment destinations: US, IBAN, and Canadian bank accounts, cards, and external crypto wallets. Register with POST /external-accounts: an owner ({ "kind": "customer", "customerId": ... } or { "kind": "counterparty", "counterpartyId": ... }), a type (bankUs | bankIban | bankCanada | card | walletExternal), and exactly one per-type object named after the type; a mismatched or missing per-type object is rejected with 422. Cards register here too; there is no separate cards endpoint.
curl -X POST .../external-accounts \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-d '{"owner":{"kind":"customer","customerId":"cst_..."},
"type":"bankUs",
"bankUs":{"accountNumber":"123456789","routingNumber":"021000021","accountType":"checking"}}'
# -> { "id": "ext_bankUs_...", "status": "pending" } (flips to active or failed)
Per-type shapes: bankUs takes accountNumber and routingNumber plus optional accountType (checking | savings) and bankName; card takes cardNumber, cvv, expiryMonth and expiryYear (integers), and a required billingAddress, plus optional cardProvider and webSessionId; walletExternal takes blockchainAddress, networkFamily (evm | solana), and custodian. GET /external-accounts requires customerId (optional counterpartyId filter); PATCH changes only label and metadata; DELETE soft-deletes (204). Statuses: pending flips to active on provisioning or failed; rejected means create-time screening declined it; invalid means it became unusable later.
Counterparties (ctp_...): third parties that are not your customers but own external accounts you pay (vendors, payees, billers). Full CRUD on /counterparties. POST requires customerId and name; optional entityType (individual | business), email, phone, dateOfBirth, taxId, nationality, address, and metadata. The address shape is { streetAddress, city, postalCode, country } plus optional countryArea; it is not the customer address shape (no line1, state, or zipCode). GET /counterparties requires customerId. DELETE returns 409 while the counterparty still owns active external accounts.
Virtual accounts (va...): a dedicated US bank account number per customer; incoming ACH, wire, or SWIFT auto-converts, and OMS auto-creates the transaction. POST /virtual-accounts requires customerId, source ({ "asset": "usd", "network": "usBank" }), destination, accountHolder (must be "customer"), and type (must be "bankUs"); optional bankMemo, sponsorGas, label, and metadata. The destination is side-shaped: { "type": "walletCrypto", "details": ... } or { "type": "walletExternal", "details": { "id": "ext..." } } (a registered external account only; raw addresses are not accepted). bankDetails is null until the underlying deposit account is provisioned, so share it with the user only once it appears. PATCH can re-point destination and change sponsorGas, label, and metadata; re-pointing to a healthy external account recovers inactiveActionRequired back to active. DELETE returns 202 and closes the underlying account asynchronously: deletionRequestedAt is set and status finalizes to deleted.
Deposit addresses (da...): a reusable inbound crypto address that routes incoming stablecoins to a registered bank external account (cryptoToFiat). Must be enabled for your project, and the customer must be provisioned for them. POST /deposit-addresses requires customerId, expectedSourceAsset (usdc | usdt), expectedSourceNetwork, and destination ({ "type": "bankUs" | "bankIban" | "bankCanada", "details": { "id": "ext..." } }, a registered bank external account); optional sponsorGas, label, and metadata. depositInstructions is null in the create response until provisioning populates the OMS-owned inbound address. PATCH can re-point destination and change label and metadata; re-pointing to a healthy external account recovers inactiveActionRequired back to active. There is no delete.
Sandbox simulation covers both inbound flows: POST /virtual-accounts/{id}/simulate (choose the rail: achin, wirein, or swift_in) and POST /deposit-addresses/{id}/simulate.
What is live today vs early access
This split is the most important thing to get right. The API documents the full vision; only part of it is callable in v0.12.
| Capability | Status | Endpoints | |||
|---|---|---|---|---|---|
| Authentication | Live | POST /auth/token |
|||
| Customers and KYC | Live | POST/GET/PATCH/DELETE /customers, GET /customers/{id} |
|||
| Custodial wallets and balances | Live | POST/GET /customers/{id}/wallets, GET /wallets/{id}/balance, GET /customers/{id}/balance |
|||
| Quotes and transactions | Live | POST/GET /quotes, POST/GET /transactions, GET /transactions |
|||
| Card funding, pay-in (fiat to crypto) | Live | quote + transaction (tops up a wallet by debit card; this is not card issuance) | |||
| Crypto send and withdrawal to address | Live | quote (cryptoToCrypto) + transaction | |||
| Cash-in (in-person USD deposit) | Live | POST/GET /cash-ins, POST /cash-ins/{id}/refresh, GET /cash-locations |
|||
| Webhooks | Live | POST/GET/PATCH/DELETE /webhooks |
|||
| External accounts (saved banks, cards, external wallets) | Live | POST/GET /external-accounts, GET/PATCH/DELETE /external-accounts/{id} (PATCH is label/metadata only) |
|||
| Counterparties (third-party payees) | Live | POST/GET /counterparties, GET/PATCH/DELETE /counterparties/{id} (delete 409s while active external accounts remain) |
|||
| Virtual accounts (dedicated bank account, auto-convert) | Live | POST/GET /virtual-accounts, GET/PATCH/DELETE /virtual-accounts/{id} (delete returns 202; the account closes asynchronously) |
|||
| Deposit addresses (standing crypto-to-bank route) | Live | POST/GET /deposit-addresses, GET/PATCH /deposit-addresses/{id} (no delete); must be enabled for your project |
|||
| Bank payout (crypto to a registered bank account) | Live | register with POST /external-accounts, then quote + transaction to a bankUs / bankIban / bankCanada destination, or a standing deposit address |
|||
| Sandbox simulation | Live (non-prod) | POST /cash-ins/simulate/*, POST /deposit-addresses/{id}/simulate, POST /virtual-accounts/{id}/simulate |
|||
| Cash pickup (crypto to physical cash) | Early access | documented flow; contact us to enable | |||
| Card issuance (spendable cards for users) | Not available | no endpoint; this is distinct from card funding above. Do not build a Cards screen as a core surface; the example design's card visual is illustrative | |||
| In-app FX / exchange between currencies | Not available | no endpoint; a US/USD product has nothing to convert. Do not build an Exchange screen as a core surface | |||
| Bulk payouts and turnkey fiat-to-fiat remittance | Early access | compose from the live rails above; the managed products are early access, contact us | |||
Top-level POST /wallets with custodyType; embedded wallets |
Not yet callable | use nested POST /customers/{id}/wallets |
|||
POST /authorization |
Not yet callable | bearer token from POST /auth/token is the auth surface today |
|||
| Reference and config endpoints | Not yet callable | GET /assets, GET /networks, GET /reference/account-type-requirements, GET /project/configuration |
|||
*.statusChanged webhook events |
Not yet emitted | handle the v1 catalog: `transaction.<type>.processing \ | completed \ | failed \ | refund, cashIn., virtualAccount., depositAddress., externalAccount., endorsement., wallet.*` |
When a plan needs an early-access rail, design the data model for it but ship the live fallback first.
Neobank: worked implementation plan (flagship)
Use this as the quality bar. A neobank that ships on OMS today gives users a KYC'd account, a custodial USDC balance, card, cash, and bank top-ups, peer and external sends, bank withdrawals to registered accounts, and full transaction history.
# Neobank on Polygon OMS
## Summary
Each user gets a KYC'd customer record and a custodial USDC wallet on Polygon.
They top up by debit card, in-person cash, or bank transfer into a virtual
account, send USDC to other users or external addresses, withdraw to a
registered bank account, and view balances and history.
## Custody
Custodial (OMS wallets). Users do not manage keys; the OMS holds funds.
## Design
Start from the Universal Exports example design system (example-design.md); restyle to your brand.
A neobank is a mobile-first app, so render its app screens inside an iPhone device frame
(390x844 viewport, safe-area insets, bottom tab bar) by default. Build the light web dashboard
as a standard browser layout.
## Required KYC endorsements
basic, cryptoCustody, usd.
## Build steps (live today)
1. POST /auth/token -> bearer token (re-exchange when expiresIn elapses)
2. POST /customers -> cst_ (type individual; full identity: name, email, phone, DOB, address, SSN; do not send endorsements)
3. Poll GET /customers/{cst_} -> wait until endorsements (cryptoCustody, usd) reach ACTIVE
4. POST /customers/{cst_}/wallets -> wlt_ (asset usdc, chain polygon)
5a. Top up by card: POST /quotes (fiat->crypto) -> qt_, POST /transactions {qt_} -> txn_
5b. Top up by cash: GET /cash-locations?provider=&latitude=&longitude= -> locId + cashLocationReference, POST /cash-ins -> deposit code
5c. Top up by bank: POST /virtual-accounts (accountHolder customer, type bankUs, destination walletCrypto wlt_) -> va_; share bankDetails once provisioned
6. Send / pay: resolve recipient (email/username/0x) -> POST /quotes (source wlt_) -> qt_, POST /transactions {qt_} -> txn_
7. Withdraw to bank: POST /external-accounts (owner customer, type bankUs) -> ext_, POST /quotes (source wlt_, destination bankUs ext_) -> qt_, POST /transactions {qt_} -> txn_
8. Balances/history: GET /customers/{cst_}/balance, GET /transactions?customerId={cst_}
## Webhooks to handle
transaction.fiatToCrypto.processing | completed | failed (card, cash, and bank top-ups)
transaction.cryptoToCrypto.completed | failed (sends)
transaction.cryptoToFiat.completed | failed (bank withdrawals)
cash_in.completed | expired (cash top-ups)
## Early access (design for it, ship the fallback)
- Cash withdrawal (cash pickup at retail): early access. Live fallback: bank
withdrawal to a registered external account.
## Considerations
- Idempotency-Key on every POST; derive it deterministically per user action.
- Build entirely in sandbox first; use the simulate endpoints to drive funded states.
- Gate transacting on ACTIVE endorsements to avoid rejected transactions.
- sponsorGas:true so users never need POL for gas.
- Sign-in recovers identity; it never re-onboards. The embedded wallet is deterministic per identity, so recover the OMS customer to match (look it up by email/identity via GET /customers) and show KYC only when none exists. Treat the customer and wallet as the source of truth, not local or session state, and cache only after the recovery lookup runs.
- Verify the full lifecycle, not just first-run: sign up, sign out, sign back in with the same identity, and confirm the same wallet, customer, and balance with no KYC re-prompt. Run this round-trip alongside the funding and send checks.
Other product archetypes
Each is the spine trimmed to a shape. Confirm rails against the live-vs-early-access table before promising any step. Each archetype below shows the custodial path because it is the simplest default, but every one of them can be built non-custodial instead; confirm the custody choice (see the Custody section) before settling the plan.
- On-ramp (fiat to crypto): customer + KYC -> wallet -> card quote + transaction,
delivering USDC to an OMS wallet or external address. Live today. Reference: /payments/guides/fiat-to-crypto.
- Cash services (in-person): customer ->
GET /cash-locations->POST /cash-ins
for a one-time deposit code; webhook cashIn.completed auto-creates the fiat-to-crypto transaction. Live today. Reference: /api-reference/guide-cash-in.
- Off-ramp (crypto to fiat): USDC from an OMS wallet to a bank account.
Live today. Register the bank with POST /external-accounts, then quote from the wallet to the bankUs / bankIban / bankCanada destination; or stand up a deposit address so incoming crypto routes straight to the bank. Reference: /payments/guides/crypto-to-fiat.
- Remittance (fiat to fiat, cross-border): fiat-in -> USDC on Polygon -> fiat-out.
Sender-side fiat-to-crypto, the USDC leg, and recipient-side bank payout (to a registered external account, owned by a customer or a counterparty) are live; cash pickup delivery and the turnkey remittance product are early access.
- B2B / bulk payouts: from a platform OMS wallet, loop quote + transaction per
recipient. Crypto payouts and bank payouts to registered external accounts are live; register vendors as counterparties that own the accounts. The managed bulk-payout product is early access.
Custody: custodial vs non-custodial
Every product built on the OMS picks a custody model, so treat this as a required discovery question for any app, not a neobank-only detail. The same money-movement spine works under either choice; what changes is who holds the keys and how the wallet is created. Ask the developer which they want, explain the trade-off, and offer custodial as the default only because it is the simpler starting point.
- Custodial (simplest default, common for neobanks and payment apps): OMS wallets,
created with POST /customers/{id}/wallets. Polygon holds funds; the developer never touches keys. This is the spine above.
- Non-custodial (users hold their own keys): Polygon's embedded wallet lets
users sign in with email (a one-time code) or a social login, with no seed phrase, and keep self-custody. Email authentication requires your own project ID and publishable key from the dashboard: initialize the embedded wallet with both, or email sign-in will not work and no wallet is created. The onboarding carries your own branding: your app name, logo, and theme, so the wallet feels native to your product rather than a third-party screen. Use this when users must own their assets directly. The current SDKs (web and native), the sign-in flow, and branding options are in the wallet docs: /wallets/quickstart and /wallets/sdk/overview. Use these rather than the older connect package, which is deprecated.
The two are not mutually exclusive: a product can custody operating balances while giving power users non-custodial wallets.
Design starter (bring your own, or use the example)
The OMS API is the money layer; the UI is yours. Settle the design direction during setup:
- Bring your own brand. Use your logo, colors, type, and components. The embedded
wallet onboarding can carry that branding so the wallet feels native.
- No design yet? Start from Polygon's example neobank design system, "Universal
Exports": a complete visual contract with design tokens (dark app plus light web), component specs, copy rules, a self-review checklist, and screens already mapped to OMS endpoints (review transfer to quotes/transactions, add cash to cash-in, add money to a virtual account, receive crypto to a deposit address). It is built to be read by an AI build tool so it can generate on-brand, API-wired screens, then restyled to your own brand.
- Frame the app as a phone. A neobank, like most consumer money apps, is mobile-first,
so by default render the app screens inside an iPhone device frame (mobile viewport, safe-area insets, bottom tab bar). Build web surfaces such as the dashboard as a standard browser layout. The example design system specifies the frame and the safe areas.
Read it at /example-design.md. It follows the same live-vs-early-access split as this skill, so the screens you build match what the API can actually do today.
Other building blocks (beyond the OMS API)
Once the money-movement core is in place, these API surfaces extend it. Point to the docs rather than reproducing them here.
- Trails (cross-chain routing): route and swap tokens across chains, including
contract calls on arrival (for example, depositing into an ERC-4626 vault). /cross-chain.
- x402 (agentic payments): let machines and AI agents pay for API resources
over HTTP using the 402 status code. /payment-services/agentic-payments/x402/guides/quickstart-buyers.
Chain infrastructure (Polygon Chain, CDK, Agglayer) is not where most products start. Reach for it only if you need your own chain rather than building on the APIs above; see /pos/get-started/building-on-polygon.
Reference
Base URLs
- Sandbox:
https://sandbox-api.polygon.technology/v0.12 - Production:
https://api.polygon.technology/v0.12
Key docs
- OMS overview:
/oms/overview - Get started and credentials:
/payments/get-started - API reference:
/api-reference/overview - Payments overview:
/payments/overview - On/off ramps:
/payments/onramps-offramps
Conventions
- Auth:
Authorization: Bearer <token>fromPOST /auth/token; readexpiresIn/expiresAtand re-exchange when the token expires (do not assume a fixed lifetime). - Every POST takes an
Idempotency-Keyheader. - Wallet creation uses
chain; quotes and transactions usenetwork. - IDs:
cstcustomer,wltwallet,qtquote,txntransaction,vavirtual account,dadeposit address,ctpcounterparty,extexternal account (extbankUs,extbankIban,extbankCa,extcard,extwlt); cash locations are identified bylocIdpluscashLocationReference. accountHolderis always"customer"; it is the only valid value.- New-resource lists (external accounts, counterparties, virtual accounts, deposit addresses) and
GET /transactionspaginate withlimit/startingAfter/endingBefore;GET /customersandGET /cash-insuselimit/cursor. - Present money to users as USD; USDC is the settlement asset, not a user-facing label.
- Status is webhook-driven:
transaction.<type>.processing | completed | failed.