# Get bearer token Source: https://docs.polygon.technology/api-reference/auth/get-bearer-token /api-reference/openapi.yaml post /auth/token Exchanges an OMS API key + secret for a bearer token valid for 60 minutes. The token is signed by the OMS issuer and must be presented as `Authorization Bearer ` on every other endpoint. # Create a cash-in deposit code Source: https://docs.polygon.technology/api-reference/cashins/create-a-cash-in-deposit-code /api-reference/openapi.yaml post /cash-ins Creates a cash-in: reserves a deposit code the customer presents at a physical cash location to deposit fiat, which is then converted to crypto and credited to the destination wallet. The 201 response includes the cash-in and its deposit instructions (the code and its expiry). Pass an Idempotency-Key header to safely retry without creating duplicates. # Get a cash-in by ID Source: https://docs.polygon.technology/api-reference/cashins/get-a-cash-in-by-id /api-reference/openapi.yaml get /cash-ins/{cashInId} Retrieves a single cash-in by ID, including its current status and sub-status, source and destination amounts, fees, and deposit instructions. # List cash-ins Source: https://docs.polygon.technology/api-reference/cashins/list-cash-ins /api-reference/openapi.yaml get /cash-ins Returns a paginated list of cash-ins, optionally filtered by customer, status, type, date range, or free-text search. Results are ordered newest-first. Date filters `createdAfter` and `createdBefore` are inclusive, matching `GET /transactions`. # Refresh a cash-in deposit code Source: https://docs.polygon.technology/api-reference/cashins/refresh-a-cash-in-deposit-code /api-reference/openapi.yaml post /cash-ins/{cashInId}/refresh Regenerates the deposit code for an existing cash-in whose code has expired or is close to expiring, returning a fresh code and expiry. Pass an Idempotency-Key header to safely retry. # List cash locations Source: https://docs.polygon.technology/api-reference/cashlocation/list-cash-locations /api-reference/openapi.yaml get /cash-locations Returns cash pickup and deposit locations for the given provider within range of the supplied coordinates, each with its distance from that point. Use the flow parameter to select the cash-in or cash-out provider configuration. # Create a Counterparty Source: https://docs.polygon.technology/api-reference/counterparty/create-a-counterparty /api-reference/openapi.yaml post /counterparties Create a Counterparty. # Delete a Counterparty Source: https://docs.polygon.technology/api-reference/counterparty/delete-a-counterparty /api-reference/openapi.yaml delete /counterparties/{counterpartyId} Delete a Counterparty (soft delete). Returns 409 when the counterparty still owns active or pending External Accounts. # Get a Counterparty Source: https://docs.polygon.technology/api-reference/counterparty/get-a-counterparty /api-reference/openapi.yaml get /counterparties/{counterpartyId} Get a Counterparty by id. # List Counterparties Source: https://docs.polygon.technology/api-reference/counterparty/list-counterparties /api-reference/openapi.yaml get /counterparties List Counterparties filtered by customer (required). # Update a Counterparty Source: https://docs.polygon.technology/api-reference/counterparty/update-a-counterparty /api-reference/openapi.yaml patch /counterparties/{counterpartyId} Update a Counterparty (partial; unknown keys 400). # Get customer balance Source: https://docs.polygon.technology/api-reference/customer/get-customer-balance /api-reference/openapi.yaml get /customers/{id}/balance Get the aggregated estimated balance across all customer wallets/assets. # Create a customer Source: https://docs.polygon.technology/api-reference/customers/create-a-customer /api-reference/openapi.yaml post /customers # Delete a customer Source: https://docs.polygon.technology/api-reference/customers/delete-a-customer /api-reference/openapi.yaml delete /customers/{customerId} Soft delete. Sets status to inactive. Inactive customers cannot create new transactions but data is retained for compliance purposes. # Get customer by ID Source: https://docs.polygon.technology/api-reference/customers/get-customer-by-id /api-reference/openapi.yaml get /customers/{customerId} Returns the customer with their wallets included in the response. # List customers Source: https://docs.polygon.technology/api-reference/customers/list-customers /api-reference/openapi.yaml get /customers Returns a paginated list of customers under the current Project. # Update a customer Source: https://docs.polygon.technology/api-reference/customers/update-a-customer /api-reference/openapi.yaml patch /customers/{customerId} Partial update. Only include fields you want to change. Compliance-relevant changes (residentialAddress, lastName, identifyingInformation, ipAddress) may trigger endorsement re-evaluation. Including endorsement names in the endorsements array explicitly triggers re-evaluation. # Create a Deposit Address Source: https://docs.polygon.technology/api-reference/depositaddress/create-a-deposit-address /api-reference/openapi.yaml post /deposit-addresses Create a Deposit Address. Deposit addresses must be enabled for your project and the customer must be provisioned for them. # Get a Deposit Address Source: https://docs.polygon.technology/api-reference/depositaddress/get-a-deposit-address /api-reference/openapi.yaml get /deposit-addresses/{depositAddressId} Fetch a Deposit Address by ID. # List all Deposit Addresses Source: https://docs.polygon.technology/api-reference/depositaddress/list-all-deposit-addresses /api-reference/openapi.yaml get /deposit-addresses List all Deposit Addresses across the organization. Spans every customer in the caller's organization; optionally filter by status and/or customer. # Update a Deposit Address Source: https://docs.polygon.technology/api-reference/depositaddress/update-a-deposit-address /api-reference/openapi.yaml patch /deposit-addresses/{depositAddressId} Update a Deposit Address. The patchable fields are `destination` (re-point to a different bank-type External Account), `label`, and `metadata`; any other JSON key in the body is rejected with 400. Re-pointing `destination` to a healthy bank External Account recovers a DA from `inactiveActionRequired` back to `active`. # Create an External Account Source: https://docs.polygon.technology/api-reference/externalaccount/create-an-external-account /api-reference/openapi.yaml post /external-accounts Create an External Account (saved payment destination). Provisions synchronously against the configured payment provider; the account starts `pending` and flips to `active` or `failed`. Exactly one per-type detail object must match `type`. The owner discriminator determines whether this account belongs to a customer directly or to one of the customer's counterparties. # Delete an External Account Source: https://docs.polygon.technology/api-reference/externalaccount/delete-an-external-account /api-reference/openapi.yaml delete /external-accounts/{externalAccountId} Delete an External Account (soft delete). # Get an External Account Source: https://docs.polygon.technology/api-reference/externalaccount/get-an-external-account /api-reference/openapi.yaml get /external-accounts/{externalAccountId} Get an External Account by id. # List External Accounts Source: https://docs.polygon.technology/api-reference/externalaccount/list-external-accounts /api-reference/openapi.yaml get /external-accounts List External Accounts scoped to a customer. Optionally filter by `counterpartyId`. # Update an External Account Source: https://docs.polygon.technology/api-reference/externalaccount/update-an-external-account /api-reference/openapi.yaml patch /external-accounts/{externalAccountId} Update an External Account. Only `label` and `metadata` are mutable; any other JSON key in the body is rejected with 400. # Bulk payouts Source: https://docs.polygon.technology/api-reference/guide-b2b-payouts Disburse stablecoin or fiat payments to multiple recipients from a platform treasury wallet. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. ## How it works Send payments to a list of recipients from a central platform wallet. OMS converts USDC from your treasury to each recipient's local currency and delivers via ACH, wire, or SWIFT. This pattern covers payroll, contractor payments, marketplace settlements, and any flow where you control the source funds. Each payout is a single quote and transaction. The source side is your platform customer's OMS wallet; the destination side is an [external account](/api-reference/overview#external-accounts) (an `ext_bankUs_`, `ext_bankIban_`, or `ext_bankCa_` bank account) referenced by its ID in the quote. Every transaction is owned by your platform `customerId`, and OMS records each side's identity on the transaction's `party` object. To reconcile a run, filter `GET /transactions?customerId=...`. Register each recipient once before their first payout. Create a counterparty with `POST /counterparties` (`customerId` and `name`, plus optional contact and address details), then register their bank account with `POST /external-accounts`, setting `owner` to `{ "kind": "counterparty", "counterpartyId": ... }` and supplying the bank detail object that matches `type`. The account starts `pending` and becomes `active` once provisioning succeeds; quote against its `ext_` ID from then on.
Per-recipient payout pattern
Platform treasury
OMS custodial wallet
Recipient Rail Quote Transaction
Alice ACH wlt\_treasury → ext\_bankUs\_alice txn\_alice
Bob ACH wlt\_treasury → ext\_bankUs\_bob txn\_bob
Carlos SWIFT wlt\_treasury → ext\_bankIban\_carlos txn\_carlos
## What is available today The per-recipient mechanics are part of the canonical API. Register each recipient as a counterparty with `POST /counterparties` and their bank account with `POST /external-accounts` (owner kind `counterparty`). For each recipient you then create a `POST /quotes` with your treasury wallet as the `source` and the recipient's bank as the `destination` (`bankUs`, `bankIban`, or `bankCanada`, referenced by its `ext_` ID), then execute it with `POST /transactions` referencing the quote by `quoteId`. ACH, achSameDay, wire, RTP, and SWIFT (IBAN) rails are selected with the destination's `network`. Use a deterministic `Idempotency-Key` on each request for safe retries, attach `metadata` for reconciliation, and track each payout through the `transaction.cryptoToFiat.processing`, `transaction.cryptoToFiat.completed`, and `transaction.cryptoToFiat.failed` events. Scope a run with `GET /transactions?customerId=...`. Bulk payouts are enabled per project through early access; contact us before you build against them. ## Planned capabilities * Batch endpoints for parallel quote creation and transaction execution across all recipients For a step-by-step walkthrough of a single third-party payout with full request and response payloads, see [Pay a third party](/payments/guides/third-party-payouts). *** Bulk payouts are under active development and available through early access. Contact us to enable it for your project. Share your use case and we'll reach out when bulk payouts are available. # Bank transfers Source: https://docs.polygon.technology/api-reference/guide-bank-transfers How to move money between bank accounts and OMS wallets in both directions. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. OMS moves money between bank accounts and wallets in both directions: * **Pay out from a wallet to a bank account** (USDC to fiat): run the `POST /quotes` then `POST /transactions` pattern with the customer's OMS wallet as the source and their registered bank account as the destination. OMS infers the `cryptoToFiatAccount` direction from the two instruments. * **Receive a bank deposit into a wallet** (fiat to USDC): provision a [virtual account](/payments/guides/virtual-accounts). The customer wires or ACH-transfers to the dedicated bank account number, and OMS auto-converts the deposit to crypto with a `fiatAccountToCrypto` transaction. A quote's `source` is always an OMS wallet (`walletOms`) or a registered card; a bank account is always a destination. There is no bank-funded quote: inbound bank money comes through a virtual account, not a quote. ## Pay out from a wallet to a bank account This flow sends USDC from a customer's wallet to a US bank account over ACH. OMS infers the `cryptoToFiatAccount` direction from the source and destination instruments. ### Step 1: Register the destination bank account The quote's bank destination is an [external account](/api-reference/overview#external-accounts) referenced by its ID: `ext_bankUs_` for a US bank, `ext_bankIban_` for an IBAN account, or `ext_bankCa_` for a Canadian bank. Register it with `POST /external-accounts`. The body carries an `owner`, a `type`, and exactly one per-type object named after the type; supplying a per-type object that does not match `type` is rejected with `422`. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/external-accounts \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ext-bank-001" \ -d '{ "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "bankUs": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" } }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/external-accounts \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ext-bank-001" \ -d '{ "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "bankUs": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" } }' ``` Each bank type takes its own named object: * `bankUs`: `accountNumber` and `routingNumber` required; `accountType` (`checking` or `savings`) and `bankName` optional. * `bankIban`: `iban` and `BIC` required; `bankAddress` and `countryCode` optional (the country is derived from the first two letters of the IBAN when omitted). * `bankCanada`: `institutionNumber`, `transitNumber`, and `accountNumber` required; `bankName` optional. The response returns the `ext_` ID: ```json theme={null} { "id": "ext_bankUs_01H9X...", "object": "externalAccount", "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "category": "fiatAccount", "status": "pending", "bankUs": { "accountNumberLast4": "9012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "createdAt": "2026-01-15T10:02:00Z", "updatedAt": "2026-01-15T10:02:00Z" } ``` The account starts `pending` and flips to `active` when provisioning completes, or `failed` when the provider rejects it or provisioning times out (`failureReason` says why). Sensitive values are write-only: reads expose only the last four digits (`bankUs.accountNumberLast4`, or `bankIban.ibanLast4` for IBAN accounts), never the full account number or IBAN. List a customer's accounts with `GET /external-accounts?customerId=...` (the `customerId` query parameter is required; add `counterpartyId` to filter), update `label` or `metadata` with `PATCH /external-accounts/{externalAccountId}`, and soft-delete with `DELETE` (returns `204`). Financial fields are immutable: to change account details, register a new account and re-point your flows. To pay a bank account owned by a third-party recipient, create a counterparty first with `POST /counterparties` (`customerId` and `name` required; optional `entityType`, contact fields, and address), then register the account with `owner: { "kind": "counterparty", "counterpartyId": "..." }`. Deleting a counterparty that still owns active external accounts returns `409`. ### Step 2: Create a quote The source is the customer's OMS wallet; the destination is the registered bank. Set `amount` on exactly one side. The `network` selects the ACH rail (`ach`, `achSameDay`, `wire`, or `rtp`), and `accountHolder` names the sender (`customer` is the only accepted value). ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/quotes \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: qt-bank-out-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "100.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9X...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/quotes \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: qt-bank-out-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "100.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9X...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } }' ``` The quote returns in `open` status with a locked `pricing` object. Present `pricing.destination.amountNet` to the customer before they confirm. If the quote expires before you accept it, create a new one. `sponsorGas` defaults to `true`, so OMS covers Polygon gas for the on-chain leg. At launch all fee components and `sponsorGasCost` are `0.00`. ### Step 3: Execute the transaction Accept the quote by creating a transaction. The request body references the quote ID with `quoteId`: the quote is the contract. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/transactions \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: txn-bank-out-001" \ -d '{ "quoteId": "qt_01H9Xq..." }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/transactions \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: txn-bank-out-001" \ -d '{ "quoteId": "qt_01H9Xq..." }' ``` OMS pulls USDC from the wallet, converts it, and sends the bank payout. The transaction starts at `processing` and moves to `completed` when the payout settles. Track it by polling `GET /transactions/{transactionId}` or listening for the `transaction.cryptoToFiat.processing`, `transaction.cryptoToFiat.completed`, and `transaction.cryptoToFiat.failed` events. Each event carries the full transaction under `payload`; branch on the event name or `payload.status`. ## Receive a bank deposit into a wallet Inbound bank money arrives through a virtual account, not a quote. Provision a dedicated bank account number per customer, share the routing details, and OMS auto-converts each deposit to crypto. The auto-created transaction has `sourceToDestination: fiatAccountToCrypto` and fires the `transaction.fiatToCrypto.*` events. See the [Virtual accounts](/payments/guides/virtual-accounts) guide for the full flow. ## Beneficiary requirements * Banks are referenced with standard identifiers: ABA routing numbers for US accounts, institution and transit numbers for Canada, and IBAN plus BIC for SWIFT. There is no proprietary bank catalog to query. * A beneficiary document number is not required on US domestic rails. * Every beneficiary is screened for sanctions and AML before payout. Name matching against the receiving account follows the receiving bank's rules; OMS does not impose a separate mandatory pre-validation. ## Limits There is no platform-imposed minimum or maximum per transaction, and no daily, monthly, or per-beneficiary caps on business bank rails. Amounts above bank-internal thresholds can trigger a compliance review before release: the transaction holds rather than rejects, and the hold surfaces through the standard `status` and error model. Execution is never force-split across multiple payments. ## Key points * **A quote's source is always an OMS wallet or a card.** A bank account is always a destination. There is no bank-funded quote. * **Register the bank, then reference it by ID.** Create it with `POST /external-accounts` (an `owner`, a `type`, and exactly one per-type object such as `bankUs`), then use the returned `ext_bankUs_` (or `ext_bankIban_` / `ext_bankCa_`) ID in the quote destination. Counterparty-owned accounts use `owner: { "kind": "counterparty", "counterpartyId": "..." }` after a `POST /counterparties`. * **Pick the rail with `network`.** For `bankUs`, use `ach`, `achSameDay`, `wire`, or `rtp`. IBAN routes over `swift`; Canadian USD over `swift`, CAD over `local`. Name the sender with `accountHolder` (`customer` is the only accepted value). * **Inbound uses virtual accounts.** A wire or ACH deposit into a wallet comes through a virtual account, which auto-creates a `fiatAccountToCrypto` transaction. * **Track with the `transaction.cryptoToFiat.*` events.** OMS fires `processing`, `completed`, and `failed` events for a bank payout; the full transaction object is in `payload`. See [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog. * **Idempotency keys are required on every mutating request.** Use a deterministic key so you can safely retry on network errors. # Cash-in Source: https://docs.polygon.technology/api-reference/guide-cash-in How to let a customer deposit physical cash at a retail location and receive USDC in their wallet. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow.
Cash-in flow
1 App OMS POST /cash-ins (source, destination, cash)
2 OMS App \{depositInstructions: \{code: "XXX XXX", expiresAt}}
3 App Customer Show deposit code + nearby location
4 Customer Retail Present code, hand over cash
5 Retail OMS Confirm deposit + actual amount
6 OMS Auto-create cashToCrypto transaction
7 OMS App Webhooks: cashIn.completed, transaction.fiatToCrypto.completed
## Prerequisites Before creating a cash-in, you need: 1. **A customer** with a `cst_` ID and the `usd` endorsement active (required for cash flows). 2. **A cash location**, retrieved from `GET /cash-locations` with `provider`, `latitude`, `longitude`, and `flow=cash_in`. This is the retail location where the customer will deposit cash. Each result includes a `locId` and a `cashLocationReference`, which you pass on the cash-in's `cash` field as `locationId` and `locationReference` respectively. 3. **A crypto destination wallet**: an OMS custodial wallet (`wlt_`) referenced by `destination.wallet.id`, or an external wallet referenced by `destination.wallet.blockchainAddress` or `destination.wallet.externalAccount`. 4. **A webhook endpoint** subscribed to the `cashIn.*` and `transaction.fiatToCrypto.*` events, registered with `POST /webhooks` or in the OMS Dashboard. ## Limits The following limits apply to cash-ins: | Limit | Value | | ------------------- | ------------------------------------ | | Transaction minimum | \$20 | | Transaction maximum | \$500 (\$1,000 at Walmart locations) | | Daily maximum | \$1,500 and 3 transactions | | Weekly maximum | \$3,500 and 12 transactions | | Monthly maximum | \$5,000 and 20 transactions | ## How Cash-In Works Cash-In is a specialized, ephemeral flow for in-person cash deposits. Unlike Deposit Addresses (persistent, reusable, crypto-source), a cash-in generates a one-time deposit code valid for 1 hour. The customer presents the code at a retail location, deposits cash, and OMS auto-converts to crypto. Key differences from other funding methods: * **Indicated amount, not exact amount.** You provide an optional `source.indicatedAmount`. OMS uses this to generate upfront estimates. The actual deposit amount is determined by how much cash the customer deposits at the counter. If you omit `indicatedAmount`, no estimates are returned. * **Amounts are estimated at creation, finalized after deposit.** At creation the `source` and `destination` amounts are derived from `indicatedAmount`. Once the customer deposits cash, they are recalculated on the actual amount. * **No quote step.** Cash-In auto-creates a transaction when the deposit is received, similar to Deposit Addresses and Virtual Accounts. * **Gas sponsorship.** `sponsorGas` defaults to `true`, so OMS absorbs the on-chain gas for the destination delivery. The absorbed cost appears as the top-level `sponsorGasCost`. At launch all fees and gas are `0.00`. ## Step 1: Create a Cash-In Code ``` POST /cash-ins Authorization: Bearer {accessToken} Idempotency-Key: ci_order_789 Content-Type: application/json ``` ```json theme={null} { "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "indicatedAmount": "200.00" }, "destination": { "asset": "usdc", "network": "polygon", "wallet": { "id": "wlt_01H9Xb..." } }, "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNDpsYXQ9MzguNDk1MTMzLGxuZz0tMTIxLjUwNTMzNg==" }, "sponsorGas": true, "metadata": { "orderId": "ord_789" } } ``` **Required fields:** `customerId`, `source`, `destination`, and `cash`. **`source.asset`** (required): the deposit currency; `usd`. **`source.indicatedAmount`** (optional): the expected deposit amount. OMS uses this value to generate upfront estimates in the response. If omitted, no estimates are returned. The actual deposit amount is determined by how much cash the customer deposits at the location. **`destination`** (required): the crypto the deposit is converted to. Set `asset` and `network`, and identify the wallet with `wallet.id` (an OMS wallet, `wlt_`), `wallet.blockchainAddress` (an external on-chain address), or `wallet.externalAccount` (a registered external account). **`cash`** (required): the retail location. Set `locationId` (from the `locId` field of `GET /cash-locations`) and `locationReference` (from the `cashLocationReference` field), which selects a specific store. **`sponsorGas`** (optional): defaults to `true`, so OMS absorbs the on-chain gas for the destination delivery. Only `true` is currently supported. ### Response, `201 Created` ```json theme={null} { "id": "ci_01H9Xy...", "object": "cashIn", "type": "fiatToCrypto", "status": "pending", "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "indicatedAmount": "200.00", "amountGross": "200.00", "amountNet": "200.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "destination": { "asset": "usdc", "network": "polygon", "amountGross": "199.00", "amountNet": "199.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" }, "wallet": { "id": "wlt_01H9Xb...", "blockchainAddress": "0x7B3a9F2c4D1eA8bF6390cE5d2B7fA104C8e3D9b1" } }, "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNA==" }, "location": { "name": "CVS Pharmacy #4521", "address": "123 Main St, New York, NY 10001" }, "rates": { "pair": "usd/usdc", "exchangeRate": "0.9950", "effectiveRate": "0.9950" }, "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0", "depositInstructions": { "code": "483 291", "expiresAt": "2026-03-16T19:30:00Z", "locationName": "CVS Pharmacy #4521", "locationAddress": "123 Main St, New York, NY 10001" }, "transactionId": null, "metadata": { "orderId": "ord_789" }, "createdAt": "2026-03-16T18:30:00Z", "updatedAt": "2026-03-16T18:30:00Z" } ``` **What to show the customer:** * `depositInstructions.code`: the code they present at the retail counter ("483 291"). * `depositInstructions.locationName` and `locationAddress`: where to go. * `depositInstructions.expiresAt`: the code is valid for 1 hour. * The estimated economics based on `indicatedAmount` (\$200.00): `source` and `destination` each carry `amountGross`, `amountNet`, and a `feesDeducted` breakdown, and the top-level `rates` object carries `pair`, `exchangeRate`, and `effectiveRate`. Show the customer what they would receive at this deposit amount. At launch all fee components are `0.00`. `transactionId` is `null` until the deposit is confirmed. OMS fires the `cashIn.created` event when the code is issued. ## Step 2: Customer Deposits Cash The customer visits the location and presents their code at the counter. This step happens outside OMS, at the retail location's payment terminal. OMS is notified when the deposit is received. If the code expires before the customer arrives, you can refresh it (see "Refreshing an Expired Code" below). ## Step 3: Deposit Received, Cash-In Completes Once the retail partner confirms the deposit, OMS recalculates pricing on the actual amount, converts USD to the destination crypto, and delivers it. The cash-in moves to `completed`, OMS fires the `cashIn.completed` event, and the cash-in's `transactionId` is populated. Read the cash-in to see the final state: ``` GET /cash-ins/ci_01H9Xy... Authorization: Bearer {accessToken} ``` ```json theme={null} { "id": "ci_01H9Xy...", "object": "cashIn", "type": "fiatToCrypto", "status": "completed", "subStatus": "settled", "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "indicatedAmount": "200.00", "amountGross": "200.00", "amountNet": "200.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "destination": { "asset": "usdc", "network": "polygon", "amountGross": "199.00", "amountNet": "199.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" }, "wallet": { "id": "wlt_01H9Xb...", "blockchainAddress": "0x7B3a9F2c4D1eA8bF6390cE5d2B7fA104C8e3D9b1" } }, "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNA==" }, "location": { "name": "CVS Pharmacy #4521", "address": "123 Main St, New York, NY 10001" }, "rates": { "pair": "usd/usdc", "exchangeRate": "0.9950", "effectiveRate": "0.9950" }, "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0", "depositInstructions": { "code": "483 291", "expiresAt": "2026-03-16T19:30:00Z", "locationName": "CVS Pharmacy #4521", "locationAddress": "123 Main St, New York, NY 10001" }, "transactionId": "txn_01H9Xz...", "metadata": { "orderId": "ord_789" }, "createdAt": "2026-03-16T18:30:00Z", "updatedAt": "2026-03-16T18:47:00Z", "completedAt": "2026-03-16T18:47:00Z" } ``` **What changed from the creation response:** * `status` is now `completed` and `subStatus` is `settled`. * The `source` and `destination` amounts are recalculated on the actual deposit amount (they were estimated from `indicatedAmount` at creation). * `completedAt` is populated. * `transactionId` links to the auto-created transaction (the `txn_` ID). Read that transaction to see the on-chain delivery hash. ## Step 4: Auto-Created Transaction When the cash-in completes, OMS auto-creates a transaction with `sourceToDestination: cashToCrypto`. It follows the standard processing → completed lifecycle, firing the fiat-funded transaction events (`transaction.fiatToCrypto.*`). Its `precursor` identifies the originating cash-in. ### Webhook: `transaction.fiatToCrypto.processing` ```json theme={null} { "eventId": "evt_01H9Xw2...", "eventName": "transaction.fiatToCrypto.processing", "resourceType": "transaction_fiat_to_crypto", "resourceId": "txn_01H9Xz...", "occurredAt": "2026-03-16T18:47:05Z", "payload": { "id": "txn_01H9Xz...", "object": "transaction", "sourceToDestination": "cashToCrypto", "status": "processing", "subStatus": "processing.fundsPulled", "customerId": "cst_01H9Xa...", "precursor": { "type": "cashIn", "details": { "cashInId": "ci_01H9Xy..." } }, "source": { "party": { "relationship": "customer", "customerId": "cst_01H9Xa...", "entityType": "individual" }, "type": "cash", "category": "cash", "details": { "asset": "usd", "cashLocationId": "loc_01H9Xd...", "cashLocationReference": "R1JFRU5ET1QtMjQzNA==" } }, "destination": { "party": { "relationship": "customer", "customerId": "cst_01H9Xa...", "entityType": "individual" }, "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "blockchainAddress": "0x7B3a9F2c...e3D9b1", "custodyType": "custodial", "txHash": null } }, "pricing": { "source": { "asset": "usd", "amountGross": "200.00", "amountNet": "200.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "destination": { "asset": "usdc", "amountGross": "199.00", "amountNet": "199.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "pair": "usd/usdc", "exchangeRate": "0.9950", "effectiveRate": "0.9950", "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0.00" }, "estimatedArrival": null, "error": null, "hold": null, "metadata": { "orderId": "ord_789" }, "createdAt": "2026-03-16T18:47:05Z", "updatedAt": "2026-03-16T18:47:05Z", "expiresAt": null } } ``` **Key fields on the auto-created transaction:** * `precursor.type: "cashIn"`: links this transaction back to the originating cash-in (`details.cashInId`). * `sourceToDestination: "cashToCrypto"`: the source was a cash deposit; the destination is crypto. * `source.type: "cash"`: there is no blockchain transaction on the source side. * All amounts live in `pricing`; the sides carry only identity and instrument detail. ### Webhook: `transaction.fiatToCrypto.completed` OMS fires `transaction.fiatToCrypto.completed` when the crypto lands. The envelope is the same shape; in the payload, `status` is now `completed`, `subStatus` is `null`, and `destination.details.txHash` is populated with the on-chain hash. At this point the flow is done. The customer deposited \$200.00 cash at CVS Pharmacy #4521, and the converted USDC was delivered to wallet `wlt_01H9Xb...` on Polygon. At launch all fee components are `0.00`. ## Refreshing an Expired Code If the customer doesn't arrive before the code expires, refresh it to issue a new code with a new 1-hour window. The previous code is invalidated. ``` POST /cash-ins/ci_01H9Xy.../refresh Authorization: Bearer {accessToken} Idempotency-Key: ci_refresh_789_2 ``` The response is the full cash-in object with a fresh `depositInstructions.code` and an `expiresAt` extended by another hour. Everything else stays the same: ```json theme={null} { "id": "ci_01H9Xy...", "object": "cashIn", "status": "pending", "depositInstructions": { "code": "917 042", "expiresAt": "2026-03-16T20:30:00Z", "locationName": "CVS Pharmacy #4521", "locationAddress": "123 Main St, New York, NY 10001" }, "transactionId": null } ``` **Constraints:** Refresh can only be called while the cash-in is in `pending` status. If the cash-in has already `completed` or `expired`, the call returns `422 Unprocessable`. ## Code Expiration If the code expires without a deposit, the cash-in moves to `expired` and OMS fires the `cashIn.expired` event. You can also detect this by reading the cash-in: ``` GET /cash-ins/ci_01H9Xy... Authorization: Bearer {accessToken} ``` The response shows `"status": "expired"` and `transactionId: null`. An expired cash-in is terminal: no transaction is created and no money moves. If the customer still wants to deposit cash, create a new cash-in. ## Polling You can read the cash-in status at any time: ``` GET /cash-ins/ci_01H9Xy... Authorization: Bearer {accessToken} ``` Or list cash-ins, optionally filtered by customer: ``` GET /cash-ins?customerId=cst_01H9Xa... Authorization: Bearer {accessToken} ``` The list response is `{ data: [...], hasMore, nextCursor }`. Page with the `limit` and `cursor` query parameters. ## Failure Handling Cash-In failures can happen at two levels: **Cash-In level:** The code expires before the customer deposits cash. The cash-in moves to `expired` and no transaction is created. No money moved, no refund needed. OMS fires `cashIn.expired`; you can also detect this by reading the cash-in. **Transaction level:** The cash deposit is received but crypto delivery fails. The auto-created transaction moves to `failed` and you receive a `transaction.fiatToCrypto.failed` event with the payload's `error` object populated: ```json theme={null} { "eventId": "evt_01H9Xw5...", "eventName": "transaction.fiatToCrypto.failed", "resourceType": "transaction_fiat_to_crypto", "resourceId": "txn_01H9Xz...", "occurredAt": "2026-03-16T18:50:00Z", "payload": { "id": "txn_01H9Xz...", "object": "transaction", "sourceToDestination": "cashToCrypto", "status": "failed", "subStatus": null, "customerId": "cst_01H9Xa...", "precursor": { "type": "cashIn", "details": { "cashInId": "ci_01H9Xy..." } }, "error": { "code": "onChainRevert", "message": "Delivery transaction reverted on-chain" }, "metadata": { "orderId": "ord_789" }, "createdAt": "2026-03-16T18:47:05Z", "updatedAt": "2026-03-16T18:50:00Z" } } ``` Cash refunds are handled outside OMS at the retail location. ## Compliance Review A transaction triggered by a cash-in may be held for compliance review. It stays in `processing` with `subStatus` `processing.underReview`. Do not surface this to the customer. Once resolved, the transaction proceeds to `completed` or `failed`, firing `transaction.fiatToCrypto.completed` or `transaction.fiatToCrypto.failed`. ## Summary: Webhook Events The cash-in flow delivers two event streams: the cash-in's own lifecycle events, and the fiat-funded transaction events fired on the auto-created transaction. See [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog. | Event | When | | ------------------------------------- | ---------------------------------------------------------------------------------------- | | `cashIn.created` | The deposit code was issued | | `cashIn.completed` | The cash was deposited and converted; `transactionId` links the auto-created transaction | | `cashIn.expired` | The code expired before a deposit was made | | `transaction.fiatToCrypto.processing` | Cash received, crypto delivery underway | | `transaction.fiatToCrypto.completed` | Crypto delivered to the destination | | `transaction.fiatToCrypto.failed` | Crypto delivery failed; see `payload.error` | Each event carries the full resource object under `payload`. Branch on the event name or `payload.status`; treat `subStatus` as optional operational detail. ## Key Points * **Indicated amount for upfront estimates.** Pass an optional `source.indicatedAmount`. OMS returns estimated amounts at creation; final amounts are calculated when the customer deposits. If omitted, no estimates are returned. * **Amounts and rates on the cash-in.** The `source` and `destination` each carry `amountGross`, `amountNet`, and a `feesDeducted` breakdown; the top-level `rates` object carries `pair`, `exchangeRate`, and `effectiveRate`. At launch all fee components are `0.00`. * **Gas sponsorship.** `sponsorGas` defaults to `true`; OMS absorbs the destination gas. The absorbed cost shows as the top-level `sponsorGasCost` (`"0"` at launch). * **Codes expire in 1 hour.** Call `POST /cash-ins/{cashInId}/refresh` to issue a new code if the customer needs more time. Only works while `status` is `pending`. * **Cash-In is single-use.** Once a deposit is received or the code expires, the cash-in is terminal. Create a new one for the next deposit. * **Two event streams.** The cash-in fires `cashIn.created`, `cashIn.completed`, and `cashIn.expired`; the auto-created transaction fires `transaction.fiatToCrypto.*`. Subscribe to both, or read the cash-in with `GET /cash-ins/{cashInId}` if you prefer polling. * **The source is cash.** There is no blockchain transaction on the source side; the auto-created transaction's `sourceToDestination` is `cashToCrypto`. * **The destination is an OMS or external wallet.** Identify it with `destination.wallet.id` (an OMS wallet, `wlt_`), `destination.wallet.blockchainAddress` (an external on-chain address), or `destination.wallet.externalAccount` (a registered external account). # Cash pickup Source: https://docs.polygon.technology/api-reference/guide-cash-pickup Let a customer withdraw crypto as physical cash at a retail or ATM location. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. ## How it works A customer holds USDC in an OMS wallet and wants to withdraw as physical cash. You create a quote whose source is the customer's OMS wallet and whose destination is a `cash` instrument at an ALLPOINT pickup location, with the cash amount set on the destination side. OMS converts the crypto to fiat, issues a time-limited pickup code, and the customer collects cash at a retail counter or ATM. The transaction direction is `cryptoToCash`. This is the complement to [Cash-in](/api-reference/guide-cash-in). Cash-in funds a wallet from cash; cash pickup withdraws from a wallet to cash.
Cash pickup flow
1 App OMS POST /quotes (cryptoToCash, amount on destination)
2 OMS App Quote with pickup fee breakdown
3 App Customer Show amount to receive
4 Customer App Confirm
5 App OMS POST /transactions \{quoteId}
6 OMS Pull USDC from wallet
7 OMS App Webhook: transaction.cryptoToFiat.cashPickupReady
8 App Customer Show pickup code + location
9 Customer Retail Present code, collect cash
10 OMS App Webhook: transaction.cryptoToFiat.completed
## Limits The following limits apply to cash pickups (cash-outs): | Limit | Value | | --------------------- | ----------------------------------------------------------------- | | Transaction increment | Multiples of \$20.00 USD | | Transaction minimum | \$20 | | Transaction maximum | \$400 | | Daily maximum | \$1,000 in California and New Mexico; \$2,000 in all other states | | Monthly maximum | \$60,000 | ## What is available today The quote and transaction mechanics for a cash pickup are part of the canonical API. A `cryptoToCash` quote takes an OMS wallet as the `source` and a `cash` destination whose `details` carry `asset: usd`, a `cashLocationId`, a `cashLocationReference`, and the `amount` (a multiple of \$20.00, up to \$400.00 per transaction). You execute it with `POST /transactions` referencing the quote by `quoteId`, and OMS drives the pickup through the `transaction.cryptoToFiat.*` events: `transaction.cryptoToFiat.processing` when the payout starts, `transaction.cryptoToFiat.cashPickupReady` when the pickup code is issued, `transaction.cryptoToFiat.completed` when the cash is collected, and `transaction.cryptoToFiat.codeExpired` if the code expires uncollected. The transaction's `subStatus` carries the matching detail (for example `completed.cashPickupCollected`). See [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog. Cash pickup is enabled per project through early access; contact us before you build against it. ## Planned capabilities * Cash-out location lookup with `GET /cash-locations` and `flow=cash_out` * Time-limited pickup codes with automatic refund on expiry * Wider coverage across US retail networks and ATMs *** Cash pickup is under active development and available through early access. Contact us to enable it for your project. Share your use case and we'll reach out when cash pickup is available. # Global remittance Source: https://docs.polygon.technology/api-reference/guide-global-remittance Cross-border money transfer: fiat in, stablecoin settlement on Polygon, fiat out. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. ## How it works A sender deposits fiat in their local currency via card, ACH, or cash. OMS converts to USDC on Polygon, settles in under two seconds, then converts back to fiat and delivers to the recipient's bank account, mobile wallet, or cash pickup location. In OMS terms this is a chain of transactions across two parties. The sender funds USDC into an OMS wallet (a `cashToCrypto` cash-in, a `fiatAccountToCrypto` bank deposit, or pull-from-card). You then pay out from that wallet to the recipient: `cryptoToFiatAccount` for a bank account or `cryptoToCash` for a cash pickup. The recipient is registered once as a counterparty (`POST /counterparties`) that owns a bank [external account](/api-reference/overview#external-accounts) (`POST /external-accounts` with owner kind `counterparty`); the payout quote references that account by its `ext_` ID, and OMS records each side's identity on the transaction's `party` object.
Remittance flow
1 Sender OMS Deposit fiat via card, ACH, or cash
2 OMS Converts fiat to USDC (cashToCrypto / fiatAccountToCrypto)
3 Polygon Settles USDC onchain (\~2s finality)
4 OMS Converts USDC to recipient currency (cryptoToFiatAccount / cryptoToCash)
5 OMS Recipient Delivers local fiat via bank, mobile wallet, or cash pickup
Fiat-to-fiat settlement that takes 2-5 days through correspondent banking completes in under two minutes using Polygon as the settlement layer. ## What is available today The building blocks are part of the canonical API. The sender side uses the [cash-in](/api-reference/guide-cash-in) flow or a virtual-account bank deposit; the recipient side uses a `POST /quotes` and `POST /transactions` payout (`cryptoToFiatAccount` to a bank, `cryptoToCash` to a cash pickup), executed by referencing the quote with `quoteId`. Corridor rates are locked at quote time in the quote's `pricing` (`exchangeRate` and `effectiveRate`), and compliance and KYC are handled by OMS at every step. Recipient counterparties and their bank accounts are registered with `POST /counterparties` and `POST /external-accounts`; cash pickup is enabled per project through early access. Global remittance is enabled per project through early access; contact us before you build against it. ## Planned capabilities * Card on the sender side * Additional multi-currency corridors *** Global remittance is under active development and available through early access. Contact us to enable it for your project. Share your use case and we'll reach out when global remittance is available. # Open Money Stack API Source: https://docs.polygon.technology/api-reference/overview How the Open Money Stack API works: core concepts, transaction types, and API structure. ## What Is the Open Money Stack API? The Open Money Stack API (OMS) is a unified interface for moving money across fiat rails, blockchains, and currencies. One integration gives you access to bank transfers, card payments, cash networks, and multiple blockchains. *** ## Three Ways to Move Money OMS provides three mechanisms for money movement, each suited to different use cases: | Mechanism | What It Does | When to Use | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | **Transactions** | Move money between crypto and fiat. OMS infers the direction from the source and destination instruments. Two-step flow: Create a Quote → Create a Transaction. Also includes **Cash-In**: a code-based flow for in-person cash deposits. | Amount known upfront (standard transactions) or determined at the counter (cash-in). | | **Deposit Addresses** | Persistent deposit configurations for crypto sources that auto-create transactions when funds arrive, paying out to a registered bank account. | "Send any amount to this address" flows. Amount unknown until deposit. | | **Virtual Accounts** | Dedicated bank account numbers that auto-convert incoming fiat to crypto. | Customers who need a persistent bank account number for recurring fiat-to-crypto conversion. | *** ## How a Transaction Works 1. **Create a Quote.** `POST /quotes` with source, destination, and amount. OMS locks pricing and returns the quote in `open` status with a full fee breakdown. 2. **Create a Transaction.** Review the pricing, then call `POST /transactions` with the quote ID. OMS begins execution and the transaction moves to `processing`. 3. **Track.** Poll `GET /transactions/{id}` or listen for webhook events. The direction (`sourceToDestination`, for example `cryptoToCrypto`, `fiatAccountToCrypto`, or `cryptoToFiatAccount`) is inferred from the source and destination instruments. If pricing expires before transaction creation, create a new quote. Every transaction records a `customerId` (the quote owner and sender) and carries counterparty identity inline on the source and destination `party` block. The `party` discriminator distinguishes an OMS customer from a saved external counterparty or an unrecognized external party, so two-sided flows such as remittances and B2B payouts can identify each side without extra reads. To scope a listing to a specific customer, pass `?customerId=…` to `GET /transactions`. See the [Fiat to Crypto](/payments/guides/fiat-to-crypto) and [Crypto to Fiat](/payments/guides/crypto-to-fiat) guides for full walkthroughs with request/response payloads and webhook events. ### Cash-In Cash-In is a specialized transaction flow for in-person cash deposits at retail locations. Instead of the two-step quote/transaction flow, the developer creates a cash-in that generates a deposit code. The customer takes that code to a retail location, deposits cash, and OMS automatically converts it to crypto and delivers it to the destination wallet. 1. **Create.** `POST /cash-ins` with a customer, a cash source (location), and a crypto destination. OMS returns a deposit code valid for 1 hour. 2. **Deposit.** The customer presents the code at the location and deposits any amount of cash. 3. **Convert.** OMS receives the deposit, converts to the destination asset, and delivers crypto. The cash-in status moves to `completed` and a transaction record is auto-created. The code can be refreshed via `POST /cash-ins/{id}/refresh` if it expires before the customer deposits. No source amount is specified at creation: the customer decides how much to deposit at the counter. The creation response includes `pricing` estimates so the developer can show the customer the expected amounts before they visit the location. See the [Cash-In](/api-reference/guide-cash-in) guide for a full walkthrough. *** ## Deposit Addresses Deposit Addresses are long-lived deposit configurations for crypto sources. Instead of the two-step quote/transaction flow, a deposit address monitors an onchain deposit address for incoming crypto. When funds arrive, OMS automatically creates and executes a transaction that pays out to a registered bank account. Deposit Addresses do not expire: once provisioned, OMS keeps them active for reuse across deposits. Create one with `POST /deposit-addresses`, passing the `customerId`, the expected inbound crypto (`expectedSourceAsset`, `expectedSourceNetwork`), and a registered bank external account as the `destination`. Manage with `GET /deposit-addresses` (the `customerId` filter is optional), `GET /deposit-addresses/{depositAddressId}`, and `PATCH /deposit-addresses/{depositAddressId}`; re-pointing the destination to a healthy external account recovers an address from `inactiveActionRequired` back to `active`. There is no delete operation. | Feature | POST /quotes + POST /transactions | Deposit Addresses | | --------------------- | --------------------------------- | ----------------------------------------------------------- | | Amount known upfront? | Yes: locked at creation | No: determined when funds arrive | | Source | OMS wallet or card | Incoming crypto | | Destination | Wallet, bank, card, or cash | Registered bank external account | | Lifecycle | One-shot | Persistent: reusable across deposits | | Fees and pricing | Top-level `pricing` object | Top-level `pricing` object on each auto-created transaction | `depositInstructions` is null on the create response and populates with the onchain inlet address once provisioning completes; re-fetch the address to read it. In sandbox, `POST /deposit-addresses/{id}/simulate` simulates an inbound deposit so you can test the auto-created transaction flow. Deposit addresses must be enabled for your project. [Contact us](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) to enable them. See the [Deposit Addresses](/payments/guides/deposit-addresses) guide for a full walkthrough. *** ## Virtual Accounts Virtual Accounts give each customer a dedicated bank account number. When fiat arrives via the assigned bank rail, OMS automatically creates a transaction that converts it to the specified crypto asset and delivers it to the configured wallet. Create one with `POST /virtual-accounts`, passing the `customerId`, a `source` (`{ "asset": "usd", "network": ... }`), a wallet `destination` (`walletOms` or a registered `walletExternal`), `accountHolder: "customer"`, and `type: "bankUs"`. Manage with `GET /virtual-accounts` (the `customerId` filter is optional), `GET`/`PATCH /virtual-accounts/{virtualAccountId}`, and `DELETE /virtual-accounts/{virtualAccountId}`. Deletion is asynchronous: the delete returns `202`, `deletionRequestedAt` is set, and the status finalizes to `deleted` once the underlying account closes. `bankDetails` is null until the underlying deposit account is provisioned. In sandbox, `POST /virtual-accounts/{id}/simulate` simulates an inbound bank deposit. | Feature | Deposit Addresses | Virtual Accounts | | --------------- | -------------------------- | ------------------------------------------------- | | Sources | Crypto (onchain deposits) | Bank only (fiat rail) | | Deposit details | Blockchain address | Dedicated bank account per customer | | Use case | Crypto in, bank payout out | Persistent bank account number for fiat-to-crypto | | ID prefix | `da_` | `va_` | See the [Virtual Accounts](/payments/guides/virtual-accounts) guide for a full walkthrough. *** ## Transaction Lifecycle Every transaction moves through a predictable set of statuses. The top-level `status` field is designed for programmatic branching, while the optional `subStatus` field provides finer operational detail. **Transaction statuses:** | Status | Meaning | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `processing` | Transaction created. Funds being pulled, execution underway. | | `awaitingAction` | Non-terminal. Blocked on developer, upstream, or compliance action (see the `hold` object for the reason and deadline). Returns to `processing` once cleared. | | `completed` | Funds delivered to destination. | | `failed` | Terminal failure. The `error` object describes what went wrong. | **Quote statuses:** | Status | Meaning | | ---------- | ----------------------------------------------------------------- | | `open` | Quote created with locked pricing. Awaiting transaction creation. | | `accepted` | Quote accepted. Transaction has been created from this quote. | | `expired` | Pricing expired before transaction creation. Create a new quote. | **Auto-created transactions** from Deposit Addresses, Virtual Accounts, and Cash-Ins skip the Quote step: they go directly to `processing` since there is no developer review step. **Sub-statuses** (`subStatus`) provide operational granularity as status-scoped strings namespaced by their parent (for example `processing.fundsPulled`, `processing.cashPickupReady`, or `completed.cashPickupExpired`). Cash off-ramp lifecycle events surface here rather than as a top-level status. Developers can safely ignore `subStatus` and branch only on `status`. **Webhook events** fire on every meaningful state change. Payloads include the full object, so polling is rarely necessary. Subscribe to the event types you want in the OMS Dashboard or via the Webhooks endpoints (`POST`/`GET` `/webhooks` and `GET`/`PATCH`/`DELETE` `/webhooks/{id}`); omit the events list or pass `["*"]` to receive all events. The full list of event types and the delivery envelope are documented in the [webhook events catalog](/api-reference/webhook-events). *** ## Key Concepts ### Wallets OMS provides both **custodial** and **non-custodial** wallets, so you can pick the custody model that fits your product. * **Custodial wallets** are created and managed through the OMS API (`POST /customers/{id}/wallets`). OMS holds the keys and executes all operations as server-to-server API calls, no user signing required. Each wallet holds a single asset on one chain and has an on-chain address; read its balance with `GET /wallets/{id}/balance`. * **Non-custodial wallets** put key control in the user's hands and are provided by Polygon's embedded wallet infrastructure. They sit alongside the OMS API and are useful for consumer apps that need user-signed transactions or self-custody. See the [Wallets section](/wallets/custodial-wallets) for the full custody options. ### Customers Customers represent the end users in your application. Each customer owns one or more wallets, and can hold deposit addresses, virtual accounts, counterparties, and external accounts. Customers are assigned endorsements that determine which features and financial operations they can access. Endorsements include `basic` (standard operations), `cryptoCustody` (crypto custody and advanced features), and `usd` (USD stablecoin operations). ### Assets and Networks Every side of a transaction has an **asset** (what kind of money) and a **network** (how it moves). The same field structure works for both crypto and fiat. All values are lowercase. | Asset | Network | Meaning | | ------ | ---------- | --------------------------------------------------------- | | `usdc` | `polygon` | USDC on Polygon Chain | | `usdc` | `ethereum` | USDC on Ethereum mainnet | | `usd` | `ach` | US dollars via ACH bank transfer | | `usd` | `wire` | US dollars via domestic wire transfer | | `usd` | `card` | US dollars via debit card | | `usd` | `cash` | US dollars via physical cash at retail locations and ATMs | For the documented list of supported assets and networks, see the [Currencies and rails](/payments/core-concepts/currencies-and-rails) reference page. ### Fees and Pricing Economics live in a single top-level `pricing` object on both quotes and transactions. `pricing.source` and `pricing.destination` each report `amountGross` (before fees), `amountNet` (after fees), and a `feesDeducted` object with `total`, `developer`, `oms`, and `gas` components, all denominated in that side's asset. `pricing` also carries the rate pair (`exchangeRate`, `effectiveRate`), the asset `pair`, `fixedAmountSide` (which side the caller specified), `sponsorGas`, and `sponsorGasCost`. The core equation is: `pricing.source.amountNet × pricing.exchangeRate = pricing.destination.amountGross`. Developers who want to cover gas for their users can set `sponsorGas: true` on the quote request, which moves gas costs to `pricing.sponsorGasCost` (an out-of-band developer cost). ### External Accounts External accounts represent off-platform funding and payout endpoints (bank accounts, debit cards, external wallets) that you reference by ID when creating a quote or transaction. Register them with `POST /external-accounts`: pass an `owner` (`{ "kind": "customer", "customerId": ... }` or `{ "kind": "counterparty", "counterpartyId": ... }`), a `type` (`bankUs`, `bankIban`, `bankCanada`, `card`, or `walletExternal`), and exactly one per-type object matching the type (for example `bankUs: { accountNumber, routingNumber, accountType, bankName }`). Sensitive values are write-only; responses return `accountNumberLast4` or `cardNumberLast4`. List with `GET /external-accounts?customerId=...` (required), update `label`/`metadata` with `PATCH`, and remove with `DELETE`. External accounts use type-specific ID prefixes: `ext_card_` (debit cards), `ext_bankIban_` (IBAN banks), `ext_bankUs_` (US banks), `ext_bankCa_` (Canadian banks), `ext_wlt_` (external wallets). ### Counterparties A counterparty is a third party a customer transacts with: an entry in the customer's address book, such as a vendor you pay. Create one with `POST /counterparties` (requires `customerId` and `name`; optional `entityType`, contact fields, and an `address` of `streetAddress`, `city`, `postalCode`, `country`). Counterparties can own external accounts through the `owner` field on registration. Deleting a counterparty that still owns active external accounts returns `409`. *** ## Get started Get access, exchange your API key for a bearer token, then start calling endpoints. The OMS API is in early access. API keys are granted on request, so request access and our team will reach out with sandbox and production credentials. Share your use case and we'll get you set up with API access. Once approved, open the OMS Dashboard, go to **API Keys**, and generate a key. A key is a name and secret pair; the secret is shown only once, so store it immediately. You do not send the API key directly on requests. Exchange the key and secret for a short-lived bearer token at `POST /auth/token`, then send that token as `Authorization: Bearer {accessToken}` on every other endpoint. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/auth/token \ -H "Content-Type: application/json" \ -d '{ "apiKey": "{api_key}", "apiSecret": "{api_secret}" }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/auth/token \ -H "Content-Type: application/json" \ -d '{ "apiKey": "{api_key}", "apiSecret": "{api_secret}" }' ``` The response returns an `accessToken` valid for 60 minutes. When a request returns `401`, the token has expired: request a new one and retry. The token endpoint is rate-limited per client IP; if it returns `429`, wait for the interval in the `Retry-After` response header before retrying. With a token in hand, create your first quote or browse the full endpoint reference: Zero to your first transaction, step by step. A full quote-to-transaction walkthrough with payloads and webhook events. For a step-by-step walkthrough from zero to your first transaction, see the [Get started guide](/payments/get-started). # Create a payment quote Source: https://docs.polygon.technology/api-reference/quote/create-a-payment-quote /api-reference/openapi.yaml post /quotes Creates a payment quote with a locked exchange rate and fee breakdown for a prospective transfer. Quotes expire after a short window; reference the returned quote ID when executing the transaction. Pass an Idempotency-Key header to safely retry. # Get quote by ID Source: https://docs.polygon.technology/api-reference/quote/get-quote-by-id /api-reference/openapi.yaml get /quotes/{quoteId} Retrieves a previously created quote by ID, including its locked rate, fee breakdown, and expiry. # Simulate cash-in auth commit Source: https://docs.polygon.technology/api-reference/sandbox/simulate-cash-in-auth-commit /api-reference/openapi.yaml post /cash-ins/simulate/auth-commit Commit a previously-authorized simulated cash-in transaction (sandbox only). # Simulate cash-in auth void Source: https://docs.polygon.technology/api-reference/sandbox/simulate-cash-in-auth-void /api-reference/openapi.yaml post /cash-ins/simulate/auth-void Void a previously-authorized simulated cash-in transaction (sandbox only). # Simulate cash-in barcode auth Source: https://docs.polygon.technology/api-reference/sandbox/simulate-cash-in-barcode-auth /api-reference/openapi.yaml post /cash-ins/simulate/barcode-auth Authorize a barcode load against the simulated cash-in flow (sandbox only). # Simulate inbound transfer on a Deposit Address Source: https://docs.polygon.technology/api-reference/simulation/simulate-inbound-transfer-on-a-deposit-address /api-reference/openapi.yaml post /deposit-addresses/{depositAddressId}/simulate Simulate an inbound stablecoin (blockchain) transfer against a Deposit Address, for testing webhook and reconciliation flows. Sandbox / non-production only; returns 404 in production-live. The amount currency and network are resolved server-side from the Deposit Address; the response fields are documented on the corresponding schema. # Simulate inbound transfer on a Virtual Account Source: https://docs.polygon.technology/api-reference/simulation/simulate-inbound-transfer-on-a-virtual-account /api-reference/openapi.yaml post /virtual-accounts/{virtualAccountId}/simulate Simulate an inbound fiat transfer against a Virtual Account, for testing webhook and reconciliation flows. Sandbox / non-production only; returns 404 in production-live. Set the `rail` field in the request body to choose the transfer type (`ach_in`, `wire_in`, or `swift_in`); the request and response fields for each rail are documented on the corresponding schema. # Execute a transaction (send) Source: https://docs.polygon.technology/api-reference/transaction/execute-a-transaction-send /api-reference/openapi.yaml post /transactions Executes a transfer by accepting an open quote. Reference the quote by its ID; OMS pulls funds from the quote's source and delivers them to its destination. Pass an Idempotency-Key header to safely retry without sending twice. # Get transaction by ID Source: https://docs.polygon.technology/api-reference/transaction/get-transaction-by-id /api-reference/openapi.yaml get /transactions/{transactionId} Retrieves a single transaction by ID. # List transactions with filters Source: https://docs.polygon.technology/api-reference/transaction/list-transactions-with-filters /api-reference/openapi.yaml get /transactions List transactions with filters. Results are ordered newest-first (by createdAt desc, id desc). `limit` defaults to 50 and is capped at 100. Date filters `createdAfter` and `createdBefore` are **inclusive** (treated as `>=` and `<=` against `createdAt`). Pagination returns opaque `nextCursor` / `previousCursor` tokens in the response whenever the page is non-empty: use `startingAfter=nextCursor` to fetch the next page, or `endingBefore=previousCursor` to page backward. `hasMore` tells the client whether more rows exist in the current direction of travel; an empty response in the other direction signals the start of the list. # Create a Virtual Account Source: https://docs.polygon.technology/api-reference/virtualaccount/create-a-virtual-account /api-reference/openapi.yaml post /virtual-accounts Create a Virtual Account for a customer. Partner must have virtual_account_provider configured. # Delete a Virtual Account Source: https://docs.polygon.technology/api-reference/virtualaccount/delete-a-virtual-account /api-reference/openapi.yaml delete /virtual-accounts/{virtualAccountId} Delete a customer's Virtual Account. Initiates the close flow against the underlying deposit account; the VA enters the delete-pending window (status remains as-is, `deletionRequestedAt` set) and finalizes to `deleted` once the underlying account is closed. # Get a Virtual Account Source: https://docs.polygon.technology/api-reference/virtualaccount/get-a-virtual-account /api-reference/openapi.yaml get /virtual-accounts/{virtualAccountId} Fetch a Virtual Account by ID. # List all Virtual Accounts Source: https://docs.polygon.technology/api-reference/virtualaccount/list-all-virtual-accounts /api-reference/openapi.yaml get /virtual-accounts List all Virtual Accounts across the organization. Spans every customer in the caller's organization; optionally filter by status and/or customer. # Update a Virtual Account Source: https://docs.polygon.technology/api-reference/virtualaccount/update-a-virtual-account /api-reference/openapi.yaml patch /virtual-accounts/{virtualAccountId} Update a Virtual Account. Patchable fields: `destination` (re-point), `sponsorGas`, `label`, `metadata`; any other JSON key is rejected with 400. Re-pointing `destination` to a healthy External Account recovers a VA from `inactiveActionRequired` back to `active`. # Create a new wallet asset for a customer Source: https://docs.polygon.technology/api-reference/wallet/create-a-new-wallet-asset-for-a-customer /api-reference/openapi.yaml post /customers/{id}/wallets Provisions a crypto wallet for a customer for the requested asset and chain, returning the created wallet. Pass an Idempotency-Key header to safely retry without provisioning duplicates. # Get current balance of a specific wallet Source: https://docs.polygon.technology/api-reference/wallet/get-current-balance-of-a-specific-wallet /api-reference/openapi.yaml get /wallets/{id}/balance Returns the current balance of a specific wallet, including the display balance and its estimated value in the currency requested via `estimatedBalanceCurrencyCode` (defaults to USD). # List customer wallets Source: https://docs.polygon.technology/api-reference/wallet/list-customer-wallets /api-reference/openapi.yaml get /customers/{id}/wallets Lists the crypto wallets provisioned for a customer (one per asset/chain), with cursor-based pagination. Returns an empty list if none have been provisioned yet. # List wallet transactions Source: https://docs.polygon.technology/api-reference/wallet/list-wallet-transactions /api-reference/openapi.yaml get /accounts/{id}/transactions Returns the transaction history for the specified wallet, most recent first, with cursor-based pagination and optional type/date filters. Deprecated: prefer `GET /transactions?walletId=`, which adds the full transaction-filter set. It is not yet a drop-in replacement: the `walletId` filter currently matches the wallet as the transaction *source* only (no inbound / either-side filter yet) and returns the transaction ledger shape rather than this per-wallet account ledger, so inbound history is not covered. This route remains available until that gap closes and will be removed in a future major version. # Webhook events Source: https://docs.polygon.technology/api-reference/webhook-events The catalog of webhook events OMS delivers, and the envelope every event arrives in. OMS delivers webhooks for every meaningful state change. Subscribe with `POST /webhooks`, passing a `url` and the `events` you want; omit the list or pass `["*"]` to receive everything. Verify every delivery with the `Webhook-Signature` header before processing, and use the envelope's `eventId` to handle retries idempotently. This page lists every event you can receive and the envelope it arrives in. ## Event envelope Every event shares one envelope. The resource that changed is carried in full under `payload`, so you rarely need a follow-up read. | Field | Type | Description | | ----------------- | --------- | --------------------------------------------------------------------------------------------------------- | | `eventId` | string | Unique, stable ID for this event. Use it to deduplicate retried deliveries. | | `eventName` | string | The event type from the catalog below. | | `schemaVersion` | integer | Envelope schema version. | | `producerService` | string | The OMS service that produced the event. | | `orgId` | string | Your organization. | | `projectId` | string | The project the event belongs to. Optional. | | `customerId` | string | The customer the resource belongs to, when applicable. Optional. | | `mode` | string | The environment the event was produced in. Optional. | | `resourceType` | string | The kind of resource in `payload` (for example `cashIn`, `virtualAccount`, `transaction_crypto_to_fiat`). | | `resourceId` | string | The ID of the resource in `payload`. | | `sequence` | integer | Monotonic sequence for ordering events about the same resource. Optional. | | `occurredAt` | timestamp | When the event occurred. | | `correlationId` | string | Correlates events produced by the same logical operation. Optional. | | `causationId` | string | The event that caused this one, when applicable. Optional. | | `payload` | object | The full resource object at the time of the event. | ```json theme={null} { "eventId": "evt_01H9Xz4M2k7vP3qR8t", "eventName": "transaction.cryptoToFiat.completed", "schemaVersion": 1, "producerService": "api", "orgId": "org_01H9Xo...", "projectId": "prj_01H9Xp...", "customerId": "cst_01H9Xa...", "mode": "production", "resourceType": "transaction_crypto_to_fiat", "resourceId": "txn_01H9Xd...", "sequence": 42, "occurredAt": "2026-07-08T12:00:00Z", "payload": { "id": "txn_01H9Xd...", "object": "transaction", "status": "completed", "...": "..." } } ``` The resource's `status` lives inside `payload`, not on the envelope: branch on `payload.status` (or the event name itself) rather than expecting envelope-level status fields. ## Transactions Transaction events are namespaced by transfer type. Refund events fire when a failed transaction's funds are returned (see [Returns and refunds](/payments/guides/crypto-to-fiat#returns-and-refunds)). | Event | Fires when | | -------------------------------------------- | ----------------------------------------------------------- | | `transaction.fiatToCrypto.processing` | A fiat-funded transaction starts executing. | | `transaction.fiatToCrypto.completed` | Crypto delivered to the destination. | | `transaction.fiatToCrypto.failed` | The transaction failed; see `payload.error`. | | `transaction.fiatToCrypto.refundCompleted` | The refund for a failed transaction was delivered. | | `transaction.fiatToCrypto.refundFailed` | The refund itself failed; operator follow-up. | | `transaction.cryptoToFiat.processing` | A crypto-funded payout starts executing. | | `transaction.cryptoToFiat.cashPickupReady` | Cash payout only: the pickup code was issued. | | `transaction.cryptoToFiat.codeExpired` | Cash payout only: the pickup code expired uncollected. | | `transaction.cryptoToFiat.completed` | Fiat delivered to the destination. | | `transaction.cryptoToFiat.failed` | The payout failed; see `payload.error`. | | `transaction.cryptoToFiat.refundCompleted` | The stablecoin re-credit for a failed payout was delivered. | | `transaction.cryptoToFiat.refundFailed` | The re-credit itself failed; operator follow-up. | | `transaction.cryptoToCrypto.processing` | A crypto-to-crypto send starts executing. | | `transaction.cryptoToCrypto.completed` | Crypto delivered to the destination. | | `transaction.cryptoToCrypto.failed` | The send failed; see `payload.error`. | | `transaction.cryptoToCrypto.refundCompleted` | The refund for a failed send was delivered. | | `transaction.cryptoToCrypto.refundFailed` | The refund itself failed; operator follow-up. | ## Cash-in | Event | Fires when | | ------------------ | -------------------------------------------------------------------------------------------- | | `cashIn.created` | A cash-in code was issued. | | `cashIn.completed` | The cash was deposited and converted; the auto-created transaction is linked on the cash-in. | | `cashIn.expired` | The code expired before a deposit was made. | ## Virtual accounts | Event | Fires when | | ----------------------------------------- | --------------------------------------------------------------------------- | | `virtualAccount.created` | The virtual account record was created. | | `virtualAccount.provisioned` | The underlying deposit account was provisioned; `bankDetails` is populated. | | `virtualAccount.active` | The account is live and accepting deposits. | | `virtualAccount.frozen` | Deposits are suspended. | | `virtualAccount.closed` | The account reached end of life. | | `virtualAccount.deleted` | An asynchronous delete finalized. | | `virtualAccount.failed` | Provisioning failed. | | `virtualAccount.deposit.pending` | An inbound bank deposit was detected and is settling. | | `virtualAccount.deposit.settled` | The inbound deposit settled. | | `virtualAccount.deposit.failed` | The inbound deposit failed. | | `virtualAccount.deposit.returned` | The inbound deposit was returned by the bank. | | `virtualAccount.cryptoTransfer.initiated` | The converted crypto leg was initiated toward the destination wallet. | | `virtualAccount.cryptoTransfer.pending` | The crypto leg is in flight. | | `virtualAccount.cryptoTransfer.settled` | The crypto leg was delivered. | | `virtualAccount.cryptoTransfer.failed` | The crypto leg failed. | ## Deposit addresses Lifecycle events use the `depositAddress.` prefix; per-deposit and per-payout legs use the `deposit_address.` prefix. | Event | Fires when | | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | `depositAddress.created` | The deposit address record was created. | | `depositAddress.active` | The inlet address is provisioned and monitoring; `depositInstructions` is populated. | | `depositAddress.paused` / `depositAddress.resumed` | Monitoring was paused or resumed. | | `depositAddress.frozen` | Deposits are held pending review. | | `depositAddress.closed` | The address reached end of life. | | `depositAddress.deleted` | The record was removed. | | `depositAddress.failed` | Provisioning failed. | | `deposit_address.crypto_deposit.pending` | An inbound on-chain deposit was detected. | | `deposit_address.crypto_deposit.settled` | The inbound deposit settled. | | `deposit_address.crypto_deposit.failed` | The inbound deposit failed. | | `deposit_address.crypto_deposit.needs_attribution` | The deposit came from an unrecognized sender and is held for [sender attribution](/payments/guides/deposit-addresses#held-deposits). | | `deposit_address.ach_payout.initiated` / `.pending` / `.settled` / `.failed` / `.returned` | The ACH payout leg progressed. | | `deposit_address.wire_payout.initiated` / `.pending` / `.settled` / `.failed` / `.returned` | The domestic wire payout leg progressed. | | `deposit_address.intl_wire_payout.initiated` / `.pending` / `.settled` / `.failed` / `.returned` | The international wire payout leg progressed. | ## External accounts | Event | Fires when | | -------------------------- | --------------------------------------------------- | | `externalAccount.created` | The account was registered and is being validated. | | `externalAccount.verified` | Validation passed; the account is usable on quotes. | | `externalAccount.declined` | Validation failed. | | `externalAccount.deleted` | The account was removed. | ## Endorsements | Event | Fires when | | --------------------- | --------------------------------------------------------------------------- | | `endorsement.updated` | A customer endorsement changed state. | | `endorsement.active` | An endorsement became active; the customer can use the operations it gates. | ## Wallets | Event | Fires when | | ------------------------ | ------------------------------------------------------------------------- | | `wallet.provisioned` | A custodial wallet's on-chain address was provisioned. | | `wallet.depositDetected` | Crypto arrived at a wallet address outside any OMS-initiated transaction. | ## Delivery and verification * Deliveries are signed with the `Webhook-Signature` header (`t=,v1=`, an HMAC-SHA256 over `.` with your per-endpoint signing secret). Verify before processing and reject stale timestamps. * Deliveries can arrive more than once and, across different resources, out of order. Deduplicate on `eventId` and use `sequence` to order events about the same resource. * Manage subscriptions with the [Webhooks endpoints](/api-reference/overview): `POST`/`GET /webhooks`, `GET`/`PATCH`/`DELETE /webhooks/{id}`. # Create a webhook Source: https://docs.polygon.technology/api-reference/webhook/create-a-webhook /api-reference/openapi.yaml post /webhooks Registers a new HTTPS endpoint to receive event deliveries. OMS generates a signing secret (`whsec_` prefix) that is returned **once** in this response and never again - store it in your secrets manager immediately. The endpoint starts receiving deliveries immediately after creation. # Delete a webhook Source: https://docs.polygon.technology/api-reference/webhook/delete-a-webhook /api-reference/openapi.yaml delete /webhooks/{id} Permanently removes a webhook endpoint. Any in-flight or queued deliveries for this endpoint are abandoned. There is no soft-delete - use `enabled: false` if you want to pause deliveries temporarily. # Get a webhook Source: https://docs.polygon.technology/api-reference/webhook/get-a-webhook /api-reference/openapi.yaml get /webhooks/{id} Returns a single webhook endpoint by its `whk_`-prefixed ID. The `secret` field is never included - use the value stored at creation time. # List webhooks Source: https://docs.polygon.technology/api-reference/webhook/list-webhooks /api-reference/openapi.yaml get /webhooks Returns a paginated list of all webhook endpoints registered for your organization. The `secret` field is never included in list responses. Supports cursor-based pagination via `limit` (default 10, max 100), `startingAfter` (cursor for the next page), and `endingBefore` (cursor for the previous page). # Update a webhook Source: https://docs.polygon.technology/api-reference/webhook/update-a-webhook /api-reference/openapi.yaml patch /webhooks/{id} Partially updates a webhook. All fields are optional - only supplied fields are modified. To pause deliveries without deleting the endpoint, set `enabled` to `false`. To resume, set it back to `true`. The `secret` is not returned in update responses. # Polygon CDK FAQs Source: https://docs.polygon.technology/chain-development/cdk/additional-resources/faqs Frequently asked questions about Polygon CDK, covering execution clients, rollup modes, and Agglayer integration. ## What is the Polygon CDK? Polygon Chain Development Kit (CDK) is the product institutions use to launch their own dedicated, private blockchain with built-in Agglayer connectivity. Polygon partners with you to design and operate a bespoke chain, with privacy controls on a spectrum (private validium, gated access, fully sovereign blockspace) and connection to the broader blockchain ecosystem. CDK supports **op-geth** and **op-reth** execution clients, giving operators flexibility in performance tuning and resource usage. ## What execution clients does CDK support? CDK supports two execution clients: * **op-geth**: Geth-based client with OP Stack architecture and broad tooling support. * **op-reth**: Reth-based client optimized for high throughput and lower resource consumption. Both are supported by Conduit and Gateway for production deployments. ## What is a sovereign chain in CDK? A sovereign chain operates without a prover. Instead, it uses pessimistic proofs via Agglayer to enforce safety and ensure that no chain can withdraw more than it deposits. This enables secure, low-cost execution with fast finality. Sovereign mode is the default configuration. ## What rollup modes are available? Every CDK chain supports three operating modes: * **Sovereign**: Agglayer connectivity secured by pessimistic proofs. No prover required. * **Validium**: ZK-secured execution with offchain data availability via a DAC. * **zkRollup**: Fully onchain ZK rollup for maximum security and Ethereum-aligned trust. ## How does Agglayer fit into Polygon CDK? Agglayer is a native feature of every CDK chain. It enables cross-chain interoperability, unified liquidity, and shared state across the connected network. CDK chains are connected to Agglayer by default. ## How can I start building with CDK? Use the CDK quickstarts to deploy a local testnet. For production-ready deployments, contact [**Conduit**](https://conduitxyz.typeform.com/to/CrvgqEeA?utm_source=polygonannouncement\&utm_medium=partnerblog) or [**Gateway**](https://share.hsforms.com/1toN701PtTBCpyc3bKQBCRAcy6wj?referrer=12118007123). # Glossary Source: https://docs.polygon.technology/chain-development/cdk/additional-resources/glossary Definitions of technical terms used throughout the Polygon CDK documentation. ### Agglayer Agglayer is a cross-chain settlement layer that connects the liquidity and users of any blockchain for fast, low-cost interoperability and growth. It enables trustless bridging, shared messaging, and unified state across Layer 2s using zero-knowledge (ZK) proofs. [Visit agglayer.dev](https://www.agglayer.dev/) ### Polygon CDK (Chain Development Kit) An enterprise-grade toolkit for building custom Ethereum Layer 2 chains. Each CDK chain is natively connected to Agglayer for cross-chain interoperability, shared liquidity, and integrated messaging. CDK supports **op-geth** and **op-reth** execution clients. ### Chain Operator An individual, team, or DAO responsible for launching and managing a CDK-based chain. Operators may handle sequencing, bridge operations, infrastructure deployment, data availability configuration, and other network responsibilities. ### Data Availability (DA) The requirement that transaction data remains accessible to Layer 1 validators for verifying off-chain execution. DA ensures the security of modular rollups. CDK chains can use onchain DA (e.g., Ethereum), off-chain DACs, or local solutions. ### Data Availability Committee (DAC) A decentralized set of nodes that ensures off-chain data availability for CDK chains using Validium mode. DAC nodes fetch transaction data, verify it, sign it, and store it for later retrieval. ### Implementation Providers (IPs) External infrastructure teams that assist developers with launching and maintaining CDK chains. Conduit and Gateway both support op-reth and op-geth execution clients and are building open source tooling for CDK deployments. ### Rollups Layer 2 solutions that execute transactions off-chain and post state data to Ethereum. CDK supports multiple rollup types, including: * [Optimistic rollups](https://ethereum.org/en/developers/docs/scaling/optimistic-rollups/) * [ZK rollups](https://ethereum.org/en/developers/docs/scaling/zk-rollups/) * Validium (ZK with off-chain DA) * Sovereign (non-ZK with pessimistic proofs) ### Sovereign Mode A rollup configuration that does not use a ZK prover. Instead, Agglayer enforces security through pessimistic proofs, ensuring that no chain can withdraw more than it deposits. Default configuration for CDK chains. ### Unified Bridge The shared bridge infrastructure for CDK chains, providing secure cross-chain asset transfers and messaging. It supports shared escrow, sovereign and ZK rollup flows, and native Agglayer integration. ### Unified Escrow A component of the Unified Bridge that holds tokens either bridged from Ethereum or minted natively on Layer 2. Ensures consistent accounting and solvency across all CDK chains. ### Validium A rollup configuration that uses ZK proofs for execution but stores transaction data off-chain, reducing costs and increasing throughput. CDK Validium chains rely on DACs for off-chain data availability. [Learn more about Validium](https://ethereum.org/en/developers/docs/scaling/validium/) ### ZK Proofs Zero-knowledge proofs validate computations without revealing input data. Agglayer uses ZK proofs to provide cryptographic guarantees for execution, enabling fast finality and trustless security across chains. # Deployment Modes Source: https://docs.polygon.technology/chain-development/cdk/cdk-opgeth/architecture Component reference for the three CDK deployment modes: sovereign, validium, and zkrollup. The three `cdk-opgeth` deployment modes differ primarily in data availability and prover setup. All share the same Geth-based client and OP Stack components. ### `cdk-opgeth-sovereign` CDK-opgeth Sovereign | Component | Description / Link | | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Execution Layer | [OP Geth Client](https://github.com/ethereum-optimism/op-geth): Ethereum client modified for Optimism | | Consensus Layer | [OP Node](https://github.com/ethereum-optimism/optimism): Handles block production and synchronisation | | AggKit - Oracle | [AggOracle](https://github.com/agglayer/aggkit): Updates Global Exit Root (GER) onchain | | AggKit - Sender | Sends certificates from the chain to Agglayer | | Bridge API | [zkevm-bridge-service](https://github.com/0xPolygonHermez/zkevm-bridge-service): Enables messaging between chains | | Data Availability Layer | [OP Batcher](https://github.com/ethereum-optimism/optimism): Sends transaction data to Ethereum Mainnet (Layer 1) | | Agglayer Network | [Agglayer](https://github.com/agglayer/agglayer), Agglayer Node, Agglayer Prover | | Smart Contracts (L1 + L2) | [Optimism Contracts](https://github.com/ethereum-optimism/optimism/releases/tag/op-deployer%2Fv0.0.11) | | Ethereum Bridge Contracts | [Polygon zkEVM Contracts](https://github.com/0xPolygonHermez/zkevm-contracts): Manages final settlement on Ethereum | ### `cdk-opgeth-zkrollup` CDK-opgeth-zkrollup | Component | Description / Link | | ------------------------- | ------------------------------------------------------------------------------------------------------ | | Execution Layer | [OP Geth Client](https://github.com/ethereum-optimism/op-geth) | | Consensus Layer | [OP Node](https://github.com/ethereum-optimism/optimism) | | Proposer Service | [OP Proposer](https://github.com/ethereum-optimism/optimism) | | AggKit - Oracle | [AggOracle](https://github.com/agglayer/aggkit) | | AggKit - Sender | Sends certificates to Agglayer | | Bridge API | [zkevm-bridge-service](https://github.com/0xPolygonHermez/zkevm-bridge-service) | | Data Availability Layer | Ethereum Mainnet (onchain data only) | | Agglayer Network | [Agglayer](https://github.com/agglayer/agglayer), Agglayer Node, Agglayer Prover | | Smart Contracts (L1 + L2) | [Optimism Contracts](https://github.com/ethereum-optimism/optimism/releases/tag/op-deployer%2Fv0.0.11) | | Ethereum Bridge Contracts | [Polygon zkEVM Contracts](https://github.com/0xPolygonHermez/zkevm-contracts) | | Prover Network | [SP1 Prover](https://github.com/succinctlabs/sp1): zkVM-based prover | ### `cdk-opgeth-validium` This mode shares the same architecture as `zkrollup`, but uses an alternative data availability (DA) layer. | Component | Description / Link | | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Execution Layer | [OP Geth Client](https://github.com/ethereum-optimism/op-geth) | | Consensus Layer | [OP Node](https://github.com/ethereum-optimism/optimism) | | Proposer Service | [OP Proposer](https://github.com/ethereum-optimism/optimism): Proposes blocks and batches | | AggKit - Oracle | [AggOracle](https://github.com/agglayer/aggkit) | | AggKit - Sender | Sends certificates to Agglayer | | Bridge API | [zkevm-bridge-service](https://github.com/0xPolygonHermez/zkevm-bridge-service) | | Data Availability Layer | [Alt-DA Mode (TBD)](https://docs.optimism.io/stack/beta-features/alt-da-mode): Off-chain or alternative DA provider | | Agglayer Network | [Agglayer](https://github.com/agglayer/agglayer), Agglayer Node, Agglayer Prover | | Smart Contracts (L1 + L2) | [Optimism Contracts](https://github.com/ethereum-optimism/optimism/releases/tag/op-deployer%2Fv0.0.11) | | Ethereum Bridge Contracts | [Polygon zkEVM Contracts](https://github.com/0xPolygonHermez/zkevm-contracts) | | Prover Network | [SP1 Prover](https://github.com/succinctlabs/sp1): zkVM-based prover | # Devnet Deployment Guide Source: https://docs.polygon.technology/chain-development/cdk/cdk-opgeth/devnet-deployment-guide How to deploy a production-like Pessimistic Proof (cdk-opgeth-sovereign) devnet. Use this guide to deploy a production-like **Pessimistic Proof (PP)** (`cdk-opgeth-sovereign`) devnet. *** ## Prerequisites Before starting, ensure you have: * A valid L1 RPC URL * A wallet address with Sepolia testnet funds (deployer) * [Docker](https://www.docker.com/) (or any other container orchestrator of your choice) * [Cast](https://book.getfoundry.sh/cast/) * [Polycli](https://github.com/0xPolygon/polygon-cli) * [Reference Repo](https://github.com/0xPolygon/cdk-devnets/tree/main/cdk-opgeth-pp) (configuration files used in the guide) Export these variables: ```bash theme={null} export L1_RPC_URL=https://... export SEPOLIA_PROVIDER=${L1_RPC_URL} export DEPLOYER_PRIVATE_KEY=0x0000000000000000000000000000000000000000 ``` The values shown are specific to this example. Replace them with values relevant to your own setup. *** ## Rollup Network Creation ### Step 1: Submit a Request To initiate the rollup creation: 1. The **Implementation Provider (IP)** must submit a request to Polygon Labs. 2. Use the [Polygon Support Portal](https://polygon.atlassian.net/servicedesk/customer/portal/22) to raise a support ticket. #### Required Parameters | Parameter | Description | Example | | ----------------- | --------------------------------------------- | -------------------------------------------- | | Rollup Type | Type of rollup | `Pessimistic Proofs (PP)` | | Chain ID | Unique identifier for your rollup | `473` | | Admin Address | Your control address for rollup modifications | `0xBe46896822BD1d415522F3a5629Fe28447b95563` | | Sequencer Address | Used by AggKit, must be under your control | `0x0769fcb9ca9369b0494567038E5d1f27f0CBE0aC` | | Gas Token Address | Token or zero address | `0x0000000000000000000000000000000000000000` | | Sequencer URL | *(Not used for PP)* | `https://...` | | Datastream URL | *(Not used for OP)* | `https://...` | | Network Name | Final network name (non-editable) | `bali-36-op` | *** ### Step 2: Setup by Polygon Labs Once approved: * Polygon Labs provisions your Rollup. * A transaction is recorded (e.g., [example](https://sepolia.etherscan.io/tx/0x111618eedb16b416aef393db6dd2d73d5a190dd5e15bdaa704473ba89a497f92)). You will receive: * `combined.json`: Core deployment details * `genesis-base.json`: Needed for genesis generation Store these files securely. They are required for further deployment steps. *** ## Genesis File Generation ### Option Selection Choose one: * **Option 1 (Recommended)**: Merge OP + Polygon Genesis with pre-deployed contracts. * **Option 2**: Manual L2 contract deployment. This guide focuses on **Option 1**. *** ### Step-by-Step Instructions 1. **Environment Setup** Checkout the Desired Version. We will be using`v10.0.0-rc.7` in this example ```bash theme={null} git clone https://github.com/0xPolygonHermez/zkevm-contracts.git cd zkevm-contracts git checkout v10.0.0-rc.7 ``` Install dependencies as described in the repo's [README](https://github.com/0xPolygonHermez/zkevm-contracts). 2. **Parameter File Creation** ```bash theme={null} cp ./tools/createSovereignGenesis/create-genesis-sovereign-params.json.example ./tools/createSovereignGenesis/create-genesis-sovereign-params.json ``` Update this file with the relevant information from your `combined.json` and your wallet addresses. ```json theme={null} { "rollupManagerAddress": "polygonRollupManagerAddress", "rollupID": rollupID, "chainID": chainID, "gasTokenAddress": "gasTokenAddress", "bridgeManager": "admin address", "sovereignWETHAddress": "0x0000000000000000000000000000000000000000", "sovereignWETHAddressIsNotMintable": false, "globalExitRootUpdater": "aggoracle address", "globalExitRootRemover": "0x0000000000000000000000000000000000000000", "emergencyBridgePauser": "admin address", "setPreMintAccounts": true, "preMintAccounts": [ # add as many as you like { "balance": "1000000000000000000", "address": "admin address" } ], "setTimelockParameters": true, "timelockParameters": { "adminAddress": "admin address", "minDelay": 0 # timelock delay, for devnets it's convinient to set it to zero }, "formatGenesis": "geth" } ``` 3. **Base Genesis Template** ```bash theme={null} cp ./tools/createSovereignGenesis/genesis-base.json.example ./tools/createSovereignGenesis/genesis-base.json ``` Paste the contents of your `genesis-base.json` into this file. 4. **Generate Genesis Files** ```bash theme={null} npx hardhat run ./tools/createSovereignGenesis/create-sovereign-genesis.ts --network sepolia ``` 5. **Rename the Output Files for Clarity** ```bash theme={null} mv ./tools/createSovereignGenesis/genesis-rollupID-*.json ./tools/createSovereignGenesis/polygon-genesis.json mv ./tools/createSovereignGenesis/output-rollupID-*.json ./tools/createSovereignGenesis/polygon-genesis-info.json ``` *** ## Network Deployment ### Environment Variables ```bash theme={null} export CLAIMTX_ADDRESS=0x0e40237b464f9945FDE774a2582109Aa943b9111 export AGGORACLE_ADDRESS=0x94e8844309E40f4FFa9146a7a890077561f925bc export CHAIN_ID=473 ``` The values shown are specific to this example. Replace them with values relevant to your own setup. ### L2 Deployment (Using op-deployer) To set up your Layer 2 (L2) network using the OP Stack, we recommend starting with the [official Optimism L2 Rollup tutorial](https://docs.optimism.io/operators/chain-operators/tutorials/create-l2-rollup). This guide provides a practical reference based on that tutorial and uses the [op-deployer](https://docs.optimism.io/operators/chain-operators/tools/op-deployer) tool to streamline the deployment process. All commands in this section are executed from the root directory of your project. 1. **Initialize deployer Folder** ```bash theme={null} docker run --rm -v $(pwd)/deployer:/deployer -it us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.13 \ init \ --l1-chain-id 11155111 \ --l2-chain-ids "${CHAIN_ID}" \ --workdir /deployer ``` 2. **Edit `intent.toml`** with your parameters. 3. **Deploy L1 Contracts** ```bash theme={null} docker run --rm -v $(pwd)/deployer:/deployer -it us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.13 \ apply \ --workdir /deployer \ --l1-rpc-url ${L1_RPC_URL} \ --private-key ${DEPLOYER_PRIVATE_KEY} ``` 4. **Merge Genesis Files** Next, you'll need to combine the `polygon-genesis.json` file with the OP Stack's `genesis.json`. The recommended approach is to embed the contents of `polygon-genesis.json` directly into the op-deployer state. ```bash theme={null} # extract the allocs cat deployer/state.json | jq -r '.opChainDeployments[].allocs' | base64 -d | gzip -d > allocs.json # merge jq -s add allocs.json files/polygon-genesis.json | gzip | base64 > merge # create a copy of the original state cp deployer/state.json deployer/original-state.json # replace the original allocs by the merged cat deployer/state.json | jq ".opChainDeployments[].allocs=\"$( cat merge )\"" > state.json && mv state.json deployer/state.json # cleanup rm allocs.json merge ``` 5. **Generate Files** ```bash theme={null} docker run --rm -v $(pwd)/deployer:/deployer -it us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.13 \ inspect genesis \ --workdir /deployer "${CHAIN_ID}" > ./deployer/genesis.json docker run --rm -v $(pwd)/deployer:/deployer -it us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.13 \ inspect rollup \ --workdir /deployer "${CHAIN_ID}" > ./deployer/rollup.json ``` *** ## Component Setup ### OP Stack 1. **Create `.env` values** ```bash theme={null} CHAIN_ID=473 SEQUENCER_PRIVATE_KEY=redacted BATCHER_PRIVATE_KEY=redacted L1_RPC_URL_HTTP=https://... L1_RPC_URL_WS=wss://... ``` The values shown are specific to this example. Replace them with values relevant to your own setup. 2. **Generate secret** ```bash theme={null} openssl rand -hex 32 > deployer/jwt.txt ``` 3. **Fund the batcher wallet on L1** 4. **Initialize Data Directory** ```bash theme={null} docker run --rm -it \ -v $(pwd)/deployer/genesis.json:/etc/optimism/genesis.json:ro \ -v $(pwd)/datadir:/datadir \ us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.3 \ init \ --state.scheme=hash \ --datadir=/datadir \ /etc/optimism/genesis.json ``` 5. **Start Services** ```bash theme={null} docker compose up -d op-geth docker compose up -d op-node docker compose up -d op-batcher ``` *** ### Aggkit 1. **Config** Update the `config/aggkit.toml` and `config/bridge.toml` config files with the relevant information from your `combined.json`, `polygon-genesis-info.json` and your wallet addresses. 2. **Create keystore files** The aggkit and bridge services require the sequencer, aggoracle, and claimtx wallet addresses to be provided through an encrypted keystore. ```bash theme={null} mkdir keystore cast wallet import --private-key ${SEQUENCER_PRIVATE_KEY} --keystore-dir .keystore/ sequencer.keystore cast wallet import --private-key ${AGGORACLE_PRIVATE_KEY} --keystore-dir .keystore/ aggoracle.keystore cast wallet import --private-key ${CLAIMTX_PRIVATE_KEY} --keystore-dir .keystore/ claimtx.keystore ``` 3. **Fund ClaimTX & Aggoracle** ```bash theme={null} cast send \ --value 100ether \ --mnemonic "test test test test test test test test test test test junk" \ --rpc-url http://$(docker compose port op-geth 8545) \ "${CLAIMTX_ADDRESS}" cast send \ --value 10ether \ --mnemonic "test test test test test test test test test test test junk" \ --rpc-url http://$(docker compose port op-geth 8545) \ ${AGGORACLE_ADDRESS} ``` 4. **Start PostgreSQL DB** ```bash theme={null} docker compose up -d db ``` 5. **Run Services** ```bash theme={null} docker compose up -d bridge docker compose up -d aggkit ``` *** ## Bridge ### Environment Variables ```bash theme={null} export ROLLUP_ID=36 export BRIDGE_ADDRESS=0x1348947e282138d8f377b467f7d9c2eb0f335d1f export TEST_ADDRESS=0xda9f3DCA867C4Bc7b1f8e2DB47Aa8C338Ba2e056 export TEST_PRIVATE_KEY=redacted ``` The values shown are specific to this example. Replace them with values relevant to your own setup. ### L1 to L2 1. **Initiate the Deposit from L1** Use the following command to bridge assets from L1 to your L2 network: ```bash theme={null} polycli ulxly bridge asset \ --bridge-address ${BRIDGE_ADDRESS} \ --destination-network ${ROLLUP_ID} \ --private-key ${TEST_PRIVATE_KEY} \ --rpc-url ${L1_RPC_URL} \ --value $(cast to-wei 0.1) ``` 2. **Verify the Deposit on L2** Once the deposit is claimed on L2, you can verify it by checking the balance of the target address: ```bash theme={null} cast balance ${TEST_ADDRESS} --ether --rpc-url=http://$(docker compose port op-geth 8545) ``` ### L2 to L1 1. **Initiate a Deposit** Start by making a deposit from L2 using the following command: ```bash theme={null} polycli ulxly bridge asset \ --bridge-address ${BRIDGE_ADDRESS} \ --destination-network 0 \ --private-key ${TEST_PRIVATE_KEY} \ --rpc-url http://$(docker compose port op-geth 8545) \ --value $(date +%s) \ --destination-address ${TEST_ADDRESS} ``` 2. **Wait for the Pessimistic Proof** Once the deposit is made, a pessimistic proof will be generated. After this proof is available, the deposit becomes eligible for claiming. 3. **Verify the Deposit Status** You can check the status of your deposit by querying the bridge service: ```bash theme={null} curl -s http://$(docker compose port bridge 8080)/bridges/${TEST_ADDRESS} | jq '.' { "deposits": [ { "leaf_type": 0, "orig_net": 0, "orig_addr": "0x0000000000000000000000000000000000000000", "amount": "1746047601", "dest_net": 0, "dest_addr": "0xda9f3DCA867C4Bc7b1f8e2DB47Aa8C338Ba2e056", "block_num": "14136", "deposit_cnt": 0, "network_id": 36, "tx_hash": "0x4a5d4914e4ae315af941e1fad2b2aac54dfd6c9bc2ca6033e0ea0b325fbcd90e", "claim_tx_hash": "", "metadata": "0x", "ready_for_claim": true, "global_index": "150323855360" } ], "total_cnt": "1" } ``` If `"ready_for_claim": true`, the deposit is ready to be claimed. 4. Claim the Deposit on L1 Run the following command to claim the deposit: ```bash theme={null} polycli ulxly claim asset \ --bridge-address ${BRIDGE_ADDRESS} \ --bridge-service-url http://$(docker compose port bridge 8080) \ --deposit-count 0 \ --destination-address ${TEST_ADDRESS} \ --deposit-network ${ROLLUP_ID} \ --private-key ${TEST_PRIVATE_KEY} \ --rpc-url ${L1_RPC_URL} ``` 5. Confirm the Claim Finally, confirm that the deposit has been successfully claimed by checking the balance of the destination address: ```bash theme={null} cast balance ${TEST_ADDRESS} --ether --rpc-url=${L1_RPC_URL} ``` *** Your PP network is now live. # Going to Production Source: https://docs.polygon.technology/chain-development/cdk/cdk-opgeth/going-to-production Deploy a CDK chain to testnet or mainnet with support from implementation providers. ## Overview Moving from a local devnet to a production CDK chain on testnet or mainnet requires infrastructure planning, bridge configuration, and Agglayer registration. Polygon's implementation providers handle this process end-to-end. ## Implementation providers Two providers currently support production CDK deployments: | Provider | Execution clients | What they handle | | ----------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------ | | [**Conduit**](https://conduitxyz.typeform.com/to/CrvgqEeA?utm_source=polygonannouncement\&utm_medium=partnerblog) | op-geth, op-reth | Full chain deployment, infrastructure management, monitoring, and ongoing operations | | [**Gateway**](https://share.hsforms.com/1toN701PtTBCpyc3bKQBCRAcy6wj?referrer=12118007123) | op-geth, op-reth | Chain deployment, custom configurations, and managed infrastructure | Both providers support all three [deployment modes](/chain-development/cdk/cdk-opgeth/architecture/): sovereign, validium, and zkrollup. ## Testnet deployment Testnet deployments let you validate your chain configuration, bridge behavior, and application integrations before going to mainnet. Contact an implementation provider to set up a testnet environment. They will provision: * L1 and L2 node infrastructure * Bridge service and AggKit components * Agglayer connectivity (testnet) * Monitoring and alerting ## Mainnet deployment Mainnet deployment follows the same process as testnet with additional requirements: * **Agglayer registration:** Your chain must be registered with Agglayer through the [Polygon Support Portal](https://polygon.atlassian.net/servicedesk/customer/portal/22). * **Bridge contract deployment:** L1 bridge contracts are deployed and verified on Ethereum mainnet. * **Safe Proposer setup:** Rollup registration uses a Safe multisig proposer for governance. Your implementation provider coordinates these steps as part of the deployment process. ## Self-operated chains If you prefer to operate your own infrastructure, start with the [Quickstart](/chain-development/cdk/cdk-opgeth/local-guide/) and [Devnet Deployment Guide](/chain-development/cdk/cdk-opgeth/devnet-deployment-guide/) to understand the component architecture. For production self-operation, contact the Polygon team through the [Support Portal](https://polygon.atlassian.net/servicedesk/customer/portal/22) to discuss requirements. # Quickstart Source: https://docs.polygon.technology/chain-development/cdk/cdk-opgeth/local-guide How to deploy a local cdk-opgeth testnet using Kurtosis, including L1, L2, Agglayer, and OP Stack components. Use this guide to deploy a local testnet instance of `cdk-opgeth` using Kurtosis. This includes a local L1 + L2 environment with Agglayer components and OP Stack infrastructure. *** ## 1. Install Kurtosis Follow the installation instructions from the [Kurtosis official docs](https://docs.kurtosis.com/install). *** ## 2. Launch the cdk-opgeth Stack Use the command below to run the Kurtosis package: ```bash theme={null} kurtosis run \ --enclave cdk \ --args-file https://raw.githubusercontent.com/0xPolygon/kurtosis-cdk/refs/tags/v0.4.0/.github/tests/chains/op-succinct.yml \ github.com/0xPolygon/kurtosis-cdk@v0.4.0 ``` This will: * Start an L1 devnet (Ethereum-like chain) * Deploy Agglayer common contracts * Deploy op-geth, op-node, and op-batcher * Deploy Aggkit and op-succinct infrastructure Kurtosis service output *** ## 3. Bridge Funds from L1 to L2 Use `polycli` to bridge assets from L1 to L2: ```bash theme={null} polycli ulxly bridge asset \ --bridge-address $(kurtosis service exec cdk contracts-001 'jq -r '.polygonZkEVMBridgeAddress' /opt/zkevm/combined.json') \ --private-key 0x12d7de8621a77640c9241b2595ba78ce443d05e94090365ab3bb5e19df82c625 \ --destination-address 0x9175f8176014543492234099F37a385335a017d6 \ --destination-network 1 \ --value 1000000000000000000 \ --rpc-url http://$(kurtosis port print cdk el-1-geth-lighthouse rpc) ``` Kurtosis service output *** **Command parameters:** * `polycli ulxly bridge asset`: Initiates the bridge * `--bridge-address`: Reads from `combined.json` * `--private-key`: Specifies the sender wallet * `--destination-address`: L2 recipient * `--destination-network`: Target network ID (1 for devnet) * `--value`: Amount to bridge in wei * `--rpc-url`: Dynamically resolved RPC from Kurtosis A successful call returns a transaction confirmation. Kurtosis service output *** ## 4. Check Balance on L2 ```bash theme={null} cast balance --ether \ --rpc-url $(kurtosis port print cdk op-el-1-op-geth-op-node-001 rpc) \ 0x9175f8176014543492234099F37a385335a017d6 ``` Kurtosis service output *** ## 5. Send a Transaction on L2 (Inscription) ```bash theme={null} cast send \ --rpc-url $(kurtosis port print cdk op-el-1-op-geth-op-node-001 rpc) \ --private-key 0xfa5f1cc57271a4ccbc5f0becd6bbca6a542973fa2b323d918c5e625fb67bdb20 \ 0x9175f8176014543492234099F37a385335a017d6 \ $(echo -n 'data:,Hello Agglayer!' | xxd -p) ``` This command sends a transaction with `Hello Agglayer!` embedded in the calldata. Kurtosis service output *** ## 6. View the Inscription ```bash theme={null} cast tx \ --rpc-url $(kurtosis port print cdk op-el-1-op-geth-op-node-001 rpc) \ 0x2ec6e2097ef85360cdb1fde9d711412c4ce304a79da5afa69ef9abdbebd6757e input | \ xxd -r -p ``` Expected output: ```bash theme={null} data:,Hello Agglayer! ``` # Why Choose CDK? Source: https://docs.polygon.technology/chain-development/cdk/get-started/benefits Key benefits of Polygon CDK, including EVM compatibility, Agglayer integration, rollup modes, and implementation support. ## EVM Compatibility Chains built with the Polygon Chain Development Kit (CDK) are fully EVM-equivalent. Existing smart contracts can be deployed without modification, and standard Ethereum tooling works without reconfiguration. ## Performance * 60 to 100+ Mgas/s throughput * 20,000+ TPS when optimized for payment workloads * Sub-60-minute finality in zkRollup and validium modes CDK supports two execution clients: **op-geth** (Geth-based) and **op-reth** (Reth-based), giving operators flexibility in resource usage and performance tuning. ## Rollup Modes Every CDK chain supports multiple rollup modes: * **Sovereign**: No ZK prover required. Agglayer enforces security through pessimistic proofs. Default configuration. * **Validium**: ZK-secured execution with offchain data availability. Lower cost and higher throughput than zkRollup. * **zkRollup**: Fully onchain ZK rollup. Maximum security and Ethereum-aligned trust assumptions. Operators select the mode that best balances cost, trust assumptions, and data availability for their use case. ## Agglayer Integration All CDK chains connect to **Agglayer** by default. Cross-chain interoperability, unified liquidity, and shared state are native features of every enterprise chain, not add-ons that require additional infrastructure. ## Implementation Providers For production deployments, CDK chains are typically launched with the support of an implementation provider: * **Conduit** and **Gateway** both support **op-reth** and **op-geth** execution clients. * Both providers are building open source tooling for CDK deployments. Review common questions about execution clients, Agglayer integration, and deployment paths. Contact Polygon about a dedicated enterprise chain and managed deployment support. # What is CDK? Source: https://docs.polygon.technology/chain-development/cdk/get-started/overview Enterprise-grade toolkit for building custom Ethereum L2 chains. Agglayer interoperability is built in by default. The Polygon Chain Development Kit (CDK) is an enterprise-grade toolkit for building custom Ethereum Layer 2 (L2) chains. Every CDK chain connects to **Agglayer** by default, cross-chain interoperability, shared liquidity, and unified state are native features of the infrastructure, not optional add-ons. ## Execution clients CDK supports two execution clients, both maintained as open source by implementation providers: * **op-geth**: Geth-based client with OP Stack architecture. Familiar developer environment and broad tooling support. * **op-reth**: Reth-based client optimized for high throughput and lower resource consumption. Both clients are supported by **Conduit** and **Gateway**, who are building open source tooling for production CDK deployments. ## Performance * 60 to 100+ million gas per second (Mgas/s) * Over 4,700 peak TPS in standard configuration * **20,000+ TPS** when optimized for payment workloads * Finality in under 60 minutes in zkRollup and validium modes ## Rollup modes Every CDK chain supports three operating modes: | Mode | Status | Description | | ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sovereign** | Live | Agglayer connectivity secured by pessimistic proofs. No prover required. Default configuration. | | **Validium** | Live | ZK-secured execution with offchain data availability. Available today via OP Succinct AltDA; see [Privacy Configuration](/chain-development/cdk/privacy). | | **zkRollup** | In development | Fully onchain ZK rollup for maximum security and Ethereum-aligned trust. | ## Agglayer integration All CDK chains connect to Agglayer by default. This provides unified liquidity, shared state, and cross-chain messaging as a native feature of every enterprise chain, without additional bridging infrastructure. ## Projects using CDK * Ternoa * Merlin Chain * Magic Labs (Newton) * Silicon Network * Witness Chain * WireX * Lumia (formerly Orion) * Okto Wallet * Palm Network * Prom * OKX * Moonveil *...and more* Compare execution clients, operating modes, and implementation options. Contact Polygon about a dedicated enterprise chain and managed deployment support. # Polygon CDK: private blockchains with public liquidity Source: https://docs.polygon.technology/chain-development/cdk/index Polygon CDK lets institutions launch their own private blockchain with built-in Agglayer connectivity. Privacy as a spectrum (private validium, gated access, sovereign), 20,000+ TPS, and a bespoke chain operated with Polygon. Polygon CDK **Build private blockchains. Connect to public liquidity.** For institutions that need dedicated, private blockspace but also connection to broad crypto liquidity, Polygon Chain Development Kit (CDK) provides a composable, privacy-on-a-spectrum selection of features for financial institutions: custom throughput, compliance controls, and Agglayer connectivity bundled in. Work with Polygon to design and launch a bespoke chain; CDK is the product, not a self-serve kit. Every CDK chain is part of the wider Open Money Stack: bundled with non-custodial wallets, on- and off-ramps, stablecoin orchestration, and cross-chain interoperability, so your chain has access to the same payments infrastructure as Polygon Chain from day one. ## Key capabilities Configure private validium, gated access, or fully sovereign blockspace. Privacy controls that institutions need without isolating from the broader ecosystem. Every CDK chain ships with Agglayer connectivity, the secure cross-chain bridge that connects the liquidity and users of heterogeneous blockchains in a single interoperability protocol. 20,000+ TPS when tuned for payment workloads. 100+ Mgas/s capacity and managed uptime, with lower operational complexity than self-hosted alternatives. API keys and access control lists let you filter read/write permissions and specific contract or RPC method calls. Deploy fully private or selectively gated. ## Operating modes | Mode | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sovereign** | Agglayer connectivity secured by pessimistic proofs. No prover required. Default configuration. | | **Validium** | ZK-secured execution with offchain data availability. Available today via OP Succinct AltDA; see [Privacy Configuration](/chain-development/cdk/privacy). | | **Private validium** | Validium configuration tuned for institutional privacy requirements. | ## Get started Review the architecture, execution stacks, and rollup modes before planning a deployment. Contact Polygon about a dedicated enterprise chain and managed deployment support. ## Resources Architecture, execution stacks, and technical capabilities. Key benefits and why enterprises choose CDK over alternatives. Frequently asked questions from developers and infrastructure providers. Polygon CDK product overview on agglayer.dev. # Architecture and deployment modes Source: https://docs.polygon.technology/chain-development/cdk/integration-partners/architecture-and-modes Operator-lens component map and the four deployment modes from the Integration Partner's perspective. This page maps the components a RaaS provider operates against the components Polygon operates, and describes the four practical deployment modes from an operator lens. Read this before the integration flow so the runbook steps are anchored in the architecture they touch. ## Components you run vs. components Polygon runs An Agglayer-connected CDK chain is a shared system. The Integration Partner runs the chain-side stack: the trusted sequencer, the components that talk to Agglayer, the bridge indexer, and any mode-specific extras such as a Validium DAC. Polygon operates Agglayer itself and the L1 contracts the chain attaches to. The tables below list the operational ownership boundary. For per-mode component tables and repository link references, see the [`cdk-opgeth` architecture page](/chain-development/cdk/cdk-opgeth/architecture); this page does not duplicate those tables. ### You run | Component | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Trusted sequencer | The L2 sequencer that orders transactions and proposes certificates to Agglayer. | | AggKit (aggsender) | Sends signed certificates from the chain to the Agglayer node. The trusted sequencer acts as the aggsender proposer. | | AggKit (aggoracle) | Updates the Global Exit Root (GER) onchain so the chain stays in sync with Agglayer state. | | RPC | Public read/write endpoint for your chain. | | Bridge service indexer | Indexes L1 and L2 bridge events to serve claim and deposit data to applications. | | DAC committee (Validium only) | The data availability committee that signs and serves offchain transaction data. | ### Polygon runs | Component | Description | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Agglayer node | The core Agglayer service that aggregates certificates from connected chains. | | `AgglayerManager` and rollup contracts on L1 | The L1 contracts (formerly `RollupManager`) that your chain attaches to and that the Agglayer settles against. | | Signer infrastructure for the chain-attachment multisig | The full-signer role on the Safe that executes `attachAggchainToAL`. The Integration Partner has the proposer-only role. | The proposer-only role is the most important operational distinction at the multisig boundary: the Integration Partner can submit `attachAggchainToAL` proposals on the Safe, but only Polygon signers can review, approve, and execute them. ## Deployment modes Every Agglayer-connected CDK chain runs in one of four practical modes. Three of them share the same consensus implementation (`AggchainECDSAMultisig`, `AGGCHAIN_TYPE = 0x0000`) and differ in data availability and prover topology. The fourth, `AggchainFEP` (`AGGCHAIN_TYPE = 0x0001`), is the OP-stack fault-proof integration and is the only mode that requires the Integration Partner to operate prover infrastructure. For Sovereign, Validium, and zkRollup modes, the consensus implementation is `AggchainECDSAMultisig` with `AGGCHAIN_TYPE = 0x0000`. For OP-stack fault-proof integrations the value is `AGGCHAIN_TYPE = 0x0001` (`AggchainFEP`). The `AGGCHAIN_TYPE` value selects the `rollupTypeId` you reference during chain attachment. ### Sovereign (AggchainECDSAMultisig) Sovereign is the default mode and the lowest-overhead path to Agglayer connectivity. Settlement is secured by pessimistic proofs rather than a zk validity proof, so the Integration Partner does not operate a prover. The consensus implementation is `AggchainECDSAMultisig`, with `AGGCHAIN_TYPE = 0x0000`. From an operator standpoint, you run the trusted sequencer, AggKit, and the bridge indexer; you do not need a DAC or a prover. ### Validium (AggchainECDSAMultisig) Validium shares the sovereign consensus path (`AggchainECDSAMultisig`, `AGGCHAIN_TYPE = 0x0000`) but uses an alternative data availability layer instead of posting transaction data to Ethereum. Operationally, the difference is that the Integration Partner stands up and operates a DAC committee that signs and serves offchain data. You do not need a prover. ### zkRollup (AggchainECDSAMultisig) zkRollup posts transaction data onchain to Ethereum and uses the same `AggchainECDSAMultisig` consensus implementation (`AGGCHAIN_TYPE = 0x0000`). The Integration Partner does not run a prover; proving infrastructure is supplied by the existing CDK prover stack; see the [`cdk-opgeth` architecture page](/chain-development/cdk/cdk-opgeth/architecture) for the component tables. The operator footprint is essentially sovereign plus the higher L1 cost of full onchain data. ### AggchainFEP `AggchainFEP` is the OP-stack fault-proof integration and uses a different consensus implementation: `AGGCHAIN_TYPE = 0x0001`. This is the only mode where the Integration Partner participates in prover operation: AggchainFEP chains run an SP1-based zkVM prover for fault-proof challenges. The Integration Partner stack includes the SP1 prover and the OP-stack components alongside the standard trusted sequencer and AggKit. ## Where to go next * [Overview](/chain-development/cdk/integration-partners/overview) for the section framing, the responsibility matrix, and the Jira intake fields. * [Environments and contract addresses](/chain-development/cdk/integration-partners/environments) for the public endpoints and L1 addresses you will configure tooling against. * [Integration flow](/chain-development/cdk/integration-partners/integration-flow) for the end-to-end journey from Jira intake to a live chain. # Environments and contract addresses Source: https://docs.polygon.technology/chain-development/cdk/integration-partners/environments Public Agglayer endpoints and contract addresses for Bali, Cardona, and Mainnet. This page is the reference for Agglayer endpoints and L1 contract addresses across each environment. Use it when configuring tooling, RPCs, or Etherscan lookups for chain integration. The `AgglayerManager` contract is the L1 entry point the Integration Partner targets when proposing `attachAggchainToAL`. It was previously called `RollupManager`, and the older name still appears on Etherscan for the existing deployments. ## Bali (Sepolia devnet) Bali is the Polygon-operated Sepolia devnet used for early integration work, including dry-run chain attachments before Cardona. | Endpoint or contract | Value | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Agglayer RPC | `https://agglayer-dev.polygon.technology` | | Agglayer gRPC | `grpc-agglayer-dev.polygon.technology:443` | | `AgglayerManager` (formerly `RollupManager`) | [`0xE2EF6215aDc132Df6913C8DD16487aBF118d1764`](https://sepolia.etherscan.io/address/0xE2EF6215aDc132Df6913C8DD16487aBF118d1764) | ## Cardona (Sepolia testnet) Cardona is the public Sepolia testnet for Agglayer integrations. Chains complete OP-network deployment certification against Cardona before mainnet attachment. | Endpoint or contract | Value | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Agglayer RPC | `https://agglayer-test.polygon.technology` | | Agglayer gRPC | `grpc-agglayer-test.polygon.technology:443` | | `AgglayerManager` (formerly `RollupManager`) | [`0x32d33D5137a7cFFb54c5Bf8371172bcEc5f310ff`](https://sepolia.etherscan.io/address/0x32d33D5137a7cFFb54c5Bf8371172bcEc5f310ff) | ## Mainnet Mainnet is the production Agglayer environment on Ethereum. | Endpoint or contract | Value | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Agglayer RPC | `https://agglayer.polygon.technology` | | Agglayer gRPC | `grpc-agglayer.polygon.technology:443` | | `AgglayerManager` (formerly `RollupManager`) | [`0x5132a183e9f3cb7c848b0aac5ae0c4f0491b7ab2`](https://etherscan.io/address/0x5132a183e9f3cb7c848b0aac5ae0c4f0491b7ab2) | # Integration flow Source: https://docs.polygon.technology/chain-development/cdk/integration-partners/integration-flow End-to-end sequence from Jira intake to a live chain on Agglayer, with the role of each party at each step. This page maps the end-to-end integration sequence from Jira intake to a live chain on Agglayer, with the role of each party at each step. Use it as the table of contents for the integration section; the hands-on steps are documented in the public [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth) runbook. ## The sequence Open a new-chain request on the Polygon Help Center service desk at [polygon.atlassian.net/servicedesk/customer/portal/22](https://polygon.atlassian.net/servicedesk/customer/portal/22). The intake form fields and an example payload are listed on the [overview page](/chain-development/cdk/integration-partners/overview); have your network parameters finalized before submitting so Polygon engineering can scope the work from a single ticket. Polygon reviews the ticket and either approves the integration or asks follow-up questions through Jira. On approval, Polygon assigns a technical contact who carries the integration through chain attachment and bring-up. The Integration Partner proposes the `attachAggchainToAL` transaction on the appropriate Safe; Polygon executes it as a full signer on `AgglayerManager`. The hands-on procedure is documented in the public [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth/blob/main/01-rollup-creation.md) runbook. After Polygon executes the attachment, you call `initialize()` on the new Rollup contract using the AggchainManager. The call sets the admin, trusted sequencer, gas token, network name, and the initial signer configuration; the exact arguments depend on whether the chain uses `AggchainECDSAMultisig` or `AggchainFEP`. The hands-on procedure is documented in the public [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth/blob/main/04-rollup-initialization.md) runbook. Generate the L2 genesis file by merging the OP Stack genesis with the Polygon pre-deployed contracts. The exact procedure is documented in the [Genesis Generation runbook](https://github.com/0xPolygon/cdk-op-reth/blob/main/02-genesis.md). Stand up the trusted sequencer, AggKit, bridge service, and any mode-specific components (DAC committee for Validium). See [Architecture and deployment modes](/chain-development/cdk/integration-partners/architecture-and-modes) for the operator-lens component map and the mode-specific extras, and the [`cdk-op-reth` runbook](https://github.com/0xPolygon/cdk-op-reth) for the component versions and bring-up steps. Once your chain is producing certificates, your chain is live on Agglayer. ## Who does what The table below restates the seven steps with the responsibility split between the Integration Partner and Polygon, plus the tooling involved at each step. | Step | Integration Partner responsibility | Polygon responsibility | Tooling | | ----------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | 1. Submit a Jira request | Fill the intake form with finalized network parameters. | None at this step. | [Polygon Help Center](https://polygon.atlassian.net/servicedesk/customer/portal/22) | | 2. Polygon reviews and approves | Respond to follow-up questions on the ticket. | Review the request, approve or decline, assign a technical contact. | Jira | | 3. Chain attachment via Safe Wallet | Propose the `attachAggchainToAL` transaction on the Safe (proposer-only role). | Review the proposal, sign, and execute as a full signer. | [Safe Wallet](https://app.safe.global), `AgglayerManager` on L1 | | 4. Rollup initialization | Call `initialize()` on the new Rollup contract using the AggchainManager. | Available to support if the call reverts. | Foundry `cast` or equivalent EVM tooling | | 5. Genesis generation | Merge the OP Stack genesis with the Polygon pre-deployed contracts and validate the output. | Maintain the canonical procedure and component versions. | [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth/blob/main/02-genesis.md) | | 6. Network bring-up | Operate the trusted sequencer, AggKit, bridge service, and mode-specific components (DAC for Validium). | None at this step. | Your chain's node stack, AggKit, bridge service indexer | | 7. Chain goes live | Confirm the chain is producing certificates as expected. | None at this step. | Your chain's monitoring | ## What's next * [Architecture and deployment modes](/chain-development/cdk/integration-partners/architecture-and-modes) anchors the components and mode choices that the bring-up step depends on. * [Environments and contract addresses](/chain-development/cdk/integration-partners/environments) is the reference for the endpoints and L1 addresses you will point tooling at during attachment, initialization, and bring-up. * [Support and resources](/chain-development/cdk/integration-partners/support): Jira service desk and tool repositories. # Integration Partner overview Source: https://docs.polygon.technology/chain-development/cdk/integration-partners/overview What Agglayer + CDK integration means for a RaaS provider, the end-to-end journey, and what to expect from Polygon. This section is for Rollup-as-a-Service (RaaS) providers operating an Agglayer-connected CDK chain on behalf of a customer. Use it when planning, requesting, attaching, initializing, and operating a new chain on Agglayer. **New RaaS providers**: if your team isn't yet an approved Polygon CDK integration partner, apply through the [CDK Integration Partner interest form](https://docs.google.com/forms/d/e/1FAIpQLSfAjR5FsjDSXt55Fb4zW6FLTBFtYzCih2CqY6ur9-iS17FH9w/viewform) before opening chain requests. The rest of this section assumes your team is already onboarded. ## Who this is for This section is written for experienced L2 infrastructure engineers at a RaaS provider who already operate OP-Stack or other EVM L2 networks in production. It assumes familiarity with multisig wallets, EVM contract calls, genesis configuration, and running a trusted sequencer. It is not aimed at end-user developers building dapps on top of a CDK chain. Application teams should start with the public Agglayer SDK and ARC API references linked from the [Support and resources](/chain-development/cdk/integration-partners/support) page. ## The journey at a glance A new chain moves through seven steps from intake to a live listing on Agglayer. You submit a chain request through the [Polygon Help Center service desk](https://polygon.atlassian.net/servicedesk/customer/portal/22), providing the network parameters listed in the intake form. Polygon reviews the ticket, approves the request, and assigns an technical contact for the integration. You propose the `attachAggchainToAL` transaction on the appropriate Safe; Polygon executes it as a full signer. The hands-on procedure is documented in the public [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth/blob/main/01-rollup-creation.md) runbook. You call `initialize()` on the new Rollup contract using the AggchainManager, setting the trusted sequencer, admin, and signer configuration. The hands-on procedure is documented in the public [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth/blob/main/04-rollup-initialization.md) runbook. Generate the L2 genesis file by merging the OP Stack genesis with the Polygon pre-deployed contracts. The exact procedure is documented in the [Genesis Generation runbook](https://github.com/0xPolygon/cdk-op-reth/blob/main/02-genesis.md). You stand up the trusted sequencer, AggKit, bridge service, and any mode-specific components such as a DAC committee for Validium. Once your chain is producing certificates, your chain is live on Agglayer. ## What Polygon does vs. what you do The integration is a shared workflow. Polygon owns the Agglayer protocol and signs the chain-attachment transaction. You own your chain's components and the operational responsibility for keeping it healthy. | Polygon | You (the Integration Partner) | | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Approves the Jira intake request and assigns an technical contact. | Submits the Jira intake request with your chain's parameters. | | Holds the full-signer role on the chain-attachment Safe and executes `attachAggchainToAL` on `AgglayerManager`. | Holds the proposer-only role on the Safe and submits the `attachAggchainToAL` proposal. | | Operates the Agglayer node and the Agglayer contracts on L1. | Calls `initialize()` on the new Rollup contract using the AggchainManager. | | Maintains the canonical deployment guide and component versions in [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth). | Generates the genesis file and brings up the trusted sequencer, AggKit (aggsender and aggoracle), bridge service indexer, and DAC committee (Validium). | | No further action once the chain is producing certificates. | Monitors chain health and operates all Integration Partner-owned components. | ## Engaging Polygon The primary intake channel for new chains and ongoing support requests is the public Polygon Help Center service desk at [Polygon Help Center](https://polygon.atlassian.net/servicedesk/customer/portal/22). When you submit a new-chain request, you provide the following fields. Use the example values as a guide for the format Polygon expects. | Field | Example | | ----------------- | ------------------------------------------------------ | | Rollup Type | Validium | | Network Name | pay-chain | | Chain ID (L2) | 31415 | | Admin Address | `0xecEA75e2854FD52b0aE5C51C88f5eA8e2eC4bf9A` | | Sequencer Address | `0x49a191471F248f7c86cA29477e6E969970BaEAdA` | | Agg Layer Type | mainnet | | Sequencer URL | `https://pay-chain-sequencer.eu-central-6.gateway.fm/` | | Gas Token Address | ETH | Have your network parameters finalized before submitting. Changes after Polygon assigns an technical contact cause rework for both sides. ## What's in this section This section covers the conceptual framing and the environments. The hands-on procedures for chain attachment, rollup initialization, and genesis generation are documented in the public [`cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth) runbook. The components you run, the components Polygon runs, and the four deployment modes from an operator lens. Public Agglayer endpoints and L1 contract addresses for the devnet, testnet, and mainnet environments. The full sequence with per-step role assignments. Jira service desk and the tool repositories used during integration. # Support and resources Source: https://docs.polygon.technology/chain-development/cdk/integration-partners/support Where to get help and reference the public tools used for Agglayer chain integration. This page lists the support channels and tool repositories used during Agglayer chain integration. Use it as the directory of external references and contact points across the rest of this section. ## Becoming an integration partner RaaS providers interested in operating an Agglayer-connected CDK chain start with the [CDK Integration Partner interest form](https://docs.google.com/forms/d/e/1FAIpQLSfAjR5FsjDSXt55Fb4zW6FLTBFtYzCih2CqY6ur9-iS17FH9w/viewform). Polygon reviews the submission and follows up before you open any chain requests through the Jira service desk. ## Getting help The primary support channel is the public Polygon Help Center service desk at [https://polygon.atlassian.net/servicedesk/customer/portal/22](https://polygon.atlassian.net/servicedesk/customer/portal/22). Use it for new-chain requests and ongoing support tickets. Every integration includes a human Polygon engineering handoff at the chain-attachment step. Plan timing so that ticket review and signer coordination on Polygon's side fit your project schedule. ## Tool repositories | Repository | Purpose | | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | [`0xPolygon/cdk-contracts-tooling`](https://github.com/0xPolygon/cdk-contracts-tooling) | Genesis generation for CDK-Erigon networks. | | [`agglayer/agglayer-contracts`](https://github.com/agglayer/agglayer-contracts) | Genesis generation (`tools/createSovereignGenesis`) and post-genesis checks (`tools/compareGenesis`). | | [`agglayer/aggsandbox`](https://github.com/agglayer/aggsandbox) | Local sandbox for testing Agglayer integration before deploying to a live environment. | | [`0xPolygon/cdk-op-reth`](https://github.com/0xPolygon/cdk-op-reth) | Operator-facing deployment guide for CDK + OP Stack rollups connecting to Agglayer. The canonical step-by-step runbook for IPs. | ## Downstream developer resources These are for the application teams building on your chain, not for chain operators. Share them with your customer's developer relations or dapp team. * [https://arc-api.polygon.technology/docs](https://arc-api.polygon.technology/docs): ARC API and SDK reference. * [https://github.com/agglayer/sdk](https://github.com/agglayer/sdk): Agglayer SDK. # Privacy Configuration Source: https://docs.polygon.technology/chain-development/cdk/privacy Deploy a private Polygon CDK chain where transaction data stays in your infrastructure and Ethereum settles a validity proof. Built on OP Succinct AltDA. Polygon CDK supports a privacy configuration for institutions that need to keep transaction data inside their own infrastructure while retaining Ethereum-anchored security and Agglayer interoperability. The chain runs as an OP Stack validium: raw transaction data is held in a data availability server the institution operates, and Ethereum sees only a Keccak256 commitment plus a SP1 Hypercube validity proof. This configuration is intended for regulated institutions that want full control over where customer data lives while staying connected to the broader Ethereum ecosystem for liquidity and settlement. ## Stack At A Glance The privacy configuration runs OP Stack in OP Succinct validium mode, with [SP1 Hypercube](https://blog.succinct.xyz/op-succinct-data-confidentiality/) handling proof generation and AltDA serving as the institution-operated data availability layer. Implementation partners like [Conduit](https://conduit.xyz) and [Gateway](https://gateway.fm) operate the stack as a service; institutions can also run sequencers themselves. The chain settles to Ethereum and connects to Agglayer for cross-chain liquidity and state. ## What This Configuration Delivers **Customer data confidentiality.** Sensitive transaction data stays inside infrastructure the institution owns and operates. Regulators, auditors, and counterparties receive scoped access on the institution's terms. **Self-hosted infrastructure.** Data availability runs behind the institution's existing security perimeter, in jurisdictions it has already cleared with compliance. There is no third-party dependency for data storage or retrieval. **Global liquidity.** The chain stays connected to the Ethereum ecosystem through Agglayer, retaining access to stablecoins, tokenized assets, and counterparty capital across connected chains. **Ethereum-anchored security.** Every chain transition is verified by a ZK validity proof settled to Ethereum, inheriting Ethereum's economic security for settlement. ## How It Works The privacy configuration replaces the OP Stack's default L1 data availability with a Keccak256-commitment alt-DA flow. The pipeline runs in four stages: 1. **Batch submission to private DA.** When the sequencer produces a batch, `op-batcher` submits the batch data to the institution's data availability server using the standard OP Stack alt-DA HTTP API. `PUT /put` returns a Keccak256 commitment; `GET /get/0x{commitment}` retrieves the data. 2. **L1 anchoring.** The batcher posts an L1 transaction containing the `DerivationVersion1` prefix byte (`0x01`) followed by the 32-byte Keccak256 commitment. Raw transaction data never reaches a public network. 3. **Proof generation.** The OP Succinct proposer reads the L1 commitment, fetches the batch data from the DA server, and feeds it to SP1. Inside the zkVM, the proving program verifies `keccak256(data) == commitment` via the preimage oracle before executing the OP Stack derivation pipeline. 4. **Proof settlement.** The validity proof is posted to the chain's `OPSuccinctL2OutputOracle` contract on Ethereum, where it is verified against a range verification key and rollup config hash pinned at deployment. ```mermaid theme={null} sequenceDiagram participant Batcher as op-batcher participant DA as Private DA server participant L1 as Ethereum L1 participant Proposer as op-proposer participant SP1 as SP1 zkVM participant Oracle as L2 Output Oracle Batcher->>DA: PUT /put (batch data) DA-->>Batcher: Keccak256 commitment Batcher->>L1: post 0x01 prefix + commitment Proposer->>L1: read commitment Proposer->>DA: GET /get/0x{commitment} DA-->>Proposer: batch data Proposer->>SP1: verify keccak256(data)==commitment, derive SP1-->>Proposer: validity proof Proposer->>Oracle: submit proof Oracle->>Oracle: verify against pinned VK ``` ## Trust Model The privacy configuration splits trust between data integrity and data availability: * **Data integrity is not trusted to the DA server.** SP1 verifies `keccak256(data) == commitment` inside the zkVM before accepting any data into the derivation pipeline. A malicious or compromised DA server cannot forge state. * **Data availability is trusted to the DA server.** If the DA server loses data or refuses to serve it for a given commitment, no proof can be produced for the affected range. DA-layer redundancy, backup, and access control are the operator's responsibility. For institutional deployments this is the intended trust split: the DA server sits inside the operator's perimeter precisely so the operator controls availability. Cryptographic integrity is enforced by the prover; operational availability is enforced by the institution's own infrastructure practices. ## Access Control The privacy configuration is typically paired with chain-level access controls. These are operator-configurable surfaces; some require additional implementation by the operator or the rollup-as-a-service partner: * **Gated RPC endpoints.** Custom RPC layers can be placed behind enterprise identity systems such as Okta, Azure AD, or any OIDC-compatible SSO. * **Permissioned block explorers.** Block-explorer access can be restricted to authorized users. * **Custom sequencer policies.** Sequencers can enforce KYC-gated mempools, allowlists, and other admission policies. * **Contract-level access controls.** Application logic can enforce per-function and per-role access controls within each smart contract. ## Configuration Reference The AltDA configuration is gated behind a Cargo feature and a single required environment variable: * **Feature flag:** `altda` Cargo feature on the `validity` binary. * **Environment variable:** `ALTDA_SERVER_URL` (no default; the deployment fails to start if unset). * **Service name:** `op-succinct-altda`, with a separate `docker-compose-altda.yml` for the AltDA-mode stack. * **Range ELF:** `altda-range-elf-embedded`, embedded at build time. * **Deployment helpers:** `just deploy-oracle .env altda` and `just update-parameters .env altda`. When deploying or upgrading, the range verification key, aggregation verification key, and rollup config hash must be regenerated with `--features altda`. Without this, `OPSuccinctL2OutputOracle` reverts with `ProofInvalid()` on proof submission. ## Limitations The OP Succinct AltDA mode is marked experimental. Configuration keys, feature flags, and on-disk artifacts may change without notice across releases. Pin specific versions for production deployments, and treat the [OP Succinct AltDA documentation](https://succinctlabs.github.io/op-succinct/validity/experimental/altda.html) as the canonical source of truth. Additional limitations to plan around: * **Keccak256 commitments only.** The current implementation accepts only Keccak256 commitments. Generic commitments (the OP alt-DA `0x01` type byte) are explicitly rejected. * **DA availability is operator-enforced.** The zkVM verifies that retrieved data matches the onchain commitment, but it cannot force the DA server to serve data. Availability and censorship resistance are operator responsibilities. * **No recovery past a missing commitment.** If the DA server loses or refuses to serve data for a given commitment, no proof can be produced for the affected range. There is no recovery path past a missing commitment. * **Hardcoded 30-second HTTP timeout.** The DA fetch timeout is fixed in the current implementation and is not configurable. * **Standard L1 head selection.** The proposer uses standard L1 head selection; there is no Blobstream-style finality tracking. * **Components out of scope for OP Succinct.** The alt-DA server itself, generic-commitment encoding, and onchain DA challenge/bonding logic are not provided by OP Succinct. ## Deployment Options Three deployment patterns are supported: * **Institution-operated.** The institution runs the full stack: sequencer, `op-node`, `op-batcher`, `op-proposer`, and the AltDA server. * **Conduit-managed.** [Conduit](https://conduit.xyz) operates the full stack as a service, including the AltDA server, on behalf of the institution. * **Gateway-managed.** [Gateway](https://gateway.fm) provides equivalent rollup-as-a-service operation of the stack. ## Settlement And Interop Validity proofs are posted to the chain's `OPSuccinctL2OutputOracle` contract on Ethereum L1, where each proof is verified against a range verification key and rollup config hash pinned at deployment. The chain connects to Agglayer for cross-chain liquidity and state; see the [Agglayer documentation](/interoperability/agglayer) for cross-chain mechanics and integration patterns. ## Resources Architecture, execution clients, rollup modes, and performance. Canonical upstream reference for the AltDA mode used by this configuration. Source code for the OP Succinct prover and AltDA integration. Cross-chain liquidity and state for connected chains. Announcement and positioning of the privacy configuration. Technical write-up from Succinct on the AltDA shipment. # Changelog Source: https://docs.polygon.technology/changelog Product updates across the Polygon ecosystem. OMS Payments consolidates quote and transaction economics into a single `pricing` container, refactors the source and destination shape into a typed instrument model discriminated by `type` (`walletOms`, `walletExternal`, `bankUs`, `bankIban`, `bankCanada`, `card`, `cash`), replaces the legacy origin fields on transactions with a typed `precursor` object, and retires the `GET /customers/{customerId}/transactions` route in favor of a `?customerId=` filter on `GET /transactions`. ### New * **Typed instruments on `source` and `destination`**: quote and transaction sides are now discriminated by `type`. A quote's source is an OMS wallet (`walletOms`) or a debit card (`card`, pull-from-card funding); the destination can be `walletOms`, `walletExternal`, `bankUs`, `bankIban`, `bankCanada`, `card`, or `cash`. Each type carries its own `details` envelope with the instrument-specific fields (bank routing coordinates, IBAN block, card identifiers, cash pickup coordinates, and so on) and a `party` block that identifies who is on that side. * **Consolidated `pricing` object on quotes and transactions**: per-side amounts (`amountGross`, `amountNet`, `feesDeducted`), the rate pair (`exchangeRate`, `effectiveRate`), the asset `pair`, `fixedAmountSide`, `sponsorGas`, and `sponsorGasCost` all move into a single top-level `pricing` object with `pricing.source` and `pricing.destination` per-side entries. The core equation is now `pricing.source.amountNet × pricing.exchangeRate = pricing.destination.amountGross`. * **`precursor` on transactions**: a typed `precursor` object describes what created the transaction (`quote`, `depositAddress`, `virtualAccount`, or `cashIn`) and carries that origin's deposit instructions. It supersedes the loose `quoteId` / `depositAddressId` / `virtualAccountId` / `cashInId` and top-level `depositInstructions` fields. * **`hold` and `awaitingAction` status**: transactions can now enter a non-terminal `awaitingAction` state when blocked on developer, upstream, or compliance action. The `hold` object explains the reason and carries a deadline, and the transaction returns to `processing` once cleared. `TransactionSubStatus` is now a closed set of status-scoped strings namespaced by their parent (for example `processing.cashPickupReady`, `completed.cashPickupCollected`, `awaitingAction.awaitingSenderAttribution`, `failed.attributionTimeout`). * **`sourceToDestination` corridor tag**: a new composite tag on quotes and transactions covers `cryptoToCrypto`, `cryptoToCash`, `cryptoToFiatAccount`, `cashToCrypto`, and `fiatAccountToCrypto`. It replaces the older `type` and `TransferType` for these resources; `TransferType` is retained for deposit addresses, onramp/cash-in, and customer filters. * **`Party` model on each side**: sides now carry a `party` block discriminated by `relationship` (`customer`, `otherCustomer`, `externalRegistered`, `externalUnregistered`), so two-sided flows such as remittances and B2B payouts can identify each side inline. `senderCustomerId`, `recipientCustomerId`, and the per-request `role` field are removed; use `customerId` (the quote owner and sender) plus the party blocks instead. * **`?customerId=` filter on `GET /transactions`**: pass `customerId` as a query parameter to scope the list to a customer. Matches rows where the customer is on either side. * **`SettlementError` and operator `Recovery`**: transactions carry a `SettlementError` (`code`, `message`, `occurredAt`, `recoverable`, and an optional `recovery` path or `refund`) in place of the previous generic `AsyncError`. ### Updated * **Removed the "MVP" note from the customer `type` description**: the field description now reads simply `Must be "individual"`. * **Card `settlementType` field on quote requests**: card rail only. `internal` custodies the crypto with OMS; `external` delivers to an on-chain wallet. Defaults are `external` for card buys and `internal` for card sells. ### Removed * **`GET /customers/{customerId}/transactions`**: retired. Use `GET /transactions?customerId=…` for the same behavior. * **Legacy top-level fields on `Transaction`**: `senderCustomerId`, `recipientCustomerId`, `role`, `quoteId`, `cashInId`, `depositAddressId`, `virtualAccountId`, `depositInstructions`, `rail`, `rates`, `sponsorGas`, `sponsorGasCost`, `fixedAmountSide`, `type`, `completedAt`, and `cashPickup` are removed from the top level. The information they carried now lives on `pricing`, `precursor`, `source`/`destination` `party`, `sourceToDestination`, and `subStatus`. * **`cashPickupReady` top-level status**: cash off-ramp pickup lifecycle events surface as sub-statuses (`processing.cashPickupReady`, `completed.cashPickupCollected`, `completed.cashPickupExpired`) rather than as a top-level status. The AggLayer settlement service now resumes pending settlement jobs when it restarts, so a restart no longer strands an in-progress settlement until something else re-drives it. ### Updated * **Settlement jobs resume on service restart**: on startup, the settlement service scans persisted settlement job ids and restarts any whose result is non-terminal, skipping jobs that already completed. Operator restarts and crash recovery no longer leave an active settlement job sitting idle until an unrelated event picks it back up ([#1585](https://github.com/agglayer/agglayer/pull/1585)). OMS Payments adds debit-card buy and internal sell to the existing quotes and transactions endpoints, requires `billingAddress` when registering a card external account, returns structured JWT authentication errors that distinguish expired from missing or invalid tokens, and writes a visible retry reason on transient onboarding failures so partners can tell an actively-retrying job from a stalled one. ### New * **Debit-card buy and internal sell on `/quotes` and `/transactions`**: partners reference the card by its `externalAccountId` (`ext_…`) on the quote and transaction requests and the existing pricing and transaction machinery handles the rest. Card buys produce a `fiatToCrypto` transaction and settle either internally or externally; internal card sells produce a `cryptoToFiat` transaction. A new optional `settlementType` field on the quote request (`internal` or `external`) is persisted on the quote and re-read at execute time. `externalAccountId` is surfaced on the transaction `source` (buy) and `destination` (sell) in GET and list responses and on the webhook snapshot. Source `walletId` is now optional for card buys, and a wallet target on a card sell is rejected. New validation errors include `external_sell_unsupported`, `external_account_rail_mismatch`, `external_account_required`, `external_custody_not_allowed`, and `source_wallet_address_not_allowed` (`422`), plus `invalid_wallet_target` (`400`). External card sell is not yet supported. * **`billingAddress` required for card external accounts**: registering an external account of `type=card` now requires a `billingAddress` (`addressLine1`, `city`, `state`, `country`, `zipCode`). Missing or whitespace-only fields surface a `billing_address_invalid` validation error. ### Updated * **Structured JWT authentication errors**: failed bearer-token requests now return RFC 6750-style bodies with a stable machine code on `error` and a human-readable reason on `error_description`, plus a matching `WWW-Authenticate: Bearer error="…", error_description="…"` challenge header. Distinct codes let clients tell expired tokens (`invalid_token` with `"token expired"`) from missing (`invalid_request` with `"missing authorization token"`), bad signature (`invalid_token` with `"invalid token signature"`), missing required project claims, and generic invalid tokens, so a dashboard can trigger a silent refresh on expiry instead of treating every `401` the same. A keyset-unavailable failure now returns `503 temporarily_unavailable` instead of `401`. Note the breaking response contract change: the top-level `error` field is now a stable machine code rather than free-form prose; clients reading `.error` as a human message need to read `.error_description` instead. * **Visible retry reason on transient onboarding failures**: when an onboarding phase hits a transient error, the onboarding record now carries a non-terminal `statusReason` such as `retrying after transient error (attempt N): `, refreshed on each retry and self-cleared once the phase advances or the onboarding reaches a terminal state. An actively-retrying onboarding is no longer indistinguishable from one that is silently stuck; terminal failures continue to record their final reason as before. Bor adds graded peer response on `develop` so transient sync failures no longer churn good peers, Heimdall lands the disabled-by-default Ithaca hardfork code for stalled-producer span rotation, and Erigon v3.6.1 ships with optimizations and bug fixes that RPC providers are recommended to take before the mainnet Zurich activation. ### New * **Bor devp2p peer jailing on `develop`**: the bor downloader replaces blanket peer drops with a graded response. Transient failures (timeouts, stalls, unsynced peers, empty headers) get a 30-second local backoff, four soft strikes in a 10-minute window escalate to a 5-minute jail, a whitelist (checkpoint or milestone) mismatch backs off then jails before dropping only after persistent disagreement within a 30-minute window, and a pruned-sidechain ghost-state mismatch jails on the first occurrence and drops only on a repeat inside a 30-minute window. Every terminal drop also benches the peer for 30 minutes so it cannot dodge the drop by reconnecting. Not consensus-affecting, fully backwards-compatible, and operator-visible through new `eth/downloader/peer/response/{backoff,jail,drop,mismatch}` Prometheus meters and a `reason` field on the existing `Synchronisation failed, dropping peer` log ([#2283](https://github.com/0xPolygon/bor/pull/2283)). * **Heimdall Ithaca hardfork code merged, disabled by default**: a new hardfork gate adds VEBLOP pending-stall recovery so a span rotates when the agreed actual bor head stops advancing past a stall threshold, closing the post-Rio liveness hole where a stalled single block producer could halt bor production while a pending milestone tally remained in the 1/3–2/3 band. The new behavior is gated behind a per-network `ithacaHeight` that ships at `0` (disabled) on every network; activation heights are a release-planning decision and must be `>= Rio`. No stored-state migration or genesis change ([#611](https://github.com/0xPolygon/heimdall-v2/pull/611)). ### Updated * **Erigon v3.6.1**: a non-breaking optimization and bug-fix release. RPC providers are strongly recommended to upgrade ahead of the mainnet Zurich activation; nodes already running `v3.7.1-priv` require no action. AggLayer 0.6.0 development adds a settlement-result lookup by hash, persists EIP-1559 fees per settlement attempt so replacement transactions reliably clear the standard 10% pricebump on nonce retry, and stops a retry loop from hammering L1 RPCs during indexing lag. ### New * **L1 result lookup by settlement hash**: operators can query the L1 settlement result for a given settlement hash directly, instead of re-deriving the result from local state ([#1572](https://github.com/agglayer/agglayer/pull/1572)). ### Updated * **Gas bump on settlement retry**: each settlement attempt now persists its resolved EIP-1559 fees (`max_fee_per_gas`, `max_priority_fee_per_gas`), and a replacement transaction on the same nonce bumps both fields by at least the geth pricebump floor (10%), tracking a fresh L1 estimate and respecting the configured ceiling. When the ceiling forbids a strict bump, the run loop waits on the existing ceiling-priced attempt rather than broadcasting an underpriced replacement that execution-layer clients would reject ([#1580](https://github.com/agglayer/agglayer/pull/1580)). ### Fixed * **No reorg loop during L1 indexing lag**: when a nonce is observed mined but the receipt has not yet propagated to a lagging RPC node, the settlement loop now re-checks the `(wallet, nonce)` mapping and treats a still-matching transaction as `NotSettledYet`, retrying through the existing backoff path. Only a nonce that no longer maps to the same transaction is reported as a reorg, eliminating a tight retry loop that hammered L1 during indexing lag ([#1581](https://github.com/agglayer/agglayer/pull/1581)). Trails makes Base ETH native an unconditional relay-handoff candidate on every edge rail (Solana and Tron, origin and destination), exposes per-rail edge enablement on `RuntimeStatus`, fixes a multi-wallet connection regression in the widget, and lands a broad set of edge-monitor, provider-scoring, and shutdown reliability fixes. ### New * **Base ETH native on all edge rails**: Base ETH native is now an unconditional relay-handoff candidate on both origin and destination across every edge rail. The previous flag that gated Base-native to Solana origin only is removed; Relay quotes the pair in all four corners (EVM↔Tron, exact-input and exact-output), so suppressing it on Tron origin saved no work and hid a swap-saving candidate. The strategy race still discards candidates that fail to quote ([#944](https://github.com/0xsequence/trails-api/pull/944)). * **`edgeSolanaEnabled` and `edgeTronEnabled` on `RuntimeStatus`**: the `RuntimeStatus` RPC and generated Go and TypeScript clients report per-rail edge enablement derived from `config.edge.solana.enabled` and `config.edge.tron.enabled` ([#948](https://github.com/0xsequence/trails-api/pull/948)). ### Updated * **Edge monitor enablement is rail-aware**: the edge monitor is considered enabled when either Solana or Tron edge is enabled, centralised behind `Config.EdgeEnabled()` so a Tron-only or Solana-only deployment starts the worker correctly ([#951](https://github.com/0xsequence/trails-api/pull/951)). * **Provider scoring uses real gas costs**: Relay now reports origin-chain gas in `Info()` (200k gas units), Hyperlane CrossCollateral reports a 3 bps fee and 30-second duration, and Relay's default duration moves from 10s to 30s. `Capabilities()` returns `Supported`, `Trustless`, and `Operations` from a single struct, so probe paths no longer call `Info()` with a zero chain ID and silently drop Relay, Hyperlane, and CCTP from candidate building ([#935](https://github.com/0xsequence/trails-api/pull/935)). ### Fixed * **Multi-wallet connection regression on EVM and SVM**: a widget regression that broke connecting both EVM and Solana wallets in the same session is fixed ([#1122](https://github.com/0xsequence/trails/pull/1122)). * **Edge monitor queue starvation**: persistent-error rows no longer cycle through `POLLING → PENDING` every 5 seconds (which had been resetting the 1-hour retry timeout) and starve newer edge intents behind 10 stuck rows in the dequeue. Failing origin edge quotes now park in `POLLING` until stale recovery or timeout, so persistent failures age out through the existing pending-edge pipeline ([#949](https://github.com/0xsequence/trails-api/pull/949)). * **Origin edge fast-retry on missing fill hash**: when Relay reports terminal success before the fill or handoff hash has propagated into `txHashes`, the edge quote fast-retries on the next worker cycle instead of waiting for stale-recovery to time out ([#953](https://github.com/0xsequence/trails-api/pull/953)). * **Solana edge address validation**: Solana origin, destination, token, and refund addresses are validated by decoding through `solana-go` instead of a regex shape check, so base58-shaped strings that decode to the wrong public-key length are now rejected. Token-style public keys remain valid where appropriate, but are not accepted as a user recipient or refund address ([#943](https://github.com/0xsequence/trails-api/pull/943)). * **Terminal intents clear queued child transactions**: `IntentReceipts.RefreshStatus` aborts queued child transactions still in `ON_HOLD`, `PENDING`, or `ERRORED` once the parent receipt is terminal, so `/info/workers` no longer shows stale dependents waiting for a release that will never come. Active child transactions in `RELAYING`, `SENT`, or `MINING` are preserved ([#940](https://github.com/0xsequence/trails-api/pull/940)). * **Graceful-shutdown dispatch stranding**: the dispatch decision in `ProcessTransactions` runs on a context detached from worker shutdown, so a SIGTERM landing after the dequeue claim no longer leaves an `ORIGIN`/`DESTINATION` row stuck in `RELAYING` until the 10-minute stale sweep. Receipt waits remain cancellable so shutdown still aborts long polls cleanly ([#923](https://github.com/0xsequence/trails-api/pull/923)). OMS Wallet hardens WalletConnect session handling, normalizes wallet, relayer, and EVM revert errors into clearer in-app messages across send, sign-transaction, recovery, WalletConnect, and Earn, adds a cookie consent banner with separate analytics toggles and PII scrubbing, and tightens Earn deposit preflight so capped vaults and unaffordable relayer fees are caught before signing. ### New * **Cookie consent and analytics preferences**: a consent banner now appears on first visit with separate toggles for product analytics and Google Analytics, and Account Settings exposes a re-openable analytics preferences entry. Analytics initialize only after consent and stop again on revoke; GA Consent Mode is updated and `_ga*` cookies are cleared on revoke. Wallet addresses, hashes, emails, bare hex values, ENS names, and sensitive URL substrings are scrubbed from analytics payloads before they leave the client, and contact and watched-wallet nicknames are no longer sent ([#286](https://github.com/0xsequence/wallet-webapp-v3/pull/286)). * **Earn deposit preflight and vault status guards**: ERC-4626 `maxDeposit(receiver)` and `previewDeposit(amount)` are checked before relayer execution, capped vaults surface a friendly vault-capacity error, and a fee-balance guard blocks deposits and withdrawals where amount plus relayer fee would exceed the selected token balance. Yield vault status (entry and exit availability, capacity state, entry limits, risk metadata) is now preserved through the proxy cache, so deposits and withdrawals are disabled with a clear reason when Yield reports the vault is not accepting them. Possible fees show as informational detail-page copy, not warning badges ([#285](https://github.com/0xsequence/wallet-webapp-v3/pull/285)). ### Updated * **Wallet, relayer, and EVM revert errors normalize app-wide**: a vendored `wallet-errors` module decodes wallet, relayer, and EVM revert errors (including `Error(string)` and `AllCapsReached()`) into structured, friendly messages and is wired through send, sign-transaction, recovery, WalletConnect, Earn deposit, and Earn withdraw. Raw error details (`code`, `name`, `cause`, original message) are preserved for analytics and debugging while user-facing copy is clearer. Failed relayer quotes are no longer classified as sponsored or free-gas options, and a stale selected relayer ID is cleared once relayer options refresh ([#291](https://github.com/0xsequence/wallet-webapp-v3/pull/291)). ### Fixed * **WalletConnect session-request handling and stale modal UX**: duplicate transaction modals from WalletConnect re-delivering unresponded `session_request` events on every relay reconnect are de-duplicated by an in-memory seen-request set, closing the modal now bulk-deletes all pending transactions instead of triggering a close/reopen loop, pending transactions older than 10 minutes are cleaned up on WalletKit init and every 60 seconds, the sign-transaction modal opens reliably on tab focus through the document visibility API instead of the non-reactive `document.hasFocus()`, session-proposal unmount no longer cancels approvals that triggered a navigation, and unimplemented WC methods (`eth_sign`, `eth_signTypedData`, `eth_sendRawTransaction`, unsupported defaults) now reply with `UNSUPPORTED_METHODS` so dapps get a real error. The confirm button is also disabled until the relayer fee option is selected ([#329](https://github.com/0xsequence/wallet-webapp-v3/pull/329)). * **Trails history button restored** in the wallet's Trails integration ([#323](https://github.com/0xsequence/wallet-webapp-v3/pull/323)). The OMS public API surfaces a structured on-chain identity for assets on wallets and transactions, so integrators can read the underlying chain, protocol family, and token without parsing the human-readable `chain` and `asset` strings. ### New * **`BlockchainAsset` schema**: a new component that combines the chain identifier (`chainId`), the wire-level protocol family (`protocol`: `evm`, `svm`, or `sui`), and the token identifier on that chain (`tokenId`). All three fields are required when the object is present. * **`BlockchainProtocol` enum**: `evm` for Ethereum-compatible chains, `svm` for Solana, and `sui` for the Sui Move VM. * **`blockchainAsset` field on wallets, customer wallets, transactions, and wallet balances**: `Wallet`, `CustomerWallet`, `TransactionSource`, `TransactionDestination`, and the `data` payload of `GET /wallets/{walletId}/balance` now include an optional `blockchainAsset` object. The field is additive: existing `chain` and `asset` strings continue to be returned, so current integrations keep working unchanged. * **`residentialAddress` and `identifyingInformation` on `POST /customers` and `PATCH /customers/{customerId}`**: both fields are now accepted on the public surface and persisted through to compliance review. `ResidentialAddress` and `IdentificationDocument` are exposed as reusable component schemas. * **Required fields on `IdentificationDocument`**: `type`, `issuingCountry`, and `number` are now required when an identification document is supplied. * **Single E.164 phone field**: the public customer schema standardizes on one E.164-formatted `phoneNumber` field, replacing the prior pair of country code plus subscriber number. Bor v2.8.3 ships stable across mainnet block producers, and the mainnet block gas limit lifts from 140 million to 160 million, completing the throughput target announced earlier this month. ### New * **Mainnet block gas limit raised to 160 million**: the planned increase from 140 million to 160 million gas has rolled out on mainnet, lifting the effective execution rate to around 106.66 Mgas/s at the 1.5-second block time. This is the final scheduled step on the path to roughly 5,079 TPS using POL transfers (21,000 gas) as the unit transaction. ### Updated * **Bor v2.8.3 stable rolled out on mainnet block producers**: bor v2.8.3 was cut from a multi-iteration `v2.8.3-candidate` line on Amoy and promoted to mainnet block producers around June 16 to 17. The release rolls up the v2.8.3-beta fixes, including a milestone-mismatch rewind deadlock fix and the related revert from beta4. Trails adds a gasless option to the React send hook, makes wagmi 3 the default widget integration, ships Hyperlane CrossCollateral routes with bridge-gas reimbursement, and lands a wide set of widget UX and worker-side reliability fixes. ### New * **`gasless` option on `useTrailsSendTransaction`**: integrators can opt into gasless execution directly from the React send hook. * **Hyperlane CrossCollateral routes with bridge-gas reimbursement**: Hyperlane CrossCollateral is wired in as a route option, with bridge-gas reimbursement so destinations receive the expected amount. * **Timed refund leaf on 1.5 intents**: v1.5 intents now include a timed refund leaf, so refunds can be claimed deterministically once the timeout window elapses. * **`/info/workers` queue filters and admin intent-receipt refresh**: the admin workers endpoint accepts queue filters and exposes an admin intent-receipt status refresh. ### Updated * **Wagmi 3 is the default widget integration**. * **EVM-first connect ordering**: the connect dialog requires an EVM wallet connection before exposing Solana wallets, matching the EVM-anchored intent model. The SVM tab is also hidden when the Solana edge is not enabled by the API. * **Auto-sync of origin and destination chains**: the widget auto-syncs selected origin and destination chains with the connected wallet and the entered recipient, and improves SVM and EVM address validation. * **Balance hooks**: balance hooks now take an object-shaped params argument. * **Hyperlane monitor cadence**: poll interval reduced from 15 seconds to 3 seconds for faster origin-to-destination dispatch, with CrossCollateral dispatched only after the origin receipt is confirmed. * **`/info/workers` pending-intents speedup**: the operator endpoint that lists pending intents is materially faster. ### Fixed * **SOL pricing in Pay mode**: SOL prices now resolve correctly in Pay mode. * **Chain resolution drift**: chain selection no longer drifts between widget state and the underlying connector. * **`Add wallet` prop control**: the fund-methods screen exposes a prop to hide "Add wallet" cleanly. * **CCTP supported chains**: CCTP routes honor the `chain_ids` config to restrict supported chains. * **Hyperlane decimals**: collateral checks and quote amounts normalize decimals for Hyperlane routes, and route decimal conversions are normalized across providers. * **Legacy receipts**: receipts with a null deposit transaction are refreshed correctly. * **Timeouts and retries**: dispatch and execution timeouts are bounded, ancient executing intents abort cleanly, and `WaitIntentReceipt` `getLogs` backoff is fixed so receipts do not stall behind RPC rate limits. * **Intent `SUCCEEDED` gating**: intent `SUCCEEDED` status is now gated on destination edge delivery confirmation, with related fee and quote-path consolidation in dispatch. Bor v2.8.3 stable ships with parallel-EVM (BlockSTM v2) consensus-correctness fixes, and Heimdall v0.9.0 lands the Zurich hardfork release: Amoy activates around block 37,750,000 on June 17, mainnet around block 47,880,000 on June 25 at 14:00 UTC. ### New * **Zurich hardfork on Heimdall**: v0.9.0 promotes the Zurich hardfork to mainnet. At the activation height, `x/clerk` switches to deterministic state-sync processing with block-height-based event visibility assigned in `PreBlocker`; both `PrepareProposal` and `ProcessProposal` enforce symmetric side-transaction caps (50/block) with wall-clock budgets for proposal construction (500 ms) and vote-extension generation (800 ms); checkpoint signature aggregation moves to commit-only; milestone propositions bind deterministically to the parent hash; `MsgVoteProducers` is restricted to the active validator set; and new ante decorators add a 16-output cap on aggregate bank transfers and 32-byte validation on `MsgCheckpoint.AccountRootHash`. All nodes must upgrade before their network's activation height (Amoy `37,750,000`, mainnet `47,880,000`); pre-activation behavior is unchanged, no store migration or genesis change is required ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **Full Heimdall↔Bor gRPC transport in Heimdall v0.9.0**: opt-in via `bor_grpc_flag`, requires bor v2.8.3 or newer, with a startup hash-parity check across HTTP and gRPC before traffic switches. Batched milestone-proposition calls are around 4.4× faster than the HTTP path ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **Bor endpoint failover with health probing in Heimdall**: non-producer nodes can fail over between configured bor endpoints when health probes mark an endpoint unhealthy; failover is explicitly refused on block-producing nodes to preserve a single source of truth ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **EIP-1559 L1 transactions from Heimdall**: bridge submissions and other L1 transactions now post as type-2 EIP-1559 transactions with operator-configurable gas caps ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). ### Updated * **Bor v2.8.3 stable released**: the v2.8.3-candidate branch promoted to stable after five Amoy soak iterations. Required for operators running Heimdall v0.9.0 with the gRPC transport enabled ([#2272](https://github.com/0xPolygon/bor/pull/2272)). ### Fixed * **Parallel-EVM (BlockSTM v2) consensus correctness**: three independent BlockSTM v2 vs serial-execution divergences, each capable of producing a bad block, are fixed at the `core/state` layer. A 7702-delegated `CodePath` read no longer splits across snapshot versions and poison the cross-block jumpdest cache; `Exist` now recognises accounts made to exist purely by a prior in-block nonce bump (sender increment or 7702 delegation clear), correctly applying the existing-account refund and avoiding the new-account CALL surcharge; and transfer-log emission now matches by `BalanceOpsIdx` instead of by transfer shape, so a same-sender/same-recipient/same-amount selfdestruct payout no longer steals a later transfer's log emission point. Serial `StateDB` and `core/vm` are untouched; non-parallel nodes are unaffected ([#2270](https://github.com/0xPolygon/bor/pull/2270)). * **Heimdall proposer-local liveness on dense blocks**: `PrepareProposal` previously sized the proposal with raw `len(tx)`, but CometBFT validates the returned proposal using the protobuf-encoded size of `Data.Txs`. Under high tx-count load the proposer could assemble a batch that passed its own check yet exceeded `MaxTxBytes` once CometBFT re-measured it, stalling block production on that proposer. Accounting now uses the same protobuf unit CometBFT uses ([#608](https://github.com/0xPolygon/heimdall-v2/pull/608)). Trails adds Hyperlane CrossCollateral routes with bridge-gas reimbursement on the API side, tightens the widget connect-wallet flow to require an EVM wallet before Solana wallets surface, and ships a broad set of dispatch and quote reliability fixes. ### New * **Hyperlane CrossCollateral routes**: a new Hyperlane route variant covers cross-collateral cases that the standard Warp Route flow cannot, including bridge-gas reimbursement on the destination side. Routing dispatches the CrossCollateral call after the origin receipt lands so the destination side only acts once the origin commitment is confirmed ([#843](https://github.com/0xsequence/trails-api/pull/843), [#913](https://github.com/0xsequence/trails-api/pull/913)). * **wagmi 3 as the default widget integration**: the Trails widget and demo now ship wagmi 3 as the default wagmi path, with Privy and Sequence Connect using wagmi3-native provider/connector stacks and `viem` pinned to Privy's peer requirement ([#1102](https://github.com/0xsequence/trails/pull/1102)). ### Updated * **Connect-wallet flow requires an EVM wallet before Solana**: the widget's Connect Wallet screen no longer lets users connect a Solana wallet before an EVM wallet is connected, both in the widget and in the standalone connect screen. The widget also enforces an EVM wallet before allowing the connect screen to be left ([#1099](https://github.com/0xsequence/trails/pull/1099), [#1096](https://github.com/0xsequence/trails/pull/1096)). * **SVM wallet tab hidden when Solana edge is not enabled**: when the API returns `edges: []` for Solana, the widget hides the SVM wallet tab and all Solana wallet options, including both installable and already-detected Solana wallets ([#1101](https://github.com/0xsequence/trails/pull/1101)). * **Hyperlane monitor polling interval reduced**: the Hyperlane monitor now polls every 3 seconds instead of every 15 seconds, shortening time-to-state-transition on Hyperlane intents ([#912](https://github.com/0xsequence/trails-api/pull/912)). ### Fixed * **CCTP `chain_ids` config honoured**: CCTP route discovery now respects the configured `chain_ids` list to restrict the set of supported chains, instead of advertising chains outside the operator's configuration ([#915](https://github.com/0xsequence/trails-api/pull/915)). * **Intent SUCCEEDED gated on destination edge delivery**: an intent only transitions to SUCCEEDED once destination edge delivery is confirmed, removing a class of premature SUCCEEDED states on edge-backed routes ([#893](https://github.com/0xsequence/trails-api/pull/893)). * **Dispatch precheck ERRORED, worker interfaces, and admin transitions**: dispatch precheck failures now route through the ERRORED path, worker interfaces are narrowed to the operations each worker actually performs, and admin-driven state transitions are centralised so they cannot diverge from worker-driven ones ([#897](https://github.com/0xsequence/trails-api/pull/897)). * **Ancient executing intents aborted**: intents stuck in an executing state past their bound are now aborted instead of holding worker capacity indefinitely ([#922](https://github.com/0xsequence/trails-api/pull/922)). * **Timeout and retry bounds**: dispatch timeout and retry handling are now explicitly bounded, preventing unbounded retry loops on transient errors ([#920](https://github.com/0xsequence/trails-api/pull/920)). * **Hyperlane decimal normalisation**: the Hyperlane collateral check and quote amounts now normalise token decimals before comparing, fixing under-collateralisation false positives on tokens with non-default decimals ([#917](https://github.com/0xsequence/trails-api/pull/917)). * **Route decimal conversions normalised** across the API quote path ([#919](https://github.com/0xsequence/trails-api/pull/919)). * **Legacy receipts with null deposit transaction**: legacy receipts that recorded a null deposit transaction now refresh correctly through the receipt refresh path ([#918](https://github.com/0xsequence/trails-api/pull/918)). * **Fee and quote-path consolidation**: a set of fee-collector and quote-path edge cases are consolidated into a single corrected path ([#898](https://github.com/0xsequence/trails-api/pull/898)). * **`/info/workers` pending intents**: the pending-intents path of `/info/workers` is now substantially faster on busy deployments ([#928](https://github.com/0xsequence/trails-api/pull/928)). Heimdall v0.9.0 mainnet release schedules the Zurich hardfork for June 25, bor v2.8.3 ships as the matching stable client, and the mainnet block gas limit increases from 140M to 160M while holding 1.5s blocks. ### New * **Mainnet block gas limit raised to 160M**: with 1.5s blocks held, the upgrade lifts sustained throughput to roughly 5,000 payments per second at the same fee and reliability characteristics. Amoy continues at 200M for stress testing. * **Heimdall v0.9.0 mainnet release, Zurich hardfork**: activates on mainnet at block `47,880,000` (approximately June 25, 2026, 14:00 UTC), with Amoy already activated at block `37,750,000` on June 17, 14:00 UTC. The fork gates a set of consensus-level changes that stay inactive until the activation height: deterministic state-sync processing in `x/clerk` with block-height-based event visibility assigned in `PreBlocker`; symmetric side-transaction caps of 50 per block in `PrepareProposal` and `ProcessProposal`, with wall-clock budgets of 500 ms for proposal construction and 800 ms for vote-extension generation; commit-only checkpoint signature aggregation; deterministic milestone-proposition parent-hash binding; `MsgVoteProducers` restricted to the active validator set; and new ante decorators (16-output aggregate bank-transfer cap and `MsgCheckpoint.AccountRootHash` 32-byte validation). Validators must upgrade before their network's activation height ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **Heimdall to bor gRPC transport (opt-in)**: heimdall ships full gRPC transport for the heimdall to bor channel, enabled with the `bor_grpc_flag` config and requiring bor v2.8.3+. Startup hash-parity check and batched milestone-proposition calls included; internal benchmarks show roughly 45% lower `extendVote` latency on the heaviest `GetBorChainBlockInfoInBatch` path versus HTTP ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **Bor endpoint failover with health probing**: heimdall now supports multiple bor endpoints with health-probed failover. Disabled on block producers ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **EIP-1559 L1 transactions in heimdall**: heimdall now uses the EIP-1559 fee market for its L1 transactions, with safe-default gas caps ([#610](https://github.com/0xPolygon/heimdall-v2/pull/610)). * **Bor v2.8.3 stable release**: backwards-compatible client cut from `v2.8.3-candidate` after five Amoy beta iterations. Recommended for all operators ahead of the Zurich activation. Includes the milestone-mismatch rewind deadlock fix landed in `v2.8.3-beta5` ([#2272](https://github.com/0xPolygon/bor/pull/2272)). ### Fixed * **Parallel-EVM (BlockSTM v2) consensus divergences**: three independent BlockSTM v2 vs serial divergences, each able to produce a bad block, are fixed at the `core/state` layer. (1) Per-tx CodePath read snapshot consistency for EIP-7702-delegated code, addressing the `index out of range` panics in `validJumpdest` and `invalid gas used` bad blocks observed on mainnet. (2) `Exist` now considers prior-tx nonce bumps, so `applyAuthorization`'s existing-account refund and the new-account CALL surcharge match serial. (3) Settlement transfer-log mispairing in `tryEmitTransferAt` now matches on `TransferRecord.BalanceOpsIdx` rather than transfer shape, eliminating the receipts-root-only divergence from EIP-6780 selfdestruct payouts. Serial `StateDB` and `core/vm` are untouched, so non-parallel nodes are unaffected ([#2270](https://github.com/0xPolygon/bor/pull/2270)). * **Heimdall proposer liveness under high tx-count load**: `PrepareProposal` accounting now uses the same protobuf-encoded size that CometBFT uses to validate the returned proposal. Raw `len(tx)` could undercount per-tx protobuf overhead and let a proposer assemble a batch that passed its own check but exceeded `MaxTxBytes` on CometBFT's re-measurement, stalling block production on that proposer. Proposer-local fix; not consensus-affecting ([#608](https://github.com/0xPolygon/heimdall-v2/pull/608)). AggLayer 0.6.0 development gains a settlement-side wallet-and-nonce wait gate and certificate-to-settlement-job persistence so certificate tasks can resolve settlement jobs directly. ### Updated * **Wait for any new expected `(wallet, nonce)` to be on L1**: settlement submission now waits until any newly expected `(wallet, nonce)` is observed on L1, closing a race that could surface during recovery and re-submission ([#1571](https://github.com/agglayer/agglayer/pull/1571)). * **Certificate-to-settlement-job ID persistence**: the state store persists the mapping from certificate IDs to settlement-job IDs through a new `certificate_settlement_job_cf` column family with insert-only semantics and a migration for existing databases. Certificate tasks can resolve settlement jobs directly without re-deriving the link ([#1578](https://github.com/agglayer/agglayer/pull/1578)). Trails adds Hyperlane CrossCollateral stablecoin routes across Arbitrum, Base, and Ethereum, cuts CrossCollateral end-to-end latency on the order of `47s` by dispatching the destination right after origin receipt and tightening the Hyperlane monitor poll, fixes a cross-decimal collateral bug that was stranding intents, and gates EVM and Solana intent completion on confirmed destination fills. ### New * **Hyperlane CrossCollateral routes**: new `CROSS/moonpay` USDC and USDT route set across Arbitrum, Base, and Ethereum, including same-asset, cross-asset, and same-chain coverage. Uses the Hyperlane v4 `CrossCollateralRouter` with `transferRemoteTo` and `quoteTransferRemoteTo`. For overlapping USDC routes, `CROSS/moonpay` is preferred explicitly rather than by alphabetical order. Exact-input fee accounting reserves provider source-token overage and reimburses relayer-fronted native bridge gas through the origin-token collector sweep, so total fee no longer double-counts bridge gas ([#843](https://github.com/0xsequence/trails-api/pull/843)). * **wagmi 3 as the default widget integration**: the Trails wagmi integration is now wagmi 3 by default in the widget ([#1102](https://github.com/0xsequence/trails/pull/1102)). * **CCTP `chain_ids` config now honored**: setting `chain_ids` under `[cctp]` in `api.conf` actually restricts the supported chains for CCTP routes, aligning behavior with every other route provider. Previously the field was declared but silently ignored ([#915](https://github.com/0xsequence/trails-api/pull/915)). ### Updated * **Hide SVM wallet tab when Solana edge is not enabled by the API**: the widget gates the SVM tab and all Solana wallet options on `useSolanaEdgeActive()` in addition to the adapter check. If the API returns `edges: []` for Solana, the SVM tab and any detected or connected Solana wallets are hidden ([#1101](https://github.com/0xsequence/trails/pull/1101)). * **Require EVM wallet connection before connecting Solana**: the widget's connect screen now requires an EVM wallet before allowing a Solana wallet, and the widget itself no longer leaves the connect screen without one ([#1099](https://github.com/0xsequence/trails/pull/1099), [#1096](https://github.com/0xsequence/trails/pull/1096)). * **Hyperlane CrossCollateral destination dispatch latency cut by approximately `47s`**: CrossCollateral now releases the destination transaction as soon as the origin receipt proves the Hyperlane dispatch metadata, instead of waiting for Hyperlane API delivery visibility. Worker-owned early release replaces the API delivery wait; destination preconditions still gate execution ([#913](https://github.com/0xsequence/trails-api/pull/913)). * **Hyperlane monitor poll interval reduced from 15s to 3s**: cuts average Hyperlane delivery-detection latency from roughly `7.5s` to `1.5s`, the largest controllable overhead on CrossCollateral end-to-end time. The 10s GraphQL HTTP timeout keeps cycles from overlapping ([#912](https://github.com/0xsequence/trails-api/pull/912)). * **Fee and quote-path consolidation**: exact-input fee totals are computed per trade type in the fee calculator; gas-estimation RPCs are cached during quoting; provider fallback cascades are bounded; v1 and v1.5 protocol handlers share a single `FeeCalculator` instance; zero-value fees and gas are initialized for testnet chains so testnet quotes no longer hit a nil-pointer ([#898](https://github.com/0xsequence/trails-api/pull/898)). ### Fixed * **Hyperlane cross-decimal collateral check (caused two stuck user intents)**: collateral sufficiency now scales the origin-denominated transfer amount to destination decimals before comparing, instead of comparing destination-denominated collateral against a raw origin transfer amount. For Polygon to BSC 6 to 18 USDC routes, raw collateral `18.45e18` was numerically larger than raw transfer amount `644e6`, so an insufficient-collateral case still passed the check. Quote `ToAmount` and `ToAmountUSD` are now destination-denominated, and EXACT\_OUTPUT requests are normalized back to origin decimals before quoting and packing `transferRemote` calldata ([#917](https://github.com/0xsequence/trails-api/pull/917), [#919](https://github.com/0xsequence/trails-api/pull/919)). * **EVM to Solana intent completion gated on destination edge delivery**: for DESTINATION-mode edge intents, `WaitIntentReceipt` no longer returns `Done=true` until the edge monitor confirms the Solana fill, failure, or refund. The intent holds at `EXECUTING` until the edge worker terminates, then `RefreshStatus` derives the terminal status (Completed to SUCCEEDED, Failed to ABORTED, Refunded to REFUNDED). A delivery bound on the bridge transaction `TxnMinedAt` exits the hold if the destination delivery stalls ([#893](https://github.com/0xsequence/trails-api/pull/893)). * **Timeout, cancellation, and retry hardening**: CCTP now uses an `http.Client` with a 10s timeout instead of the no-timeout default; origin-edge success ordering calls `ExecuteIntent` before marking the edge `COMPLETED` and parks retryable failures in `POLLING` with the fill hash preserved; deposit validation no longer marks potentially funded deposits `FAILED` when shutdown cancels the retry wait, falling through to the existing fail-open provider fallback; deposit-dependent transaction release now uses a background context after the deposit is accepted, so shutdown cannot strand `ORIGIN` or `DESTINATION` rows `ON_HOLD` ([#920](https://github.com/0xsequence/trails-api/pull/920)). * **Dispatch precheck on guard error**: when `shouldDispatchToRelayer` errors, the transaction transitions to `ERRORED` with a status reason and a retry metric, instead of being silently skipped and re-dequeued forever. Context cancellation is excluded so shutdown does not produce spurious `ERRORED` writes ([#897](https://github.com/0xsequence/trails-api/pull/897)). Heimdall v0.9.0-beta announces the Zurich hard fork, with activation blocks scheduled on Amoy and mainnet, and Bor v2.8.3 ships as a non-breaking performance and stability release. ### New * **Zurich hard fork activation scheduled**: heimdall v0.9.0-beta defines the Zurich hard fork with activation at block `37,750,000` on Amoy (June 17, \~14:00 UTC) and block `47,880,000` on mainnet (June 25, \~14:00 UTC). All validators, RPC providers, and node operators should upgrade their heimdall-v2 nodes before the activation block on each network. ### Updated * **Bor v2.8.3 stable**: non-breaking release focused on performance and stability improvements, validated through an extended Amoy testing period. No operator action beyond the standard upgrade cycle is required. * **Heimdall v0.8.2 stable**: matching mainnet release of the v0.8.x line for operators that need to remain on v0.8.x ahead of the Zurich activation on mainnet. AggLayer is now fully upgraded to the SP1 Hypercube (v6) proving system across all connected networks, including mainnet. ### New * **SP1 Hypercube (v6) live on mainnet**: AggLayer mainnet recorded its first L1 settlement on the SP1 Hypercube prover on June 16, completing the v5 → v6 migration across devnets, testnets, and mainnet. The upgraded prover is up to 5x faster on proving speed, more cost effective, and brings security hardening improvements. Backward-compatible reads of legacy v5 proofs and certificates remain in place over the migration window, so no caller-side changes are required. AggLayer 0.6.0 development continues with operator usability and recovery improvements, and AggKit gains a new exit-certificate claimer service alongside resilient sync recovery for the legacy-syncer upgrade path. ### New * **Exit-certificate claimer service**: a new `exit_certificate_claimer` HTTP service in AggKit serves the data needed to call `claimAsset` on L1 for the bridge exits available to an address, with helper scripts (`list-bridges.sh`, `claim-asset.sh`, `claim-all.sh`). The service can run standalone or derive its config from an existing exit-certificate run via `--exit-certificate-config`. Default port `7080` ([#1650](https://github.com/agglayer/aggkit/pull/1650)). * **Off-chain LER computation in the exit-certificate tool**: Step G is split into G1 (lite L2 bridge sync) and G2 (`NewLocalExitRoot` computation). G2 now supports an off-chain mode that computes the LER directly from the lite exit tree without an Anvil shadow-fork, alongside the default shadow-fork verification mode. `exitAddress` is now mandatory and several option names switch to the `ignore*` convention. Config files now accept JSON or TOML ([#1633](https://github.com/agglayer/aggkit/pull/1633)). * **Settlement attempt and job persistence**: settlement attempts and jobs are persisted to storage before submission to L1, so pending settlements can recover attempt metadata and job payload after a node restart ([#1550](https://github.com/agglayer/agglayer/pull/1550), [#1551](https://github.com/agglayer/agglayer/pull/1551)). ### Updated * **Docker image version reporting**: agglayer Docker and CI builds now stamp a real git-derived version at compile time, so the node startup log prints the version instead of `VERGEN_IDEMPOTENT_OUTPUT` ([#1568](https://github.com/agglayer/agglayer/pull/1568)). ### Fixed * **Stuck settled-but-InError certificates**: re-certification now queries the aggchain hash at the pre-settlement L1 block instead of `latest`, so stateful aggchain contracts no longer revert reconciliation with `L2BlockNumberLessThanNextBlockNumber()` after `nextBlockNumber` advances past the certificate's L2 range. L1 archive access is required on the same path that already required it ([#1563](https://github.com/agglayer/agglayer/pull/1563)). * **`l1infotreesync` upgrade loop**: upgrading from a legacy syncer (for example 0.8.x) to the multidownloader implementation no longer leaves the syncer stuck with `not found in storage or blocks_reorged`. When the last-processed block is at or below the finalized block, the multidownloader validates it against L1 by hash and resumes from `lastBlock+1` on a match, removing the need to manually delete the DB ([#1639](https://github.com/agglayer/aggkit/pull/1639), [#1645](https://github.com/agglayer/aggkit/pull/1645)). * **Bare-filename config paths**: agglayer accepts a bare filename config path (for example `agglayer.toml`) again, without requiring a parent directory ([#1553](https://github.com/agglayer/agglayer/pull/1553)). Trails ships a wide set of EVM↔Solana fixes after the EVM↔Solana edge support merge, hardens intent and transaction reliability on the API side, and adds an operator flag to disable the v1 intent protocol. ### New * **Unified wallet connection with SVM**: the widget's wallet connection flow surfaces EVM and Solana wallets in the same connect dialog ([#1019](https://github.com/0xsequence/trails/pull/1019)). * **Wallet screening for quote requests**: quote requests are subject to wallet screening before a quote is returned ([#853](https://github.com/0xsequence/trails-api/pull/853)). * **`intents.disable_v1_intent_protocol` config and `GetSupportedIntentProtocols` API**: deployments can disable the v1 intent protocol, and the supported-protocols RPC reflects the setting ([#876](https://github.com/0xsequence/trails-api/pull/876), [#884](https://github.com/0xsequence/trails-api/pull/884)). * **Recipient picker pulse** and a **same-chain WETH↔ETH calldata demo scenario** in the widget ([#1080](https://github.com/0xsequence/trails/pull/1080), [#1079](https://github.com/0xsequence/trails/pull/1079)). ### Updated * **Source-token selector**: shows all wallet tokens and supports API-based search inline ([#1006](https://github.com/0xsequence/trails/pull/1006)). * **Solana wallet cancellations are retryable**: cancellations from Solana wallets during signing are treated as retryable rather than fatal ([#1072](https://github.com/0xsequence/trails/pull/1072)). * **Solana destination edge timeline**: Solana destination edge transaction states are included in intent state, and duplicate edge-tx rows in the Solana edge timeline are removed ([#866](https://github.com/0xsequence/trails-api/pull/866), [#1076](https://github.com/0xsequence/trails/pull/1076)). * **Default connector set**: select native wallet connectors are removed from the widget's default connector set ([#1068](https://github.com/0xsequence/trails/pull/1068)). ### Fixed * **Mainnet CCTP routes**: a testnet-mode gate that was breaking mainnet CCTP routes is removed ([#861](https://github.com/0xsequence/trails-api/pull/861)). * **Expired persisted fee quotes**: dispatch now regenerates a fresh fee quote when the persisted quote is within 1 minute of its 10-minute expiry, instead of sending an expired quote and being rejected. Stuck DESTINATION transactions also time out after 1 hour rather than waiting forever and starving the dispatch queue ([#868](https://github.com/0xsequence/trails-api/pull/868)). * **Deposit starvation behind in-flight rows**: the dispatch dequeue no longer includes `RELAYING`/`MINING` rows, so stale in-flight rows from crashed or restarted workers no longer block recovery or starve deposit-dependent transactions ([#858](https://github.com/0xsequence/trails-api/pull/858)). * **Same-chain calldata cases**: same-chain swaps with calldata, including same-chain same-token with exact input, route correctly ([#1074](https://github.com/0xsequence/trails/pull/1074), [#1078](https://github.com/0xsequence/trails/pull/1078)). * **Solana destination flows**: destination token selection updates correctly, destination fills no longer double-fill, and Fund is disabled until a Solana recipient is selected when the destination is Solana ([#1073](https://github.com/0xsequence/trails/pull/1073), [#1082](https://github.com/0xsequence/trails/pull/1082), [#1083](https://github.com/0xsequence/trails/pull/1083)). * **Edge metadata rail persistence** is retained correctly on intent records ([#857](https://github.com/0xsequence/trails-api/pull/857)). * **`IntentTransactionGasFee.totalFeeUsd`** is now typed as `float64` instead of `string`, aligning the API contract with how it is serialized ([#854](https://github.com/0xsequence/trails-api/pull/854)). * **Widget polish**: connect-wallet close button, WETH demo icons, clipboard fallback compatibility, and inline-SVG canvas rendering ([#1070](https://github.com/0xsequence/trails/pull/1070), [#1088](https://github.com/0xsequence/trails/pull/1088), [#1085](https://github.com/0xsequence/trails/pull/1085), [#1086](https://github.com/0xsequence/trails/pull/1086)). OMS Wallet rebuilds the Private Send experience on a REST send flow with a dedicated recovery surface, and adds a wallet screening auth gate across sign-in and protected-route entry points. ### New * **Private Send REST flow and status UI**: Private Send now runs on the REST send flow with fee quoting, order creation, deposit submission, order polling, and a five-step lifecycle and status view. Failures after the deposit hand-off are tracked as recoverable rather than fatal ([#304](https://github.com/0xsequence/wallet-webapp-v3/pull/304)). * **Private Send recovery**: a Settings recovery surface lets a connected wallet look up saved recoverable orders, inspect stuck balances, and submit stuck-UTXO withdrawals ([#306](https://github.com/0xsequence/wallet-webapp-v3/pull/306)). * **Wallet screening auth gate**: authenticated wallets and connected EOAs are checked against a screening service before sign-in, OAuth callback, route loaders, auth-token setup, and the experimental wallet and login routes proceed. Blocked wallets are cleared from local auth state, redirected to sign-in, and shown a blocked-wallet notice. Screening URL and key are configured via environment variables ([#287](https://github.com/0xsequence/wallet-webapp-v3/pull/287)). ### Updated * **Private Send availability analytics**: availability now reports `unsupported_chain` for non-supported EVM chains and `unsupported_token` for collectibles and non-coin records before checking other eligibility ([#305](https://github.com/0xsequence/wallet-webapp-v3/pull/305)). The OMS public token endpoint now publishes a `429 Too Many Requests` response so clients can handle rate-limited responses correctly. ### New * **`429` response on `POST /auth/token` (get bearer token)**: the token endpoint is rate-limited per client IP. When the limit is exceeded, the endpoint returns `429` with an `ErrorResponse` body and a `Retry-After` response header carrying the integer number of seconds to wait. Clients should branch on `429` alongside `200`, `400`, `401`, and `500` and honor `Retry-After` before retrying. The OMS public API consolidates the account-balance and account-transactions endpoints under the Wallet surface and exposes a synchronous failure response on transaction execution. ### Removed * **`GET /accounts/{id}/balance`**: the standalone account-balance endpoint is no longer part of the public API. Read balances through `GET /wallets/{id}/balance` instead. The `AccountBalance` schema is removed. * **`Account` tag**: the `Account` grouping is dropped from the API. Endpoints that referenced it now live under `Wallet`. ### Updated * **`GET /accounts/{id}/transactions` is now a Wallet operation**: the endpoint is re-tagged from `Account` to `Wallet` and the summary becomes "List wallet transactions." The path parameter `id` now expects a wallet ID with the `wlt_` prefix. The path itself is unchanged, so existing integrations continue to work; update tooling, examples, and SDK groupings to file the endpoint under wallets. ### New * **`502` response on `POST /transactions` (execute a transaction)**: synchronous provider failures during transaction execution now return `502` with a `Transaction` body in failed status, so clients can distinguish "created but failed at the provider" from "not created." Handle `502` alongside `201` when calling the execute endpoint. OMS Wallet adds an agentic wallet mode, refreshes the Earn experience with new filters, sorting, and interest disclosures, refreshes the Private Send flow, and ships a wide set of transaction and inventory fixes. ### New * **Agentic wallet mode**: dedicated landing experience with a terminal-style prompt copy flow for AI coding agents, plus an in-wallet inventory prompt card with docs link, reduced-motion support, and dismiss persistence. * **Earn interest disclosures and deposit terms**: tooltip-based interest disclosures across the Earn list, detail, and deposit surfaces, with the APY column relabeled to Interest. A terms-and-conditions notice now appears below Review Deposit. * **Earn coin filters**: switch between "Yours" (vaults for tokens in your inventory) and "All" to discover other opportunities, replacing the previous third "Vaults available for your funds" table. * **Inventory and empty-state defaults**: collectibles are hidden by default until enabled in settings, the header wallet avatar stack is hidden when only one wallet exists, decorative placeholder cards are removed from empty states, and Earn defaults to all vaults when no coin tokens are held. ### Updated * **Earn UX**: vault data is now cached across page navigation with background refresh, sorting is available on any column with retained filters and sort order across navigation, deposit and withdraw amounts can be entered as a direct percentage, and vault selection rules now require APY under 100 percent (alongside the existing \$300k minimum TVL and non-deprecated criteria) to filter out stale high-APY vaults. * **Private Send flow**: refreshed UI with new step transitions, token selection, recipient selection, amount entry, and final send states. * **Terms of Use and Privacy Policy** updated. ### Fixed * **Transaction flow reliability**: transaction relay and finality waiting now resolves only after a confirmed final status and handles cancellation and failure consistently; the sign-transaction modal scopes requested transactions correctly and keeps selected transactions visible while progressing; dapp send-wallet-transaction recovery handles wallet sync, duplicate approvals, stored transaction ids, the success idle state, and error rendering consistently. * **Inventory cache regressions** after the persisted query cache migration: auth inventory prefetch now writes the canonical summary shape consumed by the inventory query hook, and recovery inventory reads are scoped to the requested recovery wallet, including the per-chain selection flow. * **Empty inventory notice** no longer flickers when refreshing inventory. * **Earn snapshot initialization** no longer gets permanently stuck if a background refresh is interrupted; the refresh lease now expires within 90 seconds and the next request re-triggers initialization. * **Private Send withdraw finalization** behavior corrected. * **Trails success modal** content fixed in the in-wallet cross-chain flow. * **Agentic mode terminal**: text overflow fixed and generated CLI prompt arguments are now wrapped in double quotes for terminal-safe copy and paste. A coordinated heimdall hard fork activated on mainnet on June 2 at block 46361000, and one day later the mainnet block time was reduced from 1.75 seconds to 1.5 seconds, lifting effective execution throughput by roughly 16 percent. ### New * **Mainnet block time reduced from 1.75 s to 1.5 s**: on June 3 the network moved to 1.5-second blocks with the block gas limit held at 140 million, raising the effective execution rate from around 80 Mgas/s to around 93.3 Mgas/s. No operator action is required. As a heads-up, a follow-on block-gas-limit increase from 140 million to 160 million is currently scheduled for June 16 as the final step on the path to 5,000 TPS. * **Heimdall hard fork activated on mainnet at block 46361000**: the validator-set fee-withdrawal gate hard fork activated on Polygon PoS mainnet at block 46361000 on June 2 at 14:00 UTC, with heimdall v0.8.1 as the matching public release. After the fork, fee-withdrawal operations on heimdall are restricted to addresses in the active validator set; requests from non-validators are rejected at message-handling time. The fork also activates the v0.8.x features documented in the June 1 entry (planned-downtime successor nomination, deterministic state syncs gated on `visibilityTimeHeight`, and the related bridge and consensus guards). AggLayer v0.5.0 ships stable, with a revised admin API, mandatory gRPC prover transport, and a safer default for settlement confirmations. ### New * Networks can be marked as disabled in configuration, so operators can stop accepting certificates from a chain without removing it from the deployment ([#1160](https://github.com/agglayer/agglayer/pull/1160)) ### Updated * **Breaking**: the admin API replaces `forceSetCertificateStatus` with `forceEditCertificate`, which takes a certificate ID, a `process-now=true|false` flag, and structured operations such as `set-status,from=InError,to=Candidate` or `set-settlement-tx-hash,from=0x...,to=null`. Update any operator tooling that called the previous endpoint ([#1159](https://github.com/agglayer/agglayer/pull/1159)) * **Breaking**: prover transport is now gRPC-only. The `prover_entrypoint` config key is removed and the `[grpc]` block is mandatory; `prover` references a `ProverType` from the provers repository ([#1195](https://github.com/agglayer/agglayer/pull/1195)) * **Breaking**: the `[extra-certificate-signer]` configuration variable is removed; drop the matching block from deployment configs ([#1206](https://github.com/agglayer/agglayer/pull/1206)) * Default settlement confirmations raised from 1 to 12. Deployments that set this parameter explicitly are unaffected; deployments relying on the default now wait 12 blocks before processing a settlement ([#1470](https://github.com/agglayer/agglayer/pull/1470)) Trails ships SDK v0.16.1 with Hyperlane and LayerZero Value Transfer routes, the Open Intents Framework as a route option, the OMS Wallet widget theme, and a dedicated recovery flow for stuck intents. EVM↔Solana edge support has merged to master and is in QA ahead of a follow-up release. ### New * **Hyperlane routes**: Trails routes can now traverse Hyperlane Warp Routes, including first-party USDC HWR support across Arbitrum, Base, Polygon, Ethereum, and Katana. Hyperlane is enabled in AUTO routing with market-rate fee estimation, supports `EXACT_INPUT` trades, and includes destination-collateral checks, worker recovery, and underfunded-deposit refund handling. * **LayerZero Value Transfer (`LZ_TRANSFER`) routes**: a new route provider built on the LayerZero Value Transfer API replaces the deprecated `lz_stargate` route. The widget exposes it as a selectable bridge option. * **Open Intents Framework (OIF) routes**: OIF is wired in as a route provider, with an expired-order refund worker, explicit rejection of partial-fill quotes, and demo coverage for ETH USDC → Katana USDC. * **OMS Wallet widget theme**: a new widget theme aligned with OMS Wallet, including a refreshed selected-wallet background. * **Recover funds screen**: a dedicated screen in intent history for recovering stuck funds, replacing the prior inline recovery surface. Edge wallet intents are now included in history. * **Fund exact output**: funding flows can now target an exact output amount. * **Recent chains in the chain selector**: previously used chains surface at the top of the chain selector. * **Token selector refresh**: a refresh control on the token selector reloads token lists inline. * **Widget chain and token filtering**: new `supportedChains` and `supportedTokensByChain` widget props let integrators restrict the chains and tokens shown to users. * **`GetEdges` discovery API**: a new RPC enumerates enabled edge rails so integrators can discover available cross-chain routes at runtime. * **Intent history filters**: history and search now accept edge address, rail, mode, provider, and participant-address filters. * **Edge metadata on intent endpoints**: intent receipts now include `IntentEdgeMetadata` with `EdgeStatus`, refund transaction hashes, and rail metadata. * **Same-chain "Swap & Receive" label**: same-chain terminal swap steps are labeled clearly in transaction summaries. * **Wagmi auto-detection**: the widget detects an active wagmi provider without explicit adapter configuration. * **Composable action resolver APIs**: the composable-action resolver helpers are now exported for integrator use. See the [Trails docs](/cross-chain). * **Somnia chain support**: Somnia is supported as a route destination, including `USDC.e` ↔ `USDso` swaps and `SOMI` OFT routes for cross-chain `SOMI` transfers, with refreshed `SOMI` branding in the widget. * **EVM↔Solana edge support (in QA)**: end-to-end EVM↔Solana intents are merged to master and available in the dev demo, with SVM wallet auto-detection, Solana-standard wallet support, Solana funding and settings rows, Solana-side refund and failure handling, and server-side edge execution via a new edge monitor worker. Targeting a follow-up SDK release once QA completes. ### Updated * **Hyperlane fee breakdown**: Hyperlane now shows as a Bridge rather than a Swap in the fee breakdown. * **WETH passthrough**: self-wrap and self-unwrap quote shapes are now supported through passthrough. * **`lz_stargate` removed**: the deprecated `lz_stargate` route provider has been removed in favor of `LZ_TRANSFER`. * **`xai` and `blast` chains removed**: both chains are no longer supported by the Trails SDK. * **Widget input scope tightened**: `from.currency` is restricted to fiat and `from.token` to ERC-20 in the widget. * **Multi-swap route discovery**: routing now discovers multi-swap routes through intermediary tokens, broadening coverage. * **`GetEarnPools` chain list**: refreshed list of chains exposed by the earn-pools endpoint. * **`WaitIntentReceipt` synthesis**: receipts are now synthesized from intent state when no receipt row exists, removing a class of "pending" hangs. ### Fixed * **Polygon USDT permit**: salt-based EIP-712 domain handling now works correctly for the Polygon USDT permit flow. * **EVM signer resolution**: quote requests now resolve the EVM signer address when a wallet session is connected but `walletClient` is still null (for example, MetaMask on Polygon before wagmi materializes the client). * **Embedded wallet approval prompt**: the "Please approve..." prompt is hidden for embedded wallets, including OMS Wallet. * **Embedded wallet intent recovery**: resolves a recovery hang for embedded wallets on v1 intents. * **Quote owner resolution**: the connected EVM wallet is always used as the quote owner; the recipient address is never substituted. * **Quote route provider fee labels**: corrected labels for route provider fees in quote breakdowns. * **Fund method disabled state**: unlisted fund methods now render as disabled rather than enabled. * **WalletConnect recovery flow**: the WalletConnect recovery flow now completes without stalling. * **WalletConnect funding**: WalletConnect funding now redirects to token selection and exposes a disconnect button. WalletConnect wallets are also shown when direct-transfer is hidden from fund methods. * **Native wallet reconnect on reload**: native wallet sessions now reconnect on page reload. * **Recovery fee options**: fee options no longer break for WaaS wallets in the recovery flow. * **v1.5 `depositSignature`**: v1-only fields in `depositSignature` are now guarded for v1.5 compatibility. * **Credit-card flow placeholder**: fund amount placeholder text fixed in the credit-card flow. * **SDK edge sends**: the SDK no longer auto-executes intents after an edge deposit; server workers handle execution. * **Same-chain quote math**: fixed `PriceImpactUSD`, native token normalization, and `EXACT_INPUT` calldata for same-chain quotes. * **Edge quote timeout**: intents now abort cleanly on edge quote timeout. * **Fee caps**: exact-input fees are capped to the residual amount, and `CollectorFeeCap` is scoped to `EXACT_OUTPUT` to fix an `EXACT_INPUT` bridge gas regression. * **Edge worker transaction hashes**: init and refund transaction hashes now persist correctly through worker updates. * **Token decimals cache**: scoped by chain to prevent cross-chain collisions. * **Deposit transfer logs**: matching deposit transfer logs are summed instead of taking the first match. * **Outer approvals**: redundant outer approvals are skipped for SDK-managed `hydrateExecuteAndSweep` multicalls. * **Multi-swap recipients**: replay of multi-swap intents now preserves the original per-step recipients. * **Fee collector**: corrected fee-collector validation, tokenlist fallthrough, and receipt-state edge cases. * **Send-transaction destination context**: corrected propagation of destination context for send-transaction calls. The OMS public API expands the set of blockchain networks accepted for inbound transfers. ### Updated * **Inbound blockchain networks**: the `BlockchainInNetwork` enum used by the deposit-address simulation endpoint (`POST /deposit-addresses/{depositAddressId}/simulate`) now accepts `INK` and `SUI` in addition to `BASE`, `ETHEREUM`, and `SOLANA`. Integrators using the deposit-address flow can now simulate inbound USDC, USDT, or USAT transfers from Ink and Sui in sandbox. Bor v2.7.2 through v2.8.2 ship across the lookback window, alongside heimdall v0.7.1, introducing an opt-in heimdall-to-bor gRPC transport, expanded tracing for state-sync transactions, several new operator flags, and a large set of consensus, miner, and bridge fixes. ### New * **Heimdall-to-bor gRPC transport (opt-in)**: every bor-facing JSON-RPC now has a gRPC counterpart, selectable per-node via `bor_grpc_flag` in heimdall's `app.toml` against bor's new `[grpc]` config section. The `GetBlockInfoInBatch` call used in the consensus-critical `ExtendVoteHandler` path is around 4.4× faster on gRPC, and the wire path allocates 50 to 67 percent fewer bytes across the seven methods. Defaults are unchanged, and heimdall performs a startup hash-parity check across both transports before continuing. * **State-sync transaction tracing**: state-sync (bor bridge event) transactions are now first-class in the `debug_*` tracing RPCs, including `debug_traceBlockByNumber`, `debug_traceBlockByHash`, `debug_traceTransaction`, `debug_traceChain`, `debug_traceCall`, `debug_intermediateRoots`, and `debug_standardTraceBlockToFile`. A new wrapped tracing hook collects multiple bridge events under a single synthetic root call frame per state-sync transaction, so `callTracer`-class tracers see one root with N sub-calls instead of N independent traces. The legacy `debug_traceBorBlock` RPC method has been removed; switch any callers to `debug_traceBlockByNumber` or `debug_traceBlockByHash`. * **`p2p.nosnap` config flag**: decouples the local snapshot tree from serving the `snap/1` p2p sub-protocol. Block producers can keep the flat-state snapshot enabled for fast local reads while declining to serve snap sync to peers, which removes unnecessary network and CPU overhead on validator boxes. * **`miner.disable-pending-block` flag**: disables the pending-block creation loop in `miner/worker`. RPC queries against the `pending` tag return `nil`. Intended for non-validator and serving nodes that do not need a pending block. * **Builder-phase prefetch and new metrics**: the builder pipeline now runs three additional, builder-synchronized prefetch phases (upfront plan scan, per-transaction forwarding, and freed-gas overflow), reducing cache-miss costs on contract-heavy blocks. New metrics include `worker/prefetch/builder_added_percent` and `worker/txApplyDuration/{prefetched,notPrefetched}`. Additional block-building timing metrics cover prepare-work duration, pending-transaction fetch time, and the count of header-time extensions. * **Heimdall planned downtime can nominate a successor**: `MsgSetProducerDowntime` now accepts an optional `target_producer_id` to nominate a specific replacement. If the target is invalid, inactive, or also down for the range, selection falls back to round-robin. Gated on a coordinated hard fork. * **Heimdall deterministic state syncs**: introduces height-pinned, one-block-delayed visibility for clerk events so that during heimdall halts pending events remain excluded and all validators derive identical query results. Activation is gated on `visibilityTimeHeight`, which is `0` (disabled) on all networks until the next hard fork is scheduled. * **Heimdall bridge self-healing expanded**: the bridge self-heal loop now recovers `SignerChange` and `UnstakeInit` events in addition to `StakeUpdate`, unified through a single combined subgraph query that returns the max L1 nonce across the three event types. Receipt validation now rejects reverted L1 transactions and logs emitted by unexpected contracts. * **`prepareProposalBudget` in heimdall**: implements an explicit time budget for proposal preparation in ABCI, bounding the work the proposer can do before block dissemination. ### Updated * **Witness filestore is now the default**: the filesystem-backed witness storage is now enabled by default in bor's CLI. Existing operators on the database-backed store continue to work; new nodes pick up the filestore without additional flags. * **Witness configuration template hint**: bor packaging templates now show `[witness]` with `enable = false` instead of the deprecated top-level `witnessprotocol` key, which the current parser no longer consumes. Update any custom configs that still use `witnessprotocol`. * **`rpc.batchlimit` renamed to `rpc.batch-request-limit`**: bor's templates and example config now use `rpc.batch-request-limit`, matching the runtime parser. The previous `rpc.batchlimit` key was silently ignored, which could leave nodes on the default batch limit of 1000 unintentionally. * **`cache.triejournaldirectory` is now wired end-to-end**: operators who set this key in `config.toml` now get the configured directory respected. When unset, behavior is unchanged and the journal stays under `/triedb`. * **Heimdall pruning defaults retuned**: the shipped `config.toml` now defaults `indexer = "null"` (cometbft transaction index disabled), with the pruning `interval` reduced from `3h` to `10m` and `EnforcedMinRetainBlocks` reduced from 2,500,000 to 2,000,000. The effect on a mainnet RPC node is around 23 GB freed at steady state and the elimination of large pruning bursts. Operators who do not use `/tx` or `/tx_search` can also reclaim around 43 GB by removing the existing `tx_index.db` after upgrading. * **Heimdall bridge metric rename**: the three self-heal Prometheus metrics moved into the `heimdallv2` namespace and follow Prometheus naming conventions. `self_healing__StakeUpdate` is now `heimdallv2_self_healing_stake_events_processed_total`, `self_healing__StateSynced` is now `heimdallv2_self_healing_state_syncs_processed_total`, and `self_healing__NewHeaderBlock` is now `heimdallv2_self_healing_checkpoint_acks_processed_total`. Update any dashboards or alerts that reference the old names. * **Heimdall bridge performance**: vote-extension processing now pre-fetches L1 receipts in a single batched RPC call, the root-chain listener uses an O(1) topic-to-event map instead of a per-log linear ABI scan, and the clerk mempool check uses the typed CometBFT RPC client. * **Amoy bootnodes updated**: bor's compiled-in Amoy static peer list now points at the current set of Amoy bootnodes. Operators relying on the embedded list no longer need a manual override. * **Heimdall mainnet seeds and persistent peers**: refreshed to the current working set, so nodes regenerating their config pick up working peers without manual intervention. ### Fixed * **Bor recovers from milestone-mismatch forks without manual wipe**: nodes that briefly forked off the canonical chain due to a heimdall startup hiccup or span propagation gap could enter a state where rewinding required canonical-chain blocks that the downloader refused to deliver, requiring an operator to wipe chain data. The mismatch-rewind path now resyncs the canonical segment from peers while still rejecting genuine sidechain ghost-state attacks. * **Forkid wire-format change reverted**: a change that included polygon-specific forks (Rio, Madhugiri, Dandeli, Lisovo, Giugliano, Chicago, others) in the eth p2p handshake forkid hash was reverted because changing the wire forkid is a breaking peer-to-peer change. Nodes running the changed code could not handshake with older bor peers. A future, coordinated rollout will reintroduce the fix gated on a fork block. * **`eth_estimateGas` no longer fails on non-validator nodes**: nodes whose configured signer is not in the active validator set previously returned "insufficient funds for transfer" against the `pending` block because `Prepare()` was failing on every block and the pending snapshot was never refreshed. `Prepare()` no longer fails for unauthorized signers; `Seal()` continues to reject blocks from unauthorized signers independently, so no invalid blocks can be produced. * **Live tracing no longer conflicts with the miner**: bor running with live tracing enabled could call live tracing hooks from the miner path once it reached live, causing problems. The miner now disables the tracer in its `vm.Config` so only block synchronization invokes the tracer. * **Heimdall checkpoint submission off-by-one fixed**: the bridge stopped submitting checkpoints to L1 after the `indexer = "null"` migration because the new tx-bytes lookup was passing the event's block height instead of the height of the block that contained the checkpoint transaction. The lookup now uses `height - 1`, matching the invariant established by the side-tx post-handler running in the block after the one that included the message. * **Heimdall producer downtime span off-by-one fixed**: veBlop spans created by `PostHandleSetProducerDowntime` were one block longer than intended (6,401 instead of 6,400), causing overlaps with adjacent spans. Gated behind `producerDowntimeSpanFixHeight` to preserve pre-fork behavior. * **`(*Header).GetValidatorBytes` no longer panics on short `Extra`**: an exported helper used by indexers, light clients, and explorers panicked with `slice bounds out of range` when passed a header whose `Extra` field was shorter than 97 bytes on pre-Cancun configurations. The pre- and post-Cancun branches now share a single length guard and return `nil` for malformed input. * **Witness size recovers when filesystem metadata and DB disagree**: if a crash interrupted a witness write between renaming the file on disk and writing the size key to the DB, witness pagination paths could silently fail because `ReadWitnessSize` returned `nil`. The lookup now falls back to `os.Stat` on the witness file when DB metadata is missing. * **Miner state-machine wedges closed**: four silent stall paths in the producer state machine that could leak the `pendingWorkBlock` or `pendingTasks` map entry on edge cases (peer count zero during production, syncing-check early return, sealhash interrupt, full `resultCh`) are now plugged, with regression tests for each. * **`SpanStore.PurgeCache` no longer races its own poll loop**: a background poller could write a freshly purged span back into the cache before the next test read, producing intermittent CI flakes. The poll goroutine is now cancelled and waited on before state is reset. * **Milestone websocket subscriber shuts down cleanly**: the heimdall milestone subscriber outlived `chainDb.Close()` because it was launched with `context.Background()`. It now exits on the bor close channel, eliminating the burst of `pebble: closed` errors during shutdown. AggLayer migrates the prover stack to SP1 v6, aggkit ships substantial exit-certificate recovery tooling, and operators get more configurable RPC behavior plus a slate of reliability fixes. ### New * SP1 prover upgraded to v6 with backward-compatible reads of legacy v5 proofs and certificates, improving proof generation and verification performance during the migration window ([#1525](https://github.com/agglayer/agglayer/pull/1525), [#1545](https://github.com/agglayer/agglayer/pull/1545)) * gRPC ingress now rejects certificates carrying non-writable SP1 proof versions with a dedicated `UnsupportedProofVersion` error instead of failing downstream ([#1522](https://github.com/agglayer/agglayer/pull/1522)) * Settlement service gains an auto-retry helper (`retry_callback_until_success`) that recovers from transient RPC errors instead of panicking ([#1476](https://github.com/agglayer/agglayer/pull/1476)) * aggkit exit-certificate tool: Step A is split into A1 (tracing) and A2 (receipt-recovery fallback), addressable individually or together via `--step a1`, `a2`, or `a`, with automatic migration of legacy output files ([#1630](https://github.com/agglayer/aggkit/pull/1630)) * aggkit exit-certificate tool: new `stepAWindowSize` option (default 5000) for independent Step A chunk sizing, and an abort-on-error mode that stops all workers on the first trace failure when `ContinueOnTraceError=false`. Trace errors now surface both the transaction hash and the underlying RPC message ([#1629](https://github.com/agglayer/aggkit/pull/1629)) * aggkit exit-certificate tool: `targetBlock` accepts finality keywords (`LatestBlock`, `FinalizedBlock`), decimal/hex values, and relative offsets like `LatestBlock/-10`. The resolved block is persisted and reused across resumed runs ([#1627](https://github.com/agglayer/aggkit/pull/1627)) * aggkit LET operator tooling: new `craft-cert`, `send-cert --no-db`, `cert-status`, and `export-cert-exits` subcommands, with stronger fallback diagnostics when aggsender bridge-exit data is unavailable ([#1616](https://github.com/agglayer/aggkit/pull/1616), [#1570](https://github.com/agglayer/aggkit/pull/1570)) * aggkit RPC client: new `BatchBlockHeaderRetrieval` option lets operators disable batch header requests and fall back to sequential calls for nodes that do not support batching ([#1601](https://github.com/agglayer/aggkit/pull/1601)) ### Updated * Per-epoch certificate rate limiting is restored: `MAX_CERTIFICATE_PER_EPOCH` and epoch-capacity gating in `NetworkTask` once again enforce one certificate per network per epoch ([#1514](https://github.com/agglayer/agglayer/pull/1514)) * aggsender startup recovery now requires a manual database wipe when local certificate state diverges from AggLayer, replacing the previous automatic reconciliation ([#1570](https://github.com/agglayer/aggkit/pull/1570)) * aggkit docs now document that event logs must be available from genesis and provide reth configuration for disabling receipt pruning to prevent historical sync failures ([#1610](https://github.com/agglayer/aggkit/pull/1610)) ### Fixed * aggkit exit-certificate tool now patches ERC-20 balances correctly for smart-contract-locked exits in Step G, supporting both OpenZeppelin v4 (slot 0) and v5 (namespaced storage) layouts. Resolves `ERC20InsufficientBalance` errors during SC-locked exit processing ([#1622](https://github.com/agglayer/aggkit/pull/1622)) * aggkit Docker images now embed `VERSION`, `GITREV`, and `GITBRANCH` build metadata, so `docker run aggkit version` reports the correct values instead of empty fields ([#1619](https://github.com/agglayer/aggkit/pull/1619), [#1621](https://github.com/agglayer/aggkit/pull/1621)) * aggkit `l2gersync` no longer emits ERROR-level logs for max-block-range responses that are already handled by automatic chunking ([#1607](https://github.com/agglayer/aggkit/pull/1607)) * aggkit `ParseMaxRangeFromError` now recognizes the `query exceeds max block range` RPC error format, so ClaimSyncer retries with chunked requests instead of failing ([#1589](https://github.com/agglayer/aggkit/pull/1589)) * aggkit bridge-call matching now filters debug trace frames to actual `CALL` operations only, ignoring `DELEGATECALL`, `STATICCALL`, `CALLCODE`, and untyped frames ([#1609](https://github.com/agglayer/aggkit/pull/1609)) * Pending certificate validation now uses the correct error type instead of `CertificateCandidateError::UnexpectedHeight` ([#1289](https://github.com/agglayer/agglayer/pull/1289)) Trails ships composable actions, a token-first widget destination flow, auto-refresh for expired quotes, and a wide set of widget and SDK fixes. ### New * **Composable Actions**: chain multiple destination calls into a single intent, with dynamic balance routing and support for multiple ERC-20 tokens in dynamic-amount slots. Integrators can sequence approvals, swaps, and custom calls inside one quote, with backend validation rejecting ambiguous recipient or passthrough configurations. See the [Trails docs](/cross-chain) for usage. * **Token-first widget destinations**: widgets now accept token-specific destination lists, letting you configure a fixed set of destination tokens (and chains) that users can pick from. Persisted token selections survive across sessions. * **Quote auto-refresh**: expired quotes refresh automatically on the next user interaction before signing, so users no longer see an "Intent quote has expired" error in the pre-signing state. Refresh is suppressed once signing has begun. * **Wallet runtime adapters**: Trails wallet runtimes are now decoupled behind explicit adapters, giving integrators a cleaner extension point for embedding custom wallet stacks. * **Configuration validation and offline recovery**: intent configuration is now validated up front and cached locally, with pruning of stale entries. This enables offline recovery of in-flight intents. ### Updated * **Widget lifecycle callbacks renamed**: `onSwapComplete`, `onPaymentComplete`, `onFundingComplete`, and `onWithdrawComplete` are now `onSwapSuccess`, `onPaymentSuccess`, `onFundingSuccess`, and `onWithdrawSuccess` for consistency. Update your widget props to match the new names. * **WalletConnect**: improved QR rendering, deep-link handling, and loading state in the WalletConnect flow. * **Quote input**: clearer error states when the input amount or token is invalid. * **Send flow**: destination chain now defaults to the connected wallet's chain when no destination is specified. * **Mesh exchange**: mesh exchange queries can now run without a configured `trailsApiUrl`. * **Receipt step ids**: receipt step ids are now stable across renders, making them safer to use as React keys and analytics identifiers. * **SDK packaging**: improved compatibility with Node environments and better tree-shaking for smaller bundles. Server-side rendering with Trails actions is now safe. ### Fixed * Fixed swap funding method selection so the chosen method is preserved across re-renders. * Fixed the receiver token picker losing account context when switching accounts. * Fixed chain-switch detection in the widget when users change networks mid-flow. * Fixed Privy wallet modes incorrectly rendering a ConnectKit button. * Fixed `useQuote` so `QuoteError` is available as a runtime value rather than a type-only export. * Fixed missing deposit transaction enrichment in intent history. * Wrapped errors now propagate via `Error.cause`, preserving the original stack and context for debugging. * Composable-action quotes now reject a top-level recipient (which is per-call only) and omit the quote recipient in earn mode. * Guarded against undefined `details` in `UnknownRpcError` checks to prevent spurious crashes. * Removed a duplicate Pay fallback path that could double-trigger the pay flow. Agglayer adds gRPC-level SP1 proof version validation; aggkit ships configurable block header batching and fixes RPC range errors and bridge call trace matching. ### New * gRPC ingress now rejects certificates carrying non-writable SP1 proof versions and returns a structured `UnsupportedProofVersion` error, giving integrators explicit feedback when submitting proofs built with unsupported versions * `BatchBlockHeaderRetrieval` is now configurable in `[L1NetworkConfig.RPC]` and `[Common.L2RPC]` in aggkit; set to `false` to disable batch RPC requests for endpoints that do not support them (v0.10.0-rc3) ### Fixed * Node process no longer panics on corrupt certificate rows; storage decode failures now surface as `CodecError::Serialization` instead of unwinding the process * Pending certificate validation now returns the correct error type; previously reused `CertificateCandidateError::UnexpectedHeight` for a case it was not designed for * ClaimSyncer now recognizes the "query exceeds max block range" RPC error and automatically retries with smaller block ranges, preventing sync failures on restrictive RPC endpoints (aggkit v0.10.0-rc2, v0.8.3-rc2) * Bridge and claim sync debug traces now correctly match only `CALL` frames, preventing incorrect calldata extraction from `DELEGATECALL`, `STATICCALL`, or `CALLCODE` frames (aggkit v0.10.0-rc3, v0.8.3-rc3) aggkit pre-releases add configurable RPC block header batching and improve bridge call trace accuracy; agglayer corrects error type semantics in pending certificate validation. ### New * Batch block header retrieval is now configurable via `RPCClientConfig` in aggkit, letting operators control how many block headers are fetched per RPC call (v0.10.0-rc3) ### Fixed * Pending certificate validation in agglayer now returns the correct error type; the code previously reused `CertificateCandidateError::UnexpectedHeight`, which is semantically reserved for epoch-context candidate certificates * aggkit no longer emits a spurious error log when l2gersync encounters a block range that exceeds the RPC provider limit (v0.10.0-rc3) * Bridge call matching in debug traces is corrected (aggkit v0.10.0-rc3, v0.8.3-rc3) * aggkit now parses `query exceeds max block range` responses from RPC providers and retries with a reduced range instead of surfacing an unhandled error (v0.10.0-rc2, v0.8.3-rc2) Two governance proposals targeting the validator reward schedule and payments revenue distribution were merged this week. ### New * PIP-86 recalibrates `CHECKPOINT_REWARD` on the L1 `StakeManager` to hold annual POL emission at the 1% target (103,530,000 POL/yr) as Polygon Chain transitions to faster block times. The parameter decreases from 34,695.98 POL to 29,414.92 POL per checkpoint when block time moves to 1.75s (planned May 5), and to 25,212.79 POL per checkpoint at 1.5s (planned May 19). No contract upgrades are required; both changes execute via `updateCheckpointReward()` through the existing `Governance` contract. * PIP-87 proposes a fixed-cost payments program that gives payment companies predictable, fiat-denominated pricing for blockspace on Polygon. Revenue from the program is converted to stablecoins and distributed to validators and stakers via the existing PIP-65/82/85 framework, with a portion used to buy POL from the market for staker distribution and for the PIP-24 burn. Bor v2.7.2 stable ships RPC fixes including re-enabling `eth_simulateV1`, with v2.7.3-beta adding private transaction relay improvements and critical fixes for memory growth and security landing on the development branch. ### New * Added `miner.disable-pending-block` config option to disable the pending block creation loop; when set, all `pending` block RPC queries return nil ### Updated * RPC gas cap is now bypassed only for internal system transactions; external `eth_call` and simulation requests are subject to the configured gas cap * Private transaction relay now purges stale transactions more aggressively using per-block-producer error tracking, preventing unbounded in-memory growth under sustained relay load ### Fixed * `eth_simulateV1` is re-enabled in v2.7.2; a context propagation error in the RPC path was causing the method to return incorrect results * Fixed exponential memory growth in the pathDB BFS preload path: the node-child enumeration was enqueuing all 16 nibble children regardless of node type, causing the visited map to grow as O(16^depth) and consuming up to 14 GB per goroutine on restart, stalling block import for up to two hours on archive and full nodes * Go runtime updated to 1.26.2, resolving 6 standard library CVEs in `crypto/tls` and `crypto/x509` including a TLS 1.3 KeyUpdate denial-of-service and several certificate validation bypass issues Trails API adds gas fee option selection to the intent flow, persists wallet configuration at quote time, and fixes a recovery issue for intents using custom address overrides. ### New * Intents now support gas fee option selection. Specify fee options at quote time; the API validates the selected option against quoted options before execution, giving integrators explicit control over transaction costs. * Wallet configuration is now stored at quote time and returned with the quote response, enabling reliable intent recovery and re-execution across sessions. ### Fixed * Recovery now correctly applies custom address overrides when deriving wallet context, preventing derived-address mismatches for intents configured with non-default addresses. Trails SDK and widget ship smarter intent protocol defaults, better recipient handling, and improved error recovery. ### New * The SDK now automatically resolves the best intent protocol version for each transaction. An explicit override, global config, or SDK default is checked in order, with a fallback to the API-provided default when the preferred version isn't supported. ### Updated * The widget highlights the recipient address when it differs from the sender, making it easier to spot send-to-other flows before confirming. * Expired quote errors now show a warning icon and offer a clear recovery path so users can retry without restarting the flow. ### Fixed * Fixed an issue where the recipient address could go stale after switching wallets in the widget. * Fixed the add-wallet flow in the recipient selector so users can successfully add new recipient addresses. Bor v2.7.1 stable ships opt-in EVM execution optimizations, database performance improvements, and a goroutine leak fix. ### New * Added opt-in EVM execution optimization via `--switch-dispatch` CLI flag or `EnableSwitchDispatch` config option, introducing a fixed-size opcode stack and a switch-dispatch fast path for hot opcodes ported from GEVM ### Updated * Tuned PebbleDB write path (1 MiB `BytesPerSync`, adaptive compaction) and increased PathDB state buffer to 2 GB with intelligent carry-over across flushes, reducing write stalls for archive and full nodes * Removed legacy `UpdateDeps`/`GetDep` DAG algorithm from BlockSTM, fully replaced by the `DepsBuilder` implementation ### Fixed * Fixed goroutine leaks in the witness request path that caused unbounded memory growth when peers disconnected during parallel stateless import aggkit fixes claim syncer startup block selection on empty databases. ### Fixed * Claim syncer now determines its starting block from the earliest settled reference block (not the latest) when the database is empty on startup, preventing sync errors on fresh starts Bor v2.7.1-beta ships transaction propagation improvements and an SRC buffer reduction, with additional RPC and monitoring fixes merged to the development branch. ### Updated * Increased max transaction packet size from 100 KB to 1 MB, improving transaction propagation between peers * Reduced SRC buffer from 500ms to 100ms, allowing block producers to spend more time on transaction execution ### Fixed * `bor_getLogs` and `bor_getLatestLogs` now correctly return state-sync logs from blocks produced before the Madhugiri upgrade * Prometheus `_count` metrics no longer reset to zero on each scrape, restoring correct `rate()`, `increase()`, and latency calculations for node operators The Polygon Wallet adds private token transfers and Yield vault support in the Earn tab. ### New * Added end-to-end private send flow, including a dedicated status screen for deposit, withdraw, success, and failure states, and refreshed gas and privacy fee display with up to 4 decimal place precision * Added Yield vault support to the Earn tab Agglayer raises the default settlement confirmation count to 12 for safer out-of-the-box deployments. * Default confirmations for L1 settlement increased from 1 to 12. Deployments that do not set this value explicitly are now safer by default. Trails API adds a new option to suppress passthrough quotes from intent responses. * Added `intents.disable_passthrough_quotes` option. When set, the API omits passthrough quotes from intent results, giving integrators tighter control over which quote sources are returned. # Routes and discovery Source: https://docs.polygon.technology/cross-chain/api-reference/discovery Discover supported networks, tokens, routes, and prices: GetChains, GetTokenList, routes, and pricing endpoints. Use these endpoints to discover what networks, tokens, and routes are available before building a quote. They are read-only and require no active intent. For authentication and base URL, see the [API overview](/cross-chain/api-reference/overview). *** ## GetChains List all supported networks. **Endpoint**: `POST /GetChains` No request parameters required. ### Response fields | Field | Description | | ------------------ | ---------------------------------- | | `chains` | Array of supported network objects | | `chains[].chainId` | Numeric chain ID | | `chains[].name` | Human-readable network name | ### Example ```typescript theme={null} const { chains } = await trails.getChains({}); chains.forEach((chain) => { console.log(chain.chainId, chain.name); }); // 1 Ethereum // 137 Polygon // 8453 Base // 42161 Arbitrum One ``` *** ## GetTokenList List supported tokens on a specific network. **Endpoint**: `POST /GetTokenList` ### Request parameters | Field | Type | Required | Description | | --------- | ------ | -------- | ------------------------------ | | `chainId` | number | Yes | The network to list tokens for | ### Response fields | Field | Description | | ------------------- | --------------------------------------------- | | `tokens` | Array of token objects on the specified chain | | `tokens[].address` | Token contract address | | `tokens[].symbol` | Token symbol (e.g. `USDC`) | | `tokens[].decimals` | Token decimal precision | ### Example ```typescript theme={null} const { tokens } = await trails.getTokenList({ chainId: 137 }); tokens.forEach((token) => { console.log(token.symbol, token.address, token.decimals); }); ``` *** ## GetExactInputRoutes Get all available destination options for a given source token and amount. Use this to show customers what they can receive given what they have. **Endpoint**: `POST /GetExactInputRoutes` ### Request parameters | Field | Type | Required | Description | | -------------------- | ------ | -------- | -------------------------------------------- | | `originChainId` | number | Yes | Source network chain ID | | `originTokenAddress` | string | Yes | Source token contract address | | `originTokenAmount` | string | Yes | Amount to send, in the token's smallest unit | ### Example ```typescript theme={null} const routes = await trails.getExactInputRoutes({ originChainId: 1, originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", originTokenAmount: "1000000", }); ``` *** ## GetExactOutputRoutes Get all available source options for a desired destination token and amount. Use this when the customer specifies how much they want to receive. **Endpoint**: `POST /GetExactOutputRoutes` ### Request parameters | Field | Type | Required | Description | | ------------------------- | ------ | -------- | ---------------------------------------------------- | | `destinationChainId` | number | Yes | Destination network chain ID | | `destinationTokenAddress` | string | Yes | Destination token contract address | | `destinationTokenAmount` | string | Yes | Desired receive amount, in the token's smallest unit | ### Example ```typescript theme={null} const routes = await trails.getExactOutputRoutes({ destinationChainId: 137, destinationTokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", destinationTokenAmount: "100000000", // 100 USDC }); ``` *** ## GetTokenPrices Get current USD prices for one or more tokens. **Endpoint**: `POST /GetTokenPrices` ### Request parameters | Field | Type | Required | Description | | ---------------- | --------- | -------- | ------------------------------------------ | | `tokenAddresses` | string\[] | Yes | Array of token contract addresses to price | ### Example ```typescript theme={null} const prices = await trails.getTokenPrices({ tokenAddresses: [ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon ], }); prices.forEach((p) => { console.log(p.address, p.priceUsd); }); ``` *** ## GetExchangeRate Get a fiat-to-fiat or fiat-to-crypto exchange rate for display and quoting purposes. **Endpoint**: `POST /GetExchangeRate` ### Request parameters | Field | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------- | | `fromCurrency` | string | Yes | Source currency code (ISO 4217, e.g. `USD`) | | `toCurrency` | string | Yes | Target currency code (ISO 4217, e.g. `EUR`) | | `amount` | number | Yes | Amount in the source currency | ### Example ```typescript theme={null} const rate = await trails.getExchangeRate({ fromCurrency: "USD", toCurrency: "EUR", amount: 100, }); console.log(`100 USD = ${rate.convertedAmount} EUR`); ``` *** ## GetFiatCurrencyList List all fiat currencies supported for on-ramp and off-ramp funding. **Endpoint**: `POST /GetFiatCurrencyList` No request parameters required. ### Example ```typescript theme={null} const { currencies } = await trails.getFiatCurrencyList({}); currencies.forEach((c) => { console.log(c.code, c.name); // USD United States Dollar }); ``` # Status and monitoring Source: https://docs.polygon.technology/cross-chain/api-reference/monitoring Track intent status through to settlement: GetIntent, GetIntentReceipt, WaitIntentReceipt, SearchIntents, and GetIntentHistory. Use these endpoints to track the state of an intent after execution and to query historical payment data. For authentication and base URL, see the [API overview](/cross-chain/api-reference/overview). *** ## GetIntent Retrieve the current status and full details of an intent. **Endpoint**: `POST /GetIntent` ### Request parameters | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------ | | `intentId` | string | Yes | The intent ID to look up | ### Response fields | Field | Description | | --------------- | ------------------------------------------------------------------- | | `intent` | The full intent object including current `status` | | `intent.status` | One of `QUOTED`, `COMMITTED`, `EXECUTING`, `SUCCEEDED`, or `FAILED` | ### Example ```typescript theme={null} const { intent } = await trails.getIntent({ intentId }); console.log("Status:", intent.status); ``` *** ## GetIntentReceipt Get the transaction hashes and settlement status for a completed intent. Use this to confirm that funds arrived at the destination. **Endpoint**: `POST /GetIntentReceipt` ### Request parameters | Field | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `intentId` | string | Yes | The intent ID to retrieve the receipt for | ### Response fields | Field | Description | | ----------------------- | ------------------------------------------- | | `status` | `SUCCEEDED` or `FAILED` | | `transactionHash` | Transaction hash on the destination network | | `originTransactionHash` | Transaction hash on the source network | ### Example ```typescript theme={null} const receipt = await trails.getIntentReceipt({ intentId }); if (receipt.status === "SUCCEEDED") { console.log("Settled on destination:", receipt.transactionHash); console.log("Source transaction:", receipt.originTransactionHash); } ``` *** ## WaitIntentReceipt Long-poll until an intent reaches a terminal state (`SUCCEEDED` or `FAILED`). The API holds the connection open and responds as soon as the intent settles. **Endpoint**: `POST /WaitIntentReceipt` ### Request parameters | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------ | | `intentId` | string | Yes | The intent ID to wait on | ### Response fields | Field | Description | | --------------- | -------------------------------------------------------- | | `intentReceipt` | The receipt object, same structure as `GetIntentReceipt` | | `done` | `true` when a terminal state has been reached | Use `WaitIntentReceipt` when you want to block until settlement, for example in a server-side payout flow where the next step depends on confirmed settlement. Use `GetIntentReceipt` polling when you need to check status periodically without holding a connection open. ### Example ```typescript theme={null} const { intentReceipt, done } = await trails.waitIntentReceipt({ intentId }); if (done && intentReceipt.status === "SUCCEEDED") { // Funds have settled; continue with your post-settlement logic console.log("Settlement confirmed:", intentReceipt.transactionHash); } ``` *** ## SearchIntents Find intents by owner address or transaction hash. Useful for looking up an intent when you have the source transaction hash but not the intent ID. **Endpoint**: `POST /SearchIntents` ### Request parameters | Field | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------- | | `ownerAddress` | string | No | Filter by the customer's wallet address | | `transactionHash` | string | No | Filter by a source or destination transaction hash | | `page` | number | No | Page number for pagination; starts at 1 | | `pageSize` | number | No | Number of results per page | At least one of `ownerAddress` or `transactionHash` is required. ### Response fields | Field | Description | | --------- | ------------------------------------------ | | `intents` | Array of intent objects matching the query | ### Example ```typescript theme={null} const { intents } = await trails.searchIntents({ ownerAddress: "0xCUSTOMER_ADDRESS", page: 1, pageSize: 20, }); intents.forEach((intent) => { console.log(intent.intentId, intent.status); }); ``` *** ## GetIntentHistory Retrieve paginated intent history with receipts for a given address. Returns settled and in-progress intents together with their receipt data where available. **Endpoint**: `POST /GetIntentHistory` ### Request parameters | Field | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------- | | `ownerAddress` | string | Yes | The customer's wallet address | | `page` | number | No | Page number; starts at 1 | | `pageSize` | number | No | Number of results per page | ### Example ```typescript theme={null} const history = await trails.getIntentHistory({ ownerAddress: "0xCUSTOMER_ADDRESS", page: 1, pageSize: 50, }); history.intents.forEach((entry) => { console.log(entry.intent.intentId, entry.intent.status, entry.receipt?.transactionHash); }); ``` # Cross-chain API overview Source: https://docs.polygon.technology/cross-chain/api-reference/overview Authentication, base URL, and the intent lifecycle for the cross-chain payments API. The cross-chain payments API is separate from the OMS Payments API. It has its own authentication, its own base URL, and its own SDK package. Do not use OMS Payments API credentials or base URLs with these endpoints. ## Authentication All requests require an `X-Access-Key` header with your API key. ```http theme={null} X-Access-Key: YOUR_ACCESS_KEY ``` Retrieve your API key from the [Trails Dashboard](https://dashboard.trails.build). ## Base URL ``` https://trails-api.sequence.app/rpc/Trails/ ``` All endpoints are appended to this base URL. For example, the `QuoteIntent` endpoint is: ``` POST https://trails-api.sequence.app/rpc/Trails/QuoteIntent ``` ## All endpoints are POST requests This API uses POST for all endpoints, including read operations. Request bodies are JSON. ## SDK installation ```bash theme={null} pnpm install @0xtrails/api ``` Initialize the client once and reuse it across your application: ```typescript theme={null} import { TrailsApi } from "@0xtrails/api"; const trails = new TrailsApi({ baseUrl: "https://trails-api.sequence.app/rpc/Trails/", accessKey: process.env.TRAILS_ACCESS_KEY, }); ``` ## Intent lifecycle A payment is called an **intent**. Every intent moves through a defined set of states from quote to settlement. | State | Description | API call | | ----------- | ---------------------------------------------------------------------------------- | --------------- | | `QUOTED` | A route and fee have been calculated. Valid for 5 minutes. | `QuoteIntent` | | `COMMITTED` | The quote is locked in. Valid for 10 minutes; must be executed within this window. | `CommitIntent` | | `EXECUTING` | Funds are in transit; routing and conversion are in progress. | `ExecuteIntent` | | `SUCCEEDED` | Funds have arrived at the destination. Terminal state. | (automatic) | | `FAILED` | The intent could not be completed. Terminal state. | (automatic) | ### Expiry and failure * A `QUOTED` intent expires after 5 minutes. After expiry, request a new quote. * A `COMMITTED` intent expires after 10 minutes if `ExecuteIntent` is not called. After expiry, the intent moves to `FAILED`. * `FAILED` is a terminal state. Create a new intent to retry. ## Next steps QuoteIntent, CommitIntent, ExecuteIntent, and AbortIntent. Track an intent through to settlement. Supported networks, tokens, routes, and prices. Integration guide for the widget, SDK, and direct API. # Quotes and execution Source: https://docs.polygon.technology/cross-chain/api-reference/quotes QuoteIntent, CommitIntent, ExecuteIntent, and AbortIntent. These four endpoints cover the full active lifecycle of an intent: getting a route, locking it in, executing, and canceling if needed. For authentication and base URL, see the [API overview](/cross-chain/api-reference/overview). *** ## QuoteIntent Get a route and fee estimate for a payment. The quote is valid for 5 minutes. **Endpoint**: `POST /QuoteIntent` ### Request parameters | Field | Type | Required | Description | | --------------------------- | ----------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- | | `ownerAddress` | string | Yes | The customer's wallet address | | `originChainId` | number | Yes | Chain ID of the source network | | `originTokenAddress` | string | Yes | Token contract address on the source network | | `destinationChainId` | number | Yes | Chain ID of the destination network | | `destinationTokenAddress` | string | Yes | Token contract address at the destination | | `tradeType` | `"EXACT_INPUT"` \| `"EXACT_OUTPUT"` | Yes | Whether the fixed amount is on the input or output side | | `originTokenAmount` | string | If `EXACT_INPUT` | Amount to send, in the origin token's smallest unit | | `destinationTokenAmount` | string | If `EXACT_OUTPUT` | Amount to receive, in the destination token's smallest unit | | `destinationToAddress` | string | Yes | Wallet or contract address to receive funds | | `destinationCalldata` | string | No | ABI-encoded calldata for a destination action (see [programmable destinations](/cross-chain/money-actions)) | | `options.slippageTolerance` | number | No | Maximum acceptable slippage as a decimal (e.g. `0.005` for 0.5%) | ### Response fields | Field | Description | | ------------ | -------------------------------------------------------------------------------- | | `intent` | The full intent object, including `depositAddress`: the address to send funds to | | `feeOptions` | Array of fee payment options; pass your chosen option to `ExecuteIntent` | ### Example ```typescript theme={null} import { TrailsApi } from "@0xtrails/api"; const trails = new TrailsApi({ baseUrl: "https://trails-api.sequence.app/rpc/Trails/", accessKey: process.env.TRAILS_ACCESS_KEY, }); const { intent, feeOptions } = await trails.quoteIntent({ ownerAddress: "0xCUSTOMER_ADDRESS", originChainId: 1, originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum destinationChainId: 137, destinationTokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon destinationToAddress: "0xPRODUCT_WALLET", tradeType: "EXACT_INPUT", originTokenAmount: "1000000", // 1 USDC (6 decimals) options: { slippageTolerance: 0.005, }, }); console.log("Send funds to:", intent.depositAddress); console.log("Available fee options:", feeOptions); ``` *** ## CommitIntent Lock in a quoted intent. After committing, the route and fees are fixed. The intent must be executed within 10 minutes or it expires. **Endpoint**: `POST /CommitIntent` ### Request parameters | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------------------------------------- | | `intent` | object | Yes | The full `intent` object returned by `QuoteIntent` | ### Response fields | Field | Description | | ---------- | ---------------------------------------------------------------------------- | | `intentId` | Unique identifier for the committed intent; use this in all subsequent calls | ### Example ```typescript theme={null} const { intentId } = await trails.commitIntent({ intent }); console.log("Intent committed:", intentId); ``` *** ## ExecuteIntent Initiate execution of a committed intent. Two execution methods are available. **Endpoint**: `POST /ExecuteIntent` ### Method A: Transfer Send funds to `intent.depositAddress` from your wallet or on-ramp, then submit the source transaction hash. | Field | Type | Required | Description | | ------------------------ | ------ | -------- | ---------------------------------------------------- | | `intentId` | string | Yes | The intent ID from `CommitIntent` | | `depositTransactionHash` | string | Yes | Transaction hash of the transfer to `depositAddress` | ```typescript theme={null} // After sending funds to intent.depositAddress onchain: await trails.executeIntent({ intentId, depositTransactionHash: "0xSOURCE_TX_HASH", }); ``` ### Method B: Permit (gasless, ERC-2612) For tokens that support ERC-2612 permit signatures, execute without a separate approval transaction. The customer signs an off-chain permit instead of sending a transaction. | Field | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------- | | `intentId` | string | Yes | The intent ID from `CommitIntent` | | `feeOption` | object | Yes | The selected fee option from `QuoteIntent` | | `permitSignature` | string | Yes | ERC-2612 permit signature authorizing the transfer | | `intentSignature` | string | Yes | Customer's signature over the intent | | `depositSignature` | string | Yes | Signature authorizing the deposit | | `nonce` | number | Yes | Nonce used in the permit signature | | `deadline` | number | Yes | Unix timestamp deadline used in the permit signature | ```typescript theme={null} await trails.executeIntent({ intentId, feeOption: feeOptions[0], permitSignature, intentSignature, depositSignature, nonce, deadline, }); ``` *** ## AbortIntent Cancel a `QUOTED` or `COMMITTED` intent. Intents that are `EXECUTING`, `SUCCEEDED`, or `FAILED` cannot be aborted. **Endpoint**: `POST /AbortIntent` ### Request parameters | Field | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `intentId` | string | Yes | The intent ID to cancel | ### Example ```typescript theme={null} await trails.abortIntent({ intentId }); ``` # Earn accounts Source: https://docs.polygon.technology/cross-chain/earn Offer customers a yield-bearing account powered by onchain lending markets, funded from a card or bank in one step. High-yield savings is one of the most requested features in consumer fintech, and one of the most expensive to build. Traditional options require a banking charter, balance sheet exposure, or a complex treasury operation. Onchain lending markets like Aave and Morpho offer institutional-grade yields on stablecoins, but integrating them typically means building DeFi infrastructure from scratch. The cross-chain layer handles that. Your customer funds from a card or bank account, and the money lands in a yield-bearing position in one action. You define the destination protocol; the SDK handles the routing, bridging, and deposit. ## What your customers experience A customer opens your app, taps "Add funds," enters an amount, pays with their debit card, picks a market, and sees their balance earning yield. All of it happens in under two minutes, with no crypto knowledge required. The stablecoin acquisition, cross-chain routing, and vault deposit happen in the background. When they want to withdraw, the same infrastructure runs in reverse. ## What you build Drop in the `Earn` component to open a market selector. Customers fund from any source and pick where to deposit; the SDK handles the routing, bridging, and deposit. ```tsx theme={null} import { Earn } from "0xtrails"; { // Update customer balance in your system }} /> ``` ## Product-defined destinations To target a specific vault or chain a deposit with another action (e.g., swap then deposit), use composable actions. These let you predefine the protocol, the route, and any intermediate steps. ```tsx theme={null} import { useTrailsSendTransaction, swap, deposit, dynamic } from "0xtrails"; const { sendTransaction } = useTrailsSendTransaction({ to: { chain: "polygon", actions: [ swap({ tokenIn: "USDC", tokenOut: "wstETH", amountIn: dynamic(), }), deposit({ marketId: "yearn-wsteth-vault", token: "wstETH", amount: dynamic(), }), ], }, }); ``` The customer funds from their card or bank. The SDK converts to wstETH and deposits into the vault in a single atomic batch. ## Supported protocols | Protocol | Type | Networks | | -------- | --------------- | ------------------------------------------- | | Aave V3 | Lending | Ethereum, Polygon, Base, Arbitrum, and more | | Morpho | Vault / Lending | Ethereum, Base, Polygon | | Yearn | Vault | Ethereum, Polygon, Arbitrum | Use `useEarnMarkets` to query available markets by chain, token, and protocol at runtime. ## How a yield product fits together A yield product has three parts: 1. **Funding**: The SDK accepts card, bank, or crypto and routes to your vault 2. **Position tracking**: Your backend reads the customer's vault balance to display their yield 3. **Withdrawal**: The SDK routes funds from the vault back to the customer's preferred destination The SDK handles the first part entirely. The second integrates with standard ERC-4626 or protocol-specific balance reads. The third uses the withdraw flow for outbound transfers. For a complete walkthrough of funding a Morpho vault, see the [Morpho vault deposit guide](/wallets/morpho-vault-deposit). # Accept funds from anywhere Source: https://docs.polygon.technology/cross-chain/fund Let customers fund your product from a card, bank account, exchange balance, or any wallet. The SDK accepts card, bank transfer, exchange, and wallet funding and normalizes them into a single payment flow. It handles UI, source selection, and execution. ## Available funding sources | Source | Description | | -------------------- | -------------------------------------------------------------------------------- | | Bank transfer / ACH | Direct debit from a US bank account via ACH, or wire for international transfers | | Debit or credit card | Visa and Mastercard; globally supported | | Apple Pay | One-tap funding from the Apple Pay sheet on supported browsers and devices | | Exchange account | Fund from a Coinbase, Binance, or Kraken balance directly | | Crypto wallet | Any EVM-compatible wallet; any supported token on any supported network | ## React SDK (recommended) Install the package and drop the component in: ```bash theme={null} npm install 0xtrails ``` ```tsx theme={null} import { Fund } from "0xtrails"; function FundButton() { return ( console.log("Funded:", result)} /> ); } ``` ### Default to card or bank funding Set `paymentMethod` to open the widget on the fiat tab: ```tsx theme={null} ``` ### Fund an exact output amount By default, Fund treats the user's input as `EXACT_INPUT`: the customer spends a specific amount and receives whatever that converts to. To guarantee a specific delivered amount instead, set `tradeType` to `EXACT_OUTPUT`: ```tsx theme={null} ``` ### Fund directly into a product account Pass `to.calldata` to encode a destination action that executes automatically when funds arrive. Use `dynamic()` wherever the arrived amount should appear in the encoded arguments: ```tsx theme={null} import { Fund, dynamic } from "0xtrails"; import { encodeFunctionData } from "viem"; const calldata = encodeFunctionData({ abi: VAULT_ABI, functionName: "deposit", args: [dynamic(), "0xCUSTOMER_ADDRESS"], }); ``` See [Dynamic values](/cross-chain/sdk/composable-actions/dynamic-values) for details on `dynamic()` and related placeholders. ### Fund component props | Prop | Type | Required | Description | | ------------------ | ----------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | | `apiKey` | string | Yes | Your API key | | `tradeType` | `"EXACT_INPUT"` \| `"EXACT_OUTPUT"` | No | `EXACT_INPUT` (default) fixes what the user spends; `EXACT_OUTPUT` fixes what they receive | | `to.recipient` | string | No | Wallet or contract address to receive funds | | `to.token` | string | No | ERC20 symbol or contract address to deliver | | `to.chain` | string \| number | No | Destination chain name or ID | | `to.amount` | string | Required for `EXACT_OUTPUT` | Exact amount to deliver, in token units | | `to.calldata` | string | No | ABI-encoded calldata for a destination action | | `paymentMethod` | string | No | Pre-select funding tab: `"CONNECTED_WALLET"`, `"CRYPTO_TRANSFER"`, `"CREDIT_DEBIT_CARD"`, `"EXCHANGE"` | | `onFundingStart` | function | No | Called when the user begins the flow | | `onFundingSuccess` | function | No | Called on completion | | `onFundingError` | function | No | Called on failure | For theming and appearance options, see [SDK configuration](/cross-chain/sdk/configuration). ## Non-React sites For pages without React, load the widget via CDN: ```html theme={null}
``` ## Direct API For server-side integrations with no frontend component, use the API directly. ```typescript theme={null} import { TrailsApi } from "@0xtrails/api"; const trails = new TrailsApi({ baseUrl: "https://trails-api.sequence.app/rpc/Trails/", accessKey: "YOUR_ACCESS_KEY", }); // Step 1: get a quote const { intent, feeOptions } = await trails.quoteIntent({ ownerAddress: "0xCUSTOMER_WALLET", originChainId: 1, originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", destinationChainId: 137, destinationTokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", destinationToAddress: "0xYOUR_PRODUCT_WALLET", tradeType: "EXACT_INPUT", originTokenAmount: "1000000", }); // Step 2: lock the quote const { intentId } = await trails.commitIntent({ intent }); // Step 3: execute await trails.executeIntent({ intentId, depositTransactionHash: "0xTRANSACTION_HASH", }); ``` For the full intent lifecycle and all endpoints, see the [API reference](/cross-chain/api-reference/overview). # Cross-chain payments, swaps, bridging, deposits, and actions Source: https://docs.polygon.technology/cross-chain/index One Open Money Stack API for cross-chain payments, swaps, bridging, deposits, and composable onchain actions. Route customer funds from any token on any chain to any destination in one user action. OMS includes a cross-chain orchestration layer that handles payments, swaps, bridging, deposits, and composable onchain actions through a single API. It solves a coordination problem that appears in nearly every modern fintech product: the funds a customer has available are rarely on the same network, in the same token, or at the same address as the destination where they need to land. Without orchestration, your team builds per-corridor integrations, manual bridging steps, or asks users to acquire the right asset before they can use your product. Cross-chain replaces that with a single API that handles routing, conversion, and execution automatically. ## The routing problem A customer might hold dollars in their bank, USDC on Ethereum, or a balance on Coinbase. Your product might require funds in USDC on Polygon, or in a yield vault on Base, or in stablecoins settling on Arbitrum. Getting from one to the other typically requires multiple transactions, multiple approvals, and manual coordination. Cross-chain payment orchestration handles the entire path in a single customer action. The customer expresses what they want to do; the routing layer figures out how to do it. ## What it handles automatically * **Routing**: finding the best path across chains and liquidity sources * **Conversion**: exchanging tokens at the route level, not as a separate user step * **Gas**: abstracting network fee payment so users never need native gas tokens * **Fiat on-ramps**: accepting card, bank transfer, and Apple Pay as funding sources alongside crypto ## Supported funding sources | Source | Description | | -------------------- | ------------------------------------------------------------ | | Bank account / ACH | Direct bank transfer via ACH or wire | | Debit or credit card | Visa and Mastercard, globally | | Apple Pay | One-tap payment from the Apple Pay sheet | | Exchange account | Fund directly from a Coinbase, Binance, or Kraken balance | | Crypto wallet | Any EVM wallet; any supported token on any supported network | ## How a payment flows 1. Your product calls `QuoteIntent` with the customer's funding source and the desired destination. 2. The API returns a route with fees. The customer reviews and confirms. 3. You call `CommitIntent` to lock the quote, then `ExecuteIntent` to initiate the transfer. 4. Funds route across networks, convert tokens as needed, and deliver to the destination address. 5. If a destination action is encoded (for example, a vault deposit), that action executes automatically when funds arrive. The customer sees one confirmation. Your product sees one API call per payment. ## What this enables **Neobanks and fintechs**: let customers open a yield account by funding from a card or bank transfer. The conversion and deposit happen in the background. **Banks and cross-border payments**: issue payouts to counterparties on different networks through a single orchestration layer, without building per-corridor integrations. **Marketplaces**: pay sellers in any token on any chain through one integration, regardless of where the seller holds funds. **Consumer apps**: onboard new users in their first session by accepting card or bank funding directly, without requiring prior crypto acquisition. Cross-chain payments use a separate API key from the OMS Payments API. Get yours from the [Trails Dashboard](https://dashboard.trails.build) while the two systems share authentication. ## Integration options | Option | Best for | | -------------- | -------------------------------------------------------------- | | Drop-in widget | Fastest integration; customizable UI with minimal code | | Headless SDK | Custom UI with full routing logic and state management | | Direct API | Full server-side control; build your own experience end to end | Install the SDK and drop in a payment component in under five minutes. Configure funding sources: card, bank, exchange, and wallet. Encode a vault deposit, stake, or any product action that executes when funds arrive. How fintechs, neobanks, banks, and marketplaces use cross-chain payments. Try the widget in the sandbox. Authentication, endpoints, and the cross-chain payment lifecycle. # Programmable destinations Source: https://docs.polygon.technology/cross-chain/money-actions Execute a deposit, yield position, or any product action automatically when funds arrive. A programmable destination is an action encoded into the payment intent that executes automatically when funds arrive at their destination. The customer confirms once; the funding and the product action happen together. This removes a second transaction step that would otherwise require the customer to separately initiate a deposit, stake, or other product action after funds have already arrived. ## How it works When you include destination calldata, funds are delivered to the destination contract and the encoded action is called immediately using the exact amount that arrived. You do not need to know the final amount in advance; the router substitutes it at execution time. 1. You encode the desired action (for example, a vault deposit) with a placeholder for the token amount. 2. The API quotes a route that ends at the destination contract. 3. Funds route and arrive at the contract. 4. The encoded action executes with the actual arrived amount substituted in. ## The amount placeholder When encoding calldata, call `dynamic()` in the position where the token amount should appear. Trails replaces this placeholder with the exact amount of tokens that arrive at execution time. This matters because the final amount may differ slightly from the quoted amount due to slippage or fee changes between quote and execution. Using the placeholder ensures the action always matches what actually arrived at execution time. For other placeholders like `self()` (the intent wallet address), see [Dynamic values](/cross-chain/sdk/composable-actions/dynamic-values). ## React SDK (recommended) ```tsx theme={null} import { Fund, dynamic } from "0xtrails"; import { encodeFunctionData } from "viem"; const calldata = encodeFunctionData({ abi: VAULT_ABI, functionName: "deposit", args: [dynamic(), "0xCUSTOMER_ADDRESS"], }); export function VaultFundButton() { return ( console.log("Deposited:", result)} /> ); } ``` ## Direct API For server-side or headless integrations, pass `destinationCalldata` in the quote request: ```typescript theme={null} import { TrailsApi, dynamic } from "@0xtrails/api"; import { encodeFunctionData } from "viem"; const destinationCalldata = encodeFunctionData({ abi: VAULT_ABI, functionName: "deposit", args: [dynamic(), "0xCUSTOMER_ADDRESS"], }); const trails = new TrailsApi({ baseUrl: "https://trails-api.sequence.app/rpc/Trails/", accessKey: "YOUR_ACCESS_KEY", }); const { intent, feeOptions } = await trails.quoteIntent({ ownerAddress: "0xCUSTOMER_ADDRESS", originChainId: 1, originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", destinationChainId: 137, destinationTokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", destinationToAddress: "0xYOUR_VAULT_CONTRACT", tradeType: "EXACT_OUTPUT", destinationTokenAmount: "100000000", // 100 USDC destinationCalldata, }); ``` ## Common destination actions | Action | Description | | -------------------- | -------------------------------------------------------------------------------- | | Yield vault deposit | Deposit into an ERC-4626 or compatible vault; user receives yield-bearing shares | | Staking | Stake tokens in a protocol; user earns staking rewards | | Lending deposit | Supply to a lending protocol; user earns interest | | Custom contract call | Any ABI-encoded function call on any contract at the destination | Programmable destinations combine with any funding source. A customer can fund from a debit card and deposit into a yield vault in one action, with no intermediate steps. For a full walkthrough of vault deposit integration, see the [Morpho vault deposit guide](/wallets/morpho-vault-deposit). # Building actions Source: https://docs.polygon.technology/cross-chain/sdk/composable-actions/building-actions Reference for all composable action builders: swap, lend, deposit, assertCondition, and custom. All builders are exported from `0xtrails`. Amount fields accept human-readable decimal strings (e.g., `"0.5"` for 0.5 USDC), raw `bigint` values, or `dynamic()` for runtime resolution. ## swap Executes a token exchange via Uniswap V3 or SushiSwap V3. Trade type is inferred from which amount field you provide: `amountIn` produces `EXACT_INPUT`; `amountOut` produces `EXACT_OUTPUT`. ```tsx theme={null} import { swap } from "0xtrails"; swap({ tokenIn: "USDC", tokenOut: "WETH", amountIn: "100", // EXACT_INPUT: spend exactly 100 USDC // amountOut: "0.05", // EXACT_OUTPUT: receive exactly 0.05 WETH feeTier: 3000, // optional; default 0.3% slippage: 0.005, // optional; default from provider config }) ``` | Parameter | Type | Description | | ----------- | --------------------------- | ------------------------------------------------------------- | | `tokenIn` | string | Input token symbol or address | | `tokenOut` | string | Output token symbol or address | | `amountIn` | string \| bigint \| dynamic | Amount to spend (use for EXACT\_INPUT) | | `amountOut` | string \| bigint \| dynamic | Amount to receive (use for EXACT\_OUTPUT) | | `feeTier` | number | Pool fee tier in basis points; default `3000` (0.3%) | | `slippage` | number | Slippage tolerance; overrides provider default | | `provider` | string | Force a specific swap provider (`"uniswap"` or `"sushiswap"`) | ### Low-level swap utilities For advanced use cases that need pre-flight pricing or manual call construction: | Utility | Description | | ----------------------------------------------------- | ----------------------------------------------------- | | `quoteSwap({ tokenIn, tokenOut, amountIn, chainId })` | Returns a price quote without signing | | `getPool({ tokenA, tokenB, feeTier, chainId })` | Returns the pool address for a token pair | | `swapExactInputSingle(params)` | Builds an approval + swap call array for exact input | | `swapExactOutputSingle(params)` | Builds an approval + swap call array for exact output | ## lend Supplies tokens to a lending market. Use `useEarnMarkets` to discover available `marketId` values. ```tsx theme={null} import { lend } from "0xtrails"; lend({ marketId: "aave-v3-usdc-polygon", token: "USDC", amount: dynamic(), // consume whatever arrived from the previous step }) ``` | Parameter | Type | Description | | ---------- | --------------------------- | --------------------------------------- | | `marketId` | string | Market identifier from `useEarnMarkets` | | `token` | string | Token symbol or address to supply | | `amount` | string \| bigint \| dynamic | Amount to lend | ## deposit Contributes funds to a vault-shaped market: ERC-4626, Morpho, or Yearn vaults. ```tsx theme={null} import { deposit } from "0xtrails"; deposit({ marketId: "morpho-usdc-vault", token: "USDC", amount: "500", }) ``` | Parameter | Type | Description | | ---------- | --------------------------- | -------------------------------------- | | `marketId` | string | Vault identifier from `useEarnMarkets` | | `token` | string | Token symbol or address | | `amount` | string \| bigint \| dynamic | Amount to deposit | ## assertCondition An onchain guard. If the condition evaluates to false at execution time, the entire batch reverts. Use this to validate state before continuing (minimum balance, deadline, allowance). ```tsx theme={null} import { assertCondition } from "0xtrails"; assertCondition({ type: "balance", token: "USDC", minAmount: "90", // revert if less than 90 USDC arrived }) ``` `assertCondition` does not accept `dynamic()` values, it evaluates concrete conditions against live chain state. | Parameter | Type | Description | | ----------- | -------------------------------------------- | ------------------------------------------------- | | `type` | `"balance"` \| `"deadline"` \| `"allowance"` | Condition type | | `token` | string | Token to check (for balance/allowance conditions) | | `minAmount` | string \| bigint | Minimum acceptable amount | | `deadline` | number | Unix timestamp (for deadline conditions) | ## custom An escape hatch for arbitrary contract calls. Use `encodeFunctionData` from viem to build the calldata. ```tsx theme={null} import { custom } from "0xtrails"; import { encodeFunctionData } from "viem"; custom({ target: "0xCONTRACT_ADDRESS", calldata: encodeFunctionData({ abi: MY_ABI, functionName: "myFunction", args: [arg1, arg2], }), value: 0n, // optional ETH value }) ``` | Parameter | Type | Description | | ---------- | ------ | --------------------------------------------- | | `target` | string | Contract address to call | | `calldata` | string | ABI-encoded function call | | `value` | bigint | ETH value to send with the call; default `0n` | ## ERC-20 helpers Utilities for constructing standard ERC-20 calls and looking up token metadata. ### Token registry ```tsx theme={null} import { erc20Utils } from "0xtrails"; // Canonical USDC address and decimals on Base const { address, decimals } = erc20Utils.USDC.onChain("base"); // Generic symbol lookup const weth = erc20Utils.token("WETH").onChain(137); ``` ### Calldata builders ```tsx theme={null} import { approve, transfer, buildApproveAndCall } from "0xtrails"; // Generate approve calldata (raw bigint amount) const approveTx = approve({ spender: "0xSPENDER", amount: 1000000n }); // Bundle an approve + downstream call const [approveTx, callTx] = buildApproveAndCall({ token: "0xUSDC", spender: "0xSPENDER", call: { target: "0xCONTRACT", calldata: "0x..." }, // amount defaults to maxUint256 if omitted }); ``` ### Slippage calculator ```tsx theme={null} import { getAmountWithSlippage } from "0xtrails"; // Apply 50 bps slippage to a minimum output amount const minOut = getAmountWithSlippage({ amount: 1000000n, slippageBps: 50 }); ``` # Dynamic values Source: https://docs.polygon.technology/cross-chain/sdk/composable-actions/dynamic-values Use dynamic() and self() to resolve amounts and addresses at execution time. When chaining multiple actions in a single intent, you often don't know the exact amounts or addresses until the transaction executes onchain. The SDK provides two placeholders that resolve at execution time. ## dynamic() `dynamic()` represents whatever balance the intent wallet holds at execution time. Pass it as the `amount` for any action that should consume the output of the previous step. ```tsx theme={null} import { swap, lend, dynamic } from "0xtrails"; [ swap({ tokenIn: "ETH", tokenOut: "USDC", amountIn: "1", // spend exactly 1 ETH }), lend({ marketId: "aave-v3-usdc-polygon", token: "USDC", amount: dynamic(), // lend whatever USDC the swap produced }), ] ``` This is the standard pattern for multi-step sequences. Bridge fees, slippage, and price moves mean the exact output of one step can't be known in advance. `dynamic()` eliminates the need to predict it. `dynamic()` is accepted by `swap`, `lend`, `deposit`, and `custom`. It is not accepted by `assertCondition`, which evaluates concrete conditions. ## self() `self()` resolves to the intent wallet address at execution time. The intent wallet is created per-transaction and its address is unknown before execution. ```tsx theme={null} import { deposit, self } from "0xtrails"; deposit({ marketId: "morpho-usdc-vault", token: "USDC", amount: "100", recipient: self(), // deposit on behalf of the intent wallet itself }) ``` In most cases you don't need `self()` explicitly. Swap defaults to `self()` for intermediate actions and to the user's wallet for the final action. It is available for unusual compositions where you need to be explicit about the recipient. ## Using both together ```tsx theme={null} import { swap, deposit, dynamic, self } from "0xtrails"; [ swap({ tokenIn: "USDT", tokenOut: "USDC", amountIn: dynamic(), // consume incoming USDT }), deposit({ marketId: "yearn-usdc-vault", token: "USDC", amount: dynamic(), // deposit the USDC from the swap recipient: self(), // vault shares go to the intent wallet }), ] ``` # Composable actions Source: https://docs.polygon.technology/cross-chain/sdk/composable-actions/index Chain multiple onchain actions into a single intent: bridge, swap, stake, and deposit in one user signature. Composable actions let you specify a sequence of onchain operations at the destination that execute together after funds arrive. A user signs once on their origin chain; the routing layer bridges and swaps the funds, then executes every action in the sequence from a destination intent wallet. This is how you build flows like: "fund from a card on any chain, swap to WSTETH, deposit into a yield vault", one signature, no intermediate steps for the user. ## How it works Every composable action flow is two phases: 1. The user signs a single transaction on their origin chain. Funds bridge and swap to an intent wallet on the destination. 2. The SDK executes the action sequence from that wallet. All actions run top-down in a single atomic batch, if any step fails, the entire batch reverts. ## Quickstart: multi-step DeFi in one intent This example deposits USDT into Morpho, swaps the remainder to USDC, and lends it into Aave, all in one intent. ```tsx theme={null} import { useTrailsSendTransaction, deposit, swap, lend, dynamic } from "0xtrails"; function MultiStepButton({ morphoMarketId, aaveMarketId }) { const { sendTransaction } = useTrailsSendTransaction({ to: { chain: "polygon", actions: [ // Deposit USDT into Morpho deposit({ marketId: morphoMarketId, token: "USDT", amount: "100", }), // Swap remaining funds to USDC swap({ tokenIn: "USDT", tokenOut: "USDC", amountIn: dynamic(), // consume whatever the previous step left }), // Lend the USDC output into Aave lend({ marketId: aaveMarketId, token: "USDC", amount: dynamic(), }), ], }, }); return ; } ``` `dynamic()` means "use whatever balance the intent wallet holds at execution time." You don't need to predict exact amounts after bridging or swapping. See [Dynamic values](/cross-chain/sdk/composable-actions/dynamic-values) for details. ## Track status across hops The `onStatusUpdate` callback fires at each step, bridge confirmation, destination execution, and completion. Use it to drive progress UI: ```tsx theme={null} const { sendTransaction } = useTrailsSendTransaction({ to: { chain: "polygon", actions: [...], }, onStatusUpdate: (status) => { console.log("Step:", status.step, "State:", status.state); }, }); ``` ## Quote-first pattern Use `useQuote` if you want to show the user a preview before sending, or if you want to avoid the SDK modal entirely: ```tsx theme={null} import { useQuote, deposit, dynamic } from "0xtrails"; const { quote, loading } = useQuote({ from: { chainId: 1, tokenAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT on Ethereum amount: "100", }, to: { chainId: 137, actions: [ deposit({ marketId: "morpho-usdt-vault", token: "USDT", amount: dynamic(), }), ], }, }); if (quote) { console.log("Route preview:", quote.intent.depositAddress); await quote.send(); // execute after user confirms } ``` ## Discovering markets The quickstart example above hard-codes market IDs for clarity. In production, use `useEarnMarkets` to discover available markets for a given chain, token, and provider at runtime: ```tsx theme={null} import { useEarnMarkets, lend, dynamic, useTrailsSendTransaction } from "0xtrails"; function AaveLendButton({ userAddress }) { const { markets } = useEarnMarkets({ chain: "polygon", type: "lending", provider: "aave", sortBy: "apy", }); const topMarket = markets?.[0]; const { sendTransaction } = useTrailsSendTransaction({ to: { chain: "polygon", actions: topMarket ? [lend({ marketId: topMarket.id, token: "USDC", amount: dynamic() })] : [], }, }); return ; } ``` See [React hooks](/cross-chain/sdk/hooks) for the full `useEarnMarkets` reference. ## Available action builders | Builder | What it does | | ------------------- | ------------------------------------------------------- | | `swap()` | Token exchange via Uniswap V3 or SushiSwap V3 | | `lend()` | Supply to a lending market (Aave and others) | | `deposit()` | Deposit into a vault (ERC-4626, Morpho, Yearn) | | `assertCondition()` | Onchain guard, reverts the batch if the condition fails | | `custom()` | Arbitrary contract call via ABI encoding | For the full parameter reference, see [Building actions](/cross-chain/sdk/composable-actions/building-actions). # SDK configuration Source: https://docs.polygon.technology/cross-chain/sdk/configuration Shared props reference for all Trails widget components. The SDK widgets are standalone: import any of `Fund`, `Pay`, `Swap`, `Withdraw`, or `Earn` and render them directly. There is no provider wrapper to set up. Each component takes an `apiKey` and its mode-specific configuration as props. ## Shared component props These props apply to all mode components: `Fund`, `Pay`, `Swap`, `Withdraw`, and `Earn`. ### Appearance | Prop | Type | Default | Description | | -------------- | --------------------------------- | -------- | ----------------------------------------------------------- | | `theme` | `"light"` \| `"dark"` \| `"auto"` | `"auto"` | Color scheme | | `customCss` | string \| object | none | CSS variable overrides injected into the widget | | `renderInline` | boolean | `false` | Render embedded in the page instead of as a modal | | `disableCss` | boolean | `false` | Disable all default styles for fully custom implementations | ### Wallet | Prop | Type | Description | | ------------------------ | ------- | --------------------------------- | | `walletConnectProjectId` | string | WalletConnect project ID | | `wagmiConnectors` | array | Custom wagmi connectors | | `hideDisconnect` | boolean | Hide the disconnect wallet option | | `hideAddWallet` | boolean | Hide the add wallet option | ### Route behavior | Prop | Type | Description | | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- | | `slippageTolerance` | string \| number | Per-widget slippage override | | `swapProvider` | string | Preferred swap provider | | `paymentMethod` | string | Pre-select the funding method: `"CONNECTED_WALLET"`, `"CRYPTO_TRANSFER"`, `"CREDIT_DEBIT_CARD"`, or `"EXCHANGE"` | ### Chain identifier The `chain` field accepts any of: * Chain name string: `"polygon"`, `"ethereum"`, `"base"` * Numeric chain ID: `137`, `1`, `8453` * A viem chain object ## Per-component props For Fund-, Pay-, Swap-, Withdraw-, and Earn-specific props, see the dedicated component pages. ## Customization with CSS variables Pass CSS variable overrides via the `customCss` prop. Variables can reference your design system tokens. ```tsx theme={null} ``` ### Key CSS variables | Variable | Controls | | ------------------------ | -------------------------------- | | `--trails-primary` | Primary action color | | `--trails-primary-hover` | Hover state for primary elements | | `--trails-font-family` | Widget font family | | `--trails-border-radius` | Widget container corner radius | | `--trails-focus-ring` | Keyboard focus indicator color | The full variable list (60+ variables) covers typography, spacing, backgrounds, borders, inputs, and status colors. # Earn component Source: https://docs.polygon.technology/cross-chain/sdk/earn Open the market selector and let users deposit into DeFi yield protocols from any chain or funding source. The `Earn` component opens a market selector so users can deposit into yield protocols (Aave, Morpho, Yearn, and others) from any chain or funding source. Earn currently supports open market selection only. Pre-configured deposits and contract calldata deposits are not yet available. For programmable deposits today, use [composable actions](/cross-chain/sdk/composable-actions/index) with market IDs. ## Basic usage ```tsx theme={null} import { Earn } from "0xtrails"; ``` ## Props reference | Prop | Type | Required | Description | | --------------- | -------- | -------- | --------------------------- | | `apiKey` | string | Yes | Your API key | | `onEarnStart` | function | No | Called when the user begins | | `onEarnSuccess` | function | No | Called on completion | | `onEarnError` | function | No | Called on failure | For appearance and wallet options, see [SDK configuration](/cross-chain/sdk/configuration). # React hooks Source: https://docs.polygon.technology/cross-chain/sdk/hooks Hooks for building custom payment UIs with cross-chain routing and state. All hooks are exported from `0xtrails`. No provider wrapper is required. ## Quotes ### useQuote Returns a real-time quote for a cross-chain transfer or swap. Pass human-readable decimal amounts; the hook converts to raw amounts using token decimals. ```tsx theme={null} import { useQuote } from "0xtrails"; const { quote, loading, error } = useQuote({ from: { chainId: 1, tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum amount: "100", }, to: { chainId: 137, tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon recipient: "0xYOUR_WALLET", }, }); if (quote) { await quote.send(); // execute the quoted route } ``` ### useTrailsSendTransaction Creates a button-driven transaction flow that opens the SDK modal. Accepts most `useQuote` parameters. ```tsx theme={null} import { useTrailsSendTransaction } from "0xtrails"; const { sendTransaction, loading } = useTrailsSendTransaction({ to: { chainId: 137, tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", recipient: "0xYOUR_WALLET", amount: "50", }, }); return ; ``` ## Tokens and balances ### useTokenBalances Returns token balances for an address, sorted by USD value, with price enrichment. ```tsx theme={null} const { balances, loading } = useTokenBalances("0xUSER_ADDRESS"); ``` Each balance includes `symbol`, `name`, `decimals`, `contractAddress`, `chainId`, `balance`, `balanceFormatted`, `balanceDisplay`, `balanceUsd`, and `priceUsd`. ### useTokenList Returns all tokens supported across all chains. ### useSupportedTokens Filters the token list by chain. ```tsx theme={null} const { tokens } = useSupportedTokens({ chainId: 137 }); ``` ### useTokenInfo Returns ERC-20 metadata for a specific token. ```tsx theme={null} const { token } = useTokenInfo({ address: "0xCONTRACT", chainId: 137 }); ``` ### useAccountTotalBalanceUsd Returns the aggregate USD value across all tokens for an address. ```tsx theme={null} const { totalUsd } = useAccountTotalBalanceUsd("0xUSER_ADDRESS"); ``` ### useHasSufficientBalanceToken Checks whether an address holds enough of a specific token. ### useHasSufficientBalanceUsd Checks whether the total USD balance meets a threshold. ## Chains ### useSupportedChains Returns all supported chains. ```tsx theme={null} const { chains, loading } = useSupportedChains(); ``` ### getChainInfo Returns metadata for a specific chain by ID. ## Intent state ### useGetIntent Fetches the current state of an intent by ID. Use this to display status in a custom UI. ```tsx theme={null} const { intent, loading } = useGetIntent({ intentId: "INTENT_ID" }); ``` ### useIntentRecover Recovers funds from a stuck intent. Provides a single `recover()` call plus manual control. ```tsx theme={null} const { recover } = useIntentRecover({ intentId: "INTENT_ID", walletClient, refundToAddress: "0xUSER_ADDRESS", }); ``` ### useIntentRecoverWithAddress For custodial or non-custodial wallet scenarios where you handle signing externally. Returns EIP-712 data via `getDataToSign()`. ## Earn markets ### useEarnMarkets Returns lending and vault markets, filterable by chain, provider, type, and sorted by various criteria. ```tsx theme={null} const { markets } = useEarnMarkets({ chain: "polygon", sortBy: "apy" }); ``` ### useEarnProviders Returns all supported earn providers. ### useEarnBalances Returns current positions in earn markets for a wallet address. ## Transaction history ### useAccountTransactionHistory Returns the transaction history for an address. ### useIntentTransactionHistory Returns history filtered to intent-related transactions. ## Error handling ```tsx theme={null} import { getIsUserRejectionError, getIsNoAvailableQuoteError, getIsInsufficientLiquidityError, getPrettifiedErrorMessage, } from "0xtrails"; try { await quote.send(); } catch (error) { if (getIsUserRejectionError(error)) return; // user cancelled if (getIsNoAvailableQuoteError(error)) { showError("No route available for this transfer."); return; } showError(getPrettifiedErrorMessage(error)); } ``` ### Error detection utilities | Utility | Returns true when | | --------------------------------- | ----------------------------------- | | `getIsUserRejectionError` | User rejected the transaction | | `getIsBalanceTooLowError` | Insufficient token balance | | `getIsApiError` | API returned an error | | `getIsRateLimitedError` | Rate limit hit | | `getIsNoAvailableQuoteError` | No route found | | `getIsInsufficientLiquidityError` | Insufficient liquidity on the route | ### getPrettifiedErrorMessage Converts any SDK error into a user-facing string. ## Utilities | Export | Description | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `dynamic()` | Placeholder for amounts resolved at execution time, used in `to.calldata` args or composable actions. See [Dynamic values](/cross-chain/sdk/composable-actions/dynamic-values) | | `self()` | Placeholder for the intent wallet address at execution time | | `getERC20TransferData({ recipient, amount })` | Encodes an ERC-20 transfer as calldata | | `getTrailsClient(config)` | Initializes the underlying API client outside React | | `SDK_VERSION` | Current SDK version string | # SDK quickstart Source: https://docs.polygon.technology/cross-chain/sdk/index Install the SDK and drop in a payment widget in under five minutes. Cross-chain payments use a separate API key from the OMS Payments API. Get yours from the [Trails Dashboard](https://dashboard.trails.build) while the two systems share authentication. The SDK is a React component library that handles funding flows, cross-chain routing, and fiat on-ramps. You drop in a component; the SDK handles routing, execution, and state. ## Install ```bash npm theme={null} npm install 0xtrails ``` ```bash pnpm theme={null} pnpm add 0xtrails ``` ```bash yarn theme={null} yarn add 0xtrails ``` React 18 or later is required. React 19.1+ is recommended for best compatibility. ## Drop in a component The `Fund` component opens a payment widget that accepts card, bank, exchange, and wallet funding. Import it directly and pass the destination plus an optional callback. There is no provider to set up. ```tsx FundButton.tsx theme={null} import { Fund } from "0xtrails"; export function FundButton() { return ( { console.log("Funded:", result); }} /> ); } ``` The widget renders inline or as a modal depending on your layout. See [Configuration](/cross-chain/sdk/configuration) for the full props reference. ## Fund directly into a product action Pass `to.calldata` to encode a destination action that executes automatically when funds arrive. A customer can fund from a debit card and deposit into a yield vault in one step. Use `dynamic()` in the encoded args wherever the arrived amount should appear: ```tsx VaultFundButton.tsx theme={null} import { Fund, dynamic } from "0xtrails"; import { encodeFunctionData } from "viem"; const calldata = encodeFunctionData({ abi: VAULT_ABI, functionName: "deposit", args: [dynamic(), "0xCUSTOMER_ADDRESS"], }); export function VaultFundButton() { return ( { console.log("Deposited:", result); }} /> ); } ``` See [Dynamic values](/cross-chain/sdk/composable-actions/dynamic-values) for how `dynamic()` resolves at execution time. ## Non-React sites For pages without React, load the widget via CDN: ```html theme={null}
``` For direct server-side control without a UI layer, use the [direct API](/cross-chain/api-reference/overview) instead. # Pay component Source: https://docs.polygon.technology/cross-chain/sdk/pay Accept exact-output payments where the recipient receives a precise amount regardless of the sender's source token. The `Pay` component handles exact-output payment flows. You specify the precise amount the recipient receives; the SDK figures out how much the sender needs to send based on their chosen token, chain, and current rates. ## Basic usage ```tsx theme={null} import { Pay } from "0xtrails"; console.log("Paid:", result)} /> ``` ## Fund into a contract action Pass `to.calldata` to execute a contract call alongside the payment. Use `dynamic()` in the encoded args wherever the arrived amount should appear: ```tsx theme={null} import { Pay, dynamic } from "0xtrails"; import { encodeFunctionData } from "viem"; const calldata = encodeFunctionData({ abi: NFT_ABI, functionName: "mint", args: [dynamic(), "0xBUYER_ADDRESS"], }); ``` ## Props reference | Prop | Type | Required | Description | | ------------------ | ---------------- | -------- | --------------------------------------------------------------------------------------------------------- | | `apiKey` | string | Yes | Your API key | | `to.recipient` | string | Yes | Destination wallet or contract address | | `to.token` | string | Yes | ERC20 symbol or address the recipient receives | | `to.chain` | string \| number | Yes | Destination chain | | `to.amount` | string | Yes | Exact amount the recipient receives (human-readable) | | `to.calldata` | string | No | ABI-encoded calldata to execute at the destination | | `from.token` | string | No | Pre-select the sender's source ERC20 | | `from.currency` | string | No | Pre-select the sender's source fiat currency (for fiat funding flows) | | `from.chain` | string \| number | No | Pre-select the sender's source chain | | `paymentMethod` | string | No | Pre-select funding method: `"CONNECTED_WALLET"`, `"CRYPTO_TRANSFER"`, `"CREDIT_DEBIT_CARD"`, `"EXCHANGE"` | | `onPaymentStart` | function | No | Called when the user begins the flow | | `onPaymentSuccess` | function | No | Called on completion | | `onPaymentError` | function | No | Called on failure | For appearance and wallet options, see [SDK configuration](/cross-chain/sdk/configuration). # Solana support Source: https://docs.polygon.technology/cross-chain/sdk/solana How widget modes handle EVM and Solana origin and destination addresses, and the automatic chain syncing that keeps them aligned. The widget supports cross-chain flows between EVM networks and Solana across the `Swap`, `Pay`, `Fund`, and `Withdraw` modes. The widget detects address type on both the funding side and the recipient side and aligns the origin and destination selections to whichever network the connected wallet and recipient can actually transact on. This page is a reference for that behavior. No additional component props are required to enable it: connect a Solana-compatible wallet, paste a Solana recipient, or both, and the widget reacts. ## Supported widget modes | Mode | EVM ↔ Solana | Notes | | ----------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------- | | `Swap` | Yes | Origin and destination both sync to match wallet and recipient address types. | | `Pay` | Yes | Recipient address drives destination chain and token. | | `Fund` (`EXACT_INPUT` and `EXACT_OUTPUT`) | Yes | Same recipient-driven destination sync as Swap. | | `Withdraw` | Yes | Same recipient-driven destination sync as Swap. | | `Earn` | EVM destinations only | Earn keeps the destination strict: an EVM recipient is required when the origin is on Solana. | Solana to Solana flows are unsupported. When one side of the trade is Solana, the opposite side's token and chain selectors hide Solana from the list of options. ## Origin and destination auto-sync The widget keeps the origin and destination aligned with the connected wallet and recipient address. The behavior is automatic; integrators do not need to handle these transitions in application code. ### Recipient drives destination When the recipient address changes, the destination token and chain switch to match the address type: * A Solana recipient switches the destination to a default Solana token. * An EVM recipient, while the destination is on Solana, switches the destination to a default EVM token and chain. If a previously selected destination becomes invalid (for example, an existing Solana → Solana state), the widget repairs it by switching the destination back to a default EVM token where possible. ### Funding wallet drives origin When a Solana-only wallet is selected as the funding wallet, the origin auto-selects SOL on Solana as a placeholder, then upgrades to the appropriate default origin token through the same default-token logic used elsewhere in the widget. ### Token-selector behavior The `TokenSelector` and the full-screen chain list both respect disabled chains consistently. When a previously selected chain becomes disabled (for example, after a wallet switch makes it unreachable), the selector reflects the new state rather than keeping the stale value. ## Recipient validation Recipient validation is symmetric across the two address types: * A Solana recipient on `Swap`, `Pay`, `Fund`, or `Withdraw` is valid; the destination switches to a default Solana token. * An EVM recipient on a Solana destination in those same modes is also valid; the destination switches to a default EVM chain and token. * An EVM recipient on a Solana destination in `Earn` is rejected: Earn requires an EVM destination when the origin is Solana. When the destination token is switched to Solana and no Solana-compatible wallet is connected, an existing EVM recipient is cleared and the recipient field resets to "Select recipient." This prevents the widget from submitting a quote against a recipient the destination cannot use. ## Recipients screen The recipients screen separates connected wallets from address history: * **Connected wallets** appear in their own section at the top of the picker, so users can pick a wallet they have already authenticated without retyping the address. * Connected wallets are hidden from the **Recents** list to avoid duplicate entries. * Pasting a valid address auto-selects it and returns to the previous screen. * The recents list scrolls beyond four entries. Picker-time validation is separate from submit-time validation. The widget applies picker-specific rules so that a user can browse and select recipients freely; final validation runs again when the quote is submitted. ## Funding-method UI In the `Fund` flow, wallet addresses on the Fund Methods screen are now copyable. The funding-wallet row also no longer renders a chain badge: chain badges remain on token selectors but are dropped from the recipient-address and funding-wallet rows to keep those rows focused on the address itself. For chain badges that do appear elsewhere in the widget, behavior is unchanged. ## Related references * [Swap component](/cross-chain/sdk/swap) * [Pay component](/cross-chain/sdk/pay) * [Fund](/cross-chain/fund) * [Withdraw component](/cross-chain/sdk/withdraw) * [SDK configuration](/cross-chain/sdk/configuration) # Swap component Source: https://docs.polygon.technology/cross-chain/sdk/swap Cross-chain token swaps with automatic routing across bridges and DEXs. The `Swap` component handles cross-chain token exchanges. It supports exact-input (user specifies how much to spend) and exact-output (user specifies how much to receive) modes, and automatically routes across bridges and DEXs for the best price. ## Basic usage ```tsx theme={null} import { Swap } from "0xtrails"; console.log("Swapped:", result)} /> ``` ## Exact input vs. exact output ```tsx theme={null} // Exact input: user spends a fixed amount, output varies // Exact output: user receives a fixed amount, input varies ``` ## Headless with useQuote To build your own UI and manage quote refresh yourself: ```tsx theme={null} import { useQuote } from "0xtrails"; const { quote, loading, refetch } = useQuote({ from: { chainId: 1, tokenAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH amount: "0.1", }, to: { chainId: 137, tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon recipient: "0xUSER_WALLET", }, onStatusUpdate: (status) => console.log("Status:", status), }); // Refresh quote every 30 seconds useEffect(() => { const interval = setInterval(refetch, 30_000); return () => clearInterval(interval); }, [refetch]); ``` ## Props reference | Prop | Type | Required | Description | | ------------------- | ---------------- | -------- | ------------------------------------------------ | | `apiKey` | string | Yes | Your API key | | `from.token` | string | No | Source ERC20 symbol or address | | `from.chain` | string \| number | No | Source chain | | `from.amount` | string | No | Amount to spend (exact input) | | `to.token` | string | No | Destination ERC20 symbol or address | | `to.chain` | string \| number | No | Destination chain | | `to.amount` | string | No | Amount to receive (exact output) | | `slippageTolerance` | string \| number | No | Slippage tolerance; default from provider config | | `swapProvider` | string | No | Preferred DEX provider | | `bridgeProvider` | string | No | Preferred bridge provider | | `paymentMethod` | string | No | Pre-select funding method | | `onSwapStart` | function | No | Called when the user begins | | `onSwapSuccess` | function | No | Called on completion | | `onSwapError` | function | No | Called on failure | For appearance and wallet options, see [SDK configuration](/cross-chain/sdk/configuration). # Swap & bridge Source: https://docs.polygon.technology/cross-chain/swap-bridge Let customers move and convert funds across any network in a single step, without managing bridges or liquidity yourself. Cross-chain asset movement is one of the hardest infrastructure problems to build in-house. Your customers may hold ETH on Ethereum, but your product settles in USDC on Polygon. They may receive a payout in one token and want another. Getting from one to the other typically requires multiple transactions, gas on every chain involved, and UX that stops most users cold. Cross-chain payments handle the entire path (routing, bridging, and swapping) in a single customer action. ## What this enables **Token conversion at settlement**: Accept any token from customers and settle in the token your product uses. A customer paying with ETH can send the exact USDC equivalent to your contract on Polygon, without a separate conversion step. **Cross-network fund movement**: Move customer funds between networks as part of normal product operations, without your team managing bridge infrastructure or liquidity positions. **Portfolio rebalancing**: Let customers rebalance holdings across chains in one action. They specify what they want to end up with; the routing layer finds the optimal route. **FX-like stablecoin conversion**: Swap between stablecoins across networks (e.g., USDT on Tron to USDC on Polygon) as a backend settlement primitive. ## How it works The routing layer handles bridges and DEXs automatically, finding the best path for each transfer. You specify the source and destination; it handles what happens in between. ```tsx theme={null} import { Swap } from "0xtrails"; console.log("Settled:", result)} /> ``` The customer selects how much to send. The SDK quotes the route, the customer confirms once, and funds arrive on Polygon as USDC. ## Exact input vs. exact output Two trade modes cover the main product scenarios: | Mode | Use when | | ------------ | --------------------------------------------------------------------------- | | Exact input | Customer specifies how much to send; they receive whatever that converts to | | Exact output | Customer receives a fixed amount; they pay whatever is required | Exact output is the right model for payments and settlements where the destination amount must be precise. Exact input suits exchange and conversion flows where the customer controls the spend. ## Headless integration For custom UIs or server-driven flows, use `useQuote` to get a route and execute it programmatically: ```tsx theme={null} import { useQuote } from "0xtrails"; const { quote, loading } = useQuote({ from: { chainId: 1, tokenAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH on Ethereum amount: "0.5", }, to: { chainId: 137, tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon recipient: "0xSETTLEMENT_ADDRESS", }, }); if (quote) { await quote.send(); } ``` For a full reference on quote parameters and swap configuration, see the [Swap component](/cross-chain/sdk/swap) and [API reference](/cross-chain/api-reference/overview). # Use cases Source: https://docs.polygon.technology/cross-chain/use-cases How fintechs, neobanks, banks, and marketplaces use cross-chain payment orchestration. Cross-chain payment orchestration handles the routing, conversion, and settlement layer so product teams can focus on the customer experience rather than per-corridor infrastructure. The use cases below reflect how different categories of financial product apply orchestration. ## Yield accounts for neobanks and fintechs A customer opens a savings or yield account and funds it from their debit card or bank account. Without orchestration, that requires a card-to-crypto on-ramp, a manual transfer to the right network, and a vault deposit transaction. With cross-chain orchestration, those steps collapse into one. Your product encodes the vault deposit as a destination action. The SDK handles the on-ramp, routing, and deposit in a single intent. The customer funds their account in the same session they open it. See [programmable destinations](/cross-chain/money-actions) for how to encode vault deposits. ## Cross-border stablecoin payouts A bank or payments company issues payouts to counterparties who may hold accounts on different networks. Building per-corridor logic, Ethereum, Polygon, Arbitrum, Base, is expensive and operationally fragile. Cross-chain payments provide a single orchestration layer across all corridors. Your system submits a payout intent specifying the destination address and token; the routing layer handles cross-network settlement. One integration covers all corridors, and new network support requires no changes on your side. ## Marketplace seller payouts Sellers on a marketplace may prefer to receive in different tokens or on different networks. Managing that diversity typically means either restricting what sellers can receive or building multiple payout integrations. With cross-chain payments, you submit a payout to any destination address in any supported token on any supported chain. The seller receives in the token and on the network they specify. One integration covers all combinations. ## Consumer app onboarding Users who are new to a product often lack the right asset in the right place before they can get started. Asking them to acquire a specific token before onboarding loses conversions. The SDK accepts card, bank, and exchange funding sources, so new users can fund directly in their first session. No prerequisite crypto acquisition. No wallet setup before the product delivers value. ## Institutional cross-chain settlement An institution settles a transaction on one chain, but its counterparty requires settlement on another. Without orchestration, this requires manual bridging, separate reconciliation, and coordination across operations teams. Cross-chain routing handles settlement automatically. The institution submits a settlement intent to the counterparty's address on their preferred network. The routing layer settles across chains. Reconciliation matches against the intent receipt rather than multiple transaction hashes across chains. # Polygon Developer Docs Source: https://docs.polygon.technology/index Polygon developer documentation: the Open Money Stack API for stablecoin payments, non-custodial wallets, on-ramps and off-ramps, cross-chain settlement, and the Polygon Chain, CDK, and Agglayer infrastructure that powers it.
# Polygon infrastructure: Polygon Chain, CDK, Agglayer, and agentic Source: https://docs.polygon.technology/infrastructure/index The permissionless foundation of the Open Money Stack. Polygon Chain for public settlement, Polygon CDK for private blockchains, Agglayer for cross-chain interoperability, and agentic infrastructure for autonomous AI agent payments. The infrastructure layer of the Open Money Stack is permissionless. No API key, no account approval, no registration. The payments and wallet layers run on licensed, compliance-gated infrastructure. This layer is different: public chains, open protocols, and open-source tooling that any developer can access directly with a standard EVM wallet. Four components make up this layer: [Polygon Chain](#polygon-chain) for public settlement, [Polygon CDK](#polygon-cdk) for dedicated rollup infrastructure, [Agglayer](#agglayer) for cross-chain interoperability, and [Agentic infrastructure](#agentic) for autonomous agent payments. *** ## Polygon Chain **Public, Permissionless.** Polygon Chain is the public, permissionless blockchain that serves as the default settlement of the Open Money Stack. No API key required. Anyone can connect a wallet, deploy a contract, and transact. Sub-5 second finality, 3,800 TPS sustained throughput, \$0.002 average transaction cost, and full EVM compatibility mean it works with Hardhat, Foundry, Ethers.js, and Wagmi without modification. With 159M unique wallet addresses and \$54B in stablecoin transfer volume, Polygon Chain is production-proven at the scale financial applications require. [Polygon Chain overview, architecture, network stats, and how to start building.](/pos/overview) *** ## Polygon CDK **Build private blockchains. Connect to public liquidity.** For institutions that need dedicated, private blockspace but also connection to broad crypto liquidity, Polygon CDK provides a composable, privacy-on-a-spectrum selection of features for financial institutions: custom throughput, custom fee structures, and compliance-grade controls. Polygon partners with you to design and launch a bespoke chain; CDK is the product, not a self-serve kit. Every CDK chain ships with Agglayer connectivity, so it joins the broader blockchain ecosystem from day one. Operators choose between sovereign (pessimistic proof), validium, and private validium operating modes. CDK supports 20,000+ TPS when tuned for payment workloads, with granular network controls: gated access, API keys, and ACLs for read and write permissions. [Polygon CDK docs, operating modes, Agglayer connectivity, and how to launch a bespoke chain.](/chain-development/cdk/index) *** ## Choosing between Polygon Chain and Polygon CDK | | Polygon Chain | Polygon CDK | | -------------------- | ------------------------------------------- | ----------------------------------------------------------------- | | **Access** | Public, permissionless | Private or gated | | **Throughput** | 3,800 TPS | 20,000+ TPS (payment-optimized) | | **Fee control** | Network-determined | Operator-defined | | **Compliance** | Application-level | Chain-level controls, ACLs, API keys | | **Infrastructure** | Shared public network | Dedicated, private blockspace | | **Interoperability** | Native ecosystem access | Via Agglayer | | **Best for** | Open apps, broad reach, ecosystem liquidity | Institutions with regulatory, privacy, or throughput requirements | Most applications start on Polygon Chain. Institutions with regulatory or privacy requirements, dedicated throughput needs, or custom fee structures work with Polygon to launch a bespoke CDK chain that stays connected to the broader ecosystem via Agglayer. *** ## Agglayer **Secure cross-chain bridge for the Open Money Stack.** Agglayer is a secure cross-chain bridge that connects the liquidity and users of heterogeneous blockchains in a single interoperability protocol so assets can move between them. It is the secure foundation the Open Money Stack uses for cross-chain payments. A pessimistic proof system ensures that a compromised chain cannot drain more than its own deposits into the shared pool. Connected chains retain their own architecture and governance. Agglayer is bundled with Polygon CDK: every CDK chain ships with Agglayer connectivity by default. Other chains can integrate independently, and the network is no longer EVM-only since Miden joined as a non-EVM connected chain. [Agglayer overview, architecture, security model, and how to connect a chain or build cross-chain applications.](/interoperability/overview) *** ## Agentic **Payments for autonomous agents.** The agentic layer gives software agents the infrastructure to initiate, negotiate, and settle payments without human confirmation at each step. An agent can detect a payment requirement, authorize a transaction, and settle onchain using predefined policies and earned balances. Three components work together: agentic wallets for agent key management and signing, x402 Protocol for HTTP-native pay-per-call payments using the standard 402 status code, and ERC-8004 for onchain agent identity and reputation registries. [Agentic infrastructure, agentic wallets, x402 protocol, and onchain agent identity.](/wallets/agentic-wallets) # Architecture Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/architecture AggKit's three-tier architecture: chain domain, synchronization components, and the Agglayer ecosystem, with certificate submission and GER propagation flows ## Overview Traditional blockchain architecture assumes you're dealing with a single network. In the Agglayer ecosystem, you're dealing with multiple sovereign chains that each have their own block times, execution environments, and state management. AggKit provides the synchronization layer that keeps all of them coordinated. ## The Three-Tier Architecture AggKit's architecture has three distinct tiers, each with a specific purpose: ### Tier 1: Your Chain's Domain At the foundation, you have **your L2 chain** doing what it does best – processing transactions, executing smart contracts, maintaining state. This is your domain, where you have full sovereignty and control. When users perform bridge operations on your chain, several things happen simultaneously: bridge contracts emit events, state gets updated, and your chain needs to communicate these changes to the broader ecosystem. This is where AggKit steps in. ### Tier 2: The AggKit Synchronization Layer In the middle tier, AggKit components work together to maintain synchronization. Each component has a specialized role: Aggkit *Figure 1: The three-tier architecture – your chain, AggKit synchronization, and the broader ecosystem* ### Tier 3: The Unified Ecosystem At the top tier, you have **the broader Agglayer ecosystem** – Agglayer itself, Ethereum L1, and all the other chains connected to the network. This is where the global state lives, where final settlement happens, and where the unified liquidity that makes everything possible is maintained. ## Data Flow Architecture AggKit handles two directions of communication that keep chain state synchronized with Agglayer: #### **Upward Flow: L2 → Agglayer** **Purpose**: Submits L2 state transitions to Agglayer for validation and proof generation. **Components Involved**: * **BridgeSync**: Captures bridge events from L2 contracts * **L1InfoTreeSync**: Provides L1 verification data and Merkle proofs * **AggSender**: Packages data into signed certificates and submits to Agglayer #### **Downward Flow: Agglayer → L2** **Purpose**: Propagates global state updates from Agglayer/L1 to L2 chains for claim verification. **Components Involved**: * **L1InfoTreeSync**: Monitors L1 for Global Exit Root updates * **AggOracle**: Propagates GER updates to L2 contracts (with v0.3.5 committee security) * **L2GERSync**: Indexes and manages GER state locally on L2 ## Component Interaction Patterns ### **Certificate Generation Pattern** *Figure 2: Certificate generation and submission pattern* ### **Oracle Propagation Pattern** *Figure 3: GER propagation with v0.3.5 committee security* ### **v0.3.5 Security Enhancements** The major architectural improvement in v0.3.5 is the **elimination of single-address vulnerabilities**: #### **Before v0.3.5: Single Point of Failure** **Risk**: Single compromised address could steal funds or mint unauthorized assets. #### **After v0.3.5: Distributed Security** **Security**: Multiple parties must agree before any GER injection, eliminating single points of failure. # AggchainProofGen Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/aggchain-proof-gen How AggchainProofGen generates state transition proofs for chains requiring mathematical verification of internal operations beyond ECDSA signature authorization ## What AggchainProofGen Does AggchainProofGen generates state transition proofs for chains that require mathematical verification of internal operations. While chains using a trusted sequencer model can use simple ECDSA signature authorization, some chains need comprehensive proofs that verify both internal operations and cross-chain bridge activities. AggchainProofGen implements the chain-side component of the [State Transition Proof](/interoperability/agglayer/core-concepts/state-transition-proof/) system introduced in Agglayer v0.3. It verifies the chain's consensus mechanism, verifies bridge constraints, and produces an Aggchain Proof that AggSender includes in its certificate. ## When AggchainProofGen Is Needed AggchainProofGen is required for chains that use `CONSENSUS_TYPE = 1` (generic validity proof) rather than `CONSENSUS_TYPE = 0` (ECDSA signature). This applies to: * **Zero-knowledge rollups**: Chains that generate zk-SNARKs or zk-STARKs for state transitions * **Custom consensus chains**: Chains with consensus mechanisms that cannot be authorized by a single trusted sequencer address * **High-security deployments**: Chains where mathematical certainty about state correctness is required rather than trusted-party authorization Chains using ECDSA authorization do not need AggchainProofGen and can operate with AggSender alone. ## How AggchainProofGen Works AggchainProofGen implements a dual verification process: **Step 1: Consensus verification.** AggchainProofGen verifies the chain's consensus: either validating an ECDSA signature from the trusted sequencer, or verifying a validity proof using the SP1 zkVM. **Step 2: Bridge constraint verification.** AggchainProofGen verifies that all bridge operations comply with the Unified Bridge security constraints. This includes GER hash chain validation, claims hash chain validation, Local Exit Root correctness, and GER Merkle proof inclusion in the L1 Info Root. **Step 3: Proof generation.** AggchainProofGen generates an Aggchain Proof combining both verifications. AggSender includes this proof in the certificate it submits to Agglayer. ## Proof Generation Modes ### ECDSA Mode For chains with trusted sequencer models, AggchainProofGen verifies operations using ECDSA signature validation. This provides fast verification while maintaining compatibility with existing chain architectures. ### Validity Proof Mode For chains requiring mathematical certainty, AggchainProofGen generates validity proofs using zero-knowledge virtual machines. This provides cryptographic certainty about the correctness of both internal operations and bridge activities, without relying on a trusted party. ## Integration with AggSender When a chain uses AggchainProofGen, AggSender operates in AggchainProver mode: For the full context of how Aggchain Proof fits into the verification pipeline, see [State Transition Proof](/interoperability/agglayer/core-concepts/state-transition-proof/) and [Aggchain Proof](/interoperability/agglayer/core-concepts/state-transition-proof/aggchain-proof/). # AggOracle Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/aggoracle How AggOracle propagates Global Exit Root updates from Ethereum L1 to L2 chains, and the v0.3.5 committee mode security improvement ## What AggOracle Does AggOracle propagates Global Exit Root (GER) updates from Ethereum L1 to the L2 chain's Global Exit Root Manager contract. This synchronization keeps the L2 chain's local copy of global state current, which is required for verifying incoming cross-chain claims. When a user bridges assets from another chain to your chain and submits a claim, your chain verifies the claim against the current GER. Without a current GER, the verification fails and claims cannot be processed. ## The Synchronization Problem Cross-chain claim verification requires L2 chains to have current GER data from Ethereum L1. The two chains operate independently with different block times and finality requirements, so the L2 cannot query L1 directly on demand. AggOracle resolves this by implementing a pull-based synchronization mechanism: it continuously monitors L1 for GER changes and injects updates into the L2 contract whenever a new GER is detected. ## The Two Operating Modes ### Direct Injection Mode (Pre-v0.3.5) In the original design, AggOracle used single-address authorization for GER injection. One designated address had the authority to call `insertGlobalExitRoot()` on the L2 Global Exit Root Manager contract. **Vulnerability**: If the single private key is compromised, an attacker could inject invalid GERs, causing the chain to accept fraudulent bridge claims. ### Committee Mode (v0.3.5) v0.3.5 introduces multi-party consensus. Multiple independent AggOracle instances must agree before any GER injection occurs. **How it works:** 1. Multiple AggOracle committee members independently monitor L1 for GER updates 2. When a new GER is detected, one member proposes it to the committee contract 3. Other members validate and vote by proposing the same GER 4. The committee contract automatically injects the GER when the threshold quorum is reached **Security improvement**: Even if some committee members are compromised, the system remains secure because multiple independent parties must reach consensus before any state update occurs. ## Full GER Propagation Workflow ## Understanding Global Exit Roots A [Global Exit Root (GER)](/interoperability/agglayer/core-concepts/unified-bridge/data-structures/#global-exit-root) is a cryptographic hash that represents the current state of all cross-chain bridge activities across the Agglayer ecosystem: ``` GER = hash(RollupExitRoot, MainnetExitRoot) ``` Where: * **RollupExitRoot**: Aggregated root of all L2 chains' Local Exit Roots * **MainnetExitRoot**: Root of all Ethereum L1 bridge transactions Cross-chain claims require proof verification against the current GER. When users submit bridge claims to your chain, the claim proofs must reference a GER that your chain has recorded as valid. # AggSender Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/aggsender How AggSender packages L2 state transitions into cryptographically signed certificates and submits them to Agglayer for Pessimistic Proof generation ## What AggSender Does AggSender is the component responsible for submitting L2 state transitions to Agglayer. It collects bridge events and L1 verification data, packages them into a signed certificate, and submits that certificate to Agglayer on each epoch. Agglayer then uses the certificate to generate a Pessimistic Proof and validate the state transition. Without AggSender, an L2 chain cannot participate in Agglayer's security guarantees or cross-chain coordination. Every chain connected to Agglayer must run AggSender. ## The Trust Problem AggSender Solves Agglayer connects multiple independent blockchains, and any of them could theoretically be compromised. The Pessimistic Proof system limits the damage a compromised chain can do, but it needs reliable input data to work correctly. AggSender provides that input. Rather than simply reporting what happened on a chain, it creates a signed certificate that contains: * Cryptographic proof that bridge transactions occurred * Evidence that the chain has sufficient funds to back those transactions * Mathematical verification data that the chain's state transitions are valid * A digital signature committing to all of the above This allows Agglayer to verify the submission rather than trust it. ## Certificate Generation ### Epoch-Based Operation AggSender submits certificates on Agglayer epochs rather than per-transaction. On each epoch, it collects all bridge activity since the last certificate, packages it, and submits once. This batching is more efficient than per-transaction submissions and provides comprehensive context about the chain's activities. ### Certificate Lifecycle ### Certificate Structure ```go theme={null} type Certificate struct { NetworkID uint32 // L2 network identifier Height uint64 // Certificate sequence number PrevLocalExitRoot common.Hash // Previous Local Exit Root NewLocalExitRoot common.Hash // New Local Exit Root BridgeExits []BridgeExit // Outgoing bridge transactions ImportedBridgeExits []ImportedBridgeExit // Incoming bridge claims Signature []byte // Cryptographic signature Metadata []byte // Additional chain-specific data } ``` ### Certificate Sizing Limits Chain operators can bound certificate size with two `AggSender` configuration options: | Option | Type | Description | | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MaxL2BlockNumber` | `uint64` | Highest L2 block number that a certificate may include. AggSender stops submitting once coverage reaches this block. `0` disables the limit. | | `MaxL2BlockRange` | `uint64` | Maximum span (`ToBlock - FromBlock`) of a single certificate. AggSender splits work so that no certificate covers a wider range. `0` disables the limit. | `MaxL2BlockNumber` caps the absolute end block; `MaxL2BlockRange` caps the width of each certificate. The two limits compose: AggSender shrinks the current certificate to satisfy whichever is reached first. Both limits apply across Pessimistic Proof, FEP, optimistic, builder, and validator flows. The Validator inherits `MaxL2BlockRange` from `AggSender` by default. ## Full Certificate Submission Flow *Note: AggchainProofGen is only involved when the chain uses the advanced state transition proof system. Chains using basic ECDSA authorization do not require AggchainProofGen.* # BridgeSync Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/bridge-sync How BridgeSync monitors and indexes bridge events from L1 and L2 networks, and how it provides data to AggSender and Bridge Service ## What BridgeSync Does BridgeSync monitors bridge contracts on both L1 and L2 networks, captures every bridge-related event, and organizes the data into a local database. It provides the data foundation that other AggKit components depend on: AggSender uses it for certificate construction, and Bridge Service uses it to serve API responses. **Key responsibilities:** * **Event monitoring**: Real-time monitoring of bridge contract events on L1 and L2 * **Data indexing**: Comprehensive indexing of bridge and claim transactions * **State management**: Maintains bridge transaction history and status * **Reorg handling**: Manages blockchain reorganizations and maintains data integrity * **API support**: Provides data to Bridge Service APIs ## Why Indexing Matters Bridge claim verification and status queries require access to bridge transaction history. Without an index, each query would require expensive, slow queries to blockchain nodes or onchain contract calls. BridgeSync captures events as they occur and makes them instantly queryable. AggSender depends on BridgeSync's data to build certificates: it needs the list of bridge exits and imported bridge exits for each epoch. Bridge Service depends on it to respond to transaction status requests and proof generation queries. ## Architecture ## How BridgeSync Works ### Event Processing Workflow ### Bridge Event Types #### BridgeEvent Emitted when assets or messages are bridged from a network: ```solidity theme={null} event BridgeEvent( uint8 leafType, // 0 = asset, 1 = message uint32 originNetwork, // Source network ID address originAddress, // Sender address uint32 destinationNetwork, // Destination network ID address destinationAddress, // Recipient address uint256 amount, // Amount (for assets) bytes metadata, // Additional data uint32 depositCount // Index in Local Exit Tree ); ``` #### ClaimEvent Emitted when assets or messages are claimed on a network: ```solidity theme={null} event ClaimEvent( uint256 globalIndex, // Global transaction index uint32 originNetwork, // Source network ID address originAddress, // Original sender address destinationAddress, // Claim recipient uint256 amount // Claimed amount ); ``` ### Data Processing Pipeline ## Integration with Other Components ### AggSender Integration BridgeSync provides the bridge exit and imported bridge exit data that AggSender packages into certificates: **Data provided:** * Bridge exits (outbound transactions from the L2) * Imported bridge exits (inbound claims to the L2) * Transaction proofs and metadata * Block range information for certificate scope ### Bridge Service Integration BridgeSync is the primary data source for Bridge Service API endpoints: All Bridge Service endpoints query BridgeSync's local database for transaction data. # Components Overview Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/index The 7 AggKit components, their roles, dependencies, and which combinations are appropriate for different deployment scenarios ## Modular Design AggKit consists of 7 specialized components. Rather than a single monolithic service, each component handles a specific responsibility. This means you deploy only the components your use case requires. * **Basic Agglayer connectivity**: 2 components (AggSender + AggOracle) * **Full bridge infrastructure**: 5 components (add BridgeSync, L1InfoTreeSync, L2GERSync) * **Bridge API services**: 4 components (BridgeSync, L1InfoTreeSync, L2GERSync, Bridge Service) * **Advanced state transition proofs**: Add AggchainProofGen ## Communication Components These two components are required for any chain connecting to Agglayer. ### AggSender AggSender packages L2 state transitions into cryptographically signed certificates and submits them to Agglayer. On each Agglayer epoch, it collects bridge events from BridgeSync and L1 verification data from L1InfoTreeSync, builds a certificate containing bridge exits and imported bridge exits, signs it, and submits it to Agglayer for Pessimistic Proof generation. Without AggSender, a chain cannot participate in Agglayer's security guarantees or cross-chain coordination. ### AggOracle AggOracle monitors the Global Exit Root contract on Ethereum L1, detects new GER updates, and injects those updates into the L2 chain's Global Exit Root Manager contract. This keeps the chain's local copy of global state current, which is required for verifying incoming cross-chain claims. In v0.3.5, AggOracle operates in committee mode: multiple independent instances must agree on a new GER before it is injected into the L2 contract, eliminating the single-address vulnerability of earlier versions. ## Synchronization Components These components maintain the data that AggSender and AggOracle depend on. ### L1InfoTreeSync L1InfoTreeSync monitors Ethereum L1 and maintains two Merkle trees locally: * **L1 Info Tree**: An append-only tree of historical Global Exit Roots, sourced from `UpdateL1InfoTree` events on the Global Exit Root contract * **Rollup Exit Tree**: A tree of L2 Local Exit Roots, sourced from `VerifyBatches` events on the Rollup Manager contract These trees provide the Merkle proofs that AggSender includes in certificates and that AggOracle uses for GER detection. ### L2GERSync L2GERSync indexes GER injections on the L2 side. When AggOracle writes a new GER to the L2 Global Exit Root Manager contract, L2GERSync captures the event and stores it locally. This local index enables fast GER lookups during claim verification and API responses without requiring onchain queries. ### BridgeSync BridgeSync monitors bridge contracts on both L1 and L2, captures every `BridgeEvent` and `ClaimEvent`, and organizes them into a local database. It provides the bridge exit and imported bridge exit data that AggSender uses for certificate construction, and the transaction history that Bridge Service exposes through its API. ## Service Components These components extend AggKit with external-facing capabilities. ### Bridge Service Bridge Service exposes a REST API over BridgeSync's data. It provides endpoints for bridge transaction status, token mappings, and Merkle proof generation. Applications such as wallets, block explorers, and DeFi protocols use Bridge Service to query bridge data without running the full AggKit infrastructure. ### AggchainProofGen AggchainProofGen generates state transition proofs for chains that require mathematical verification of internal operations rather than simple ECDSA signature authorization. It verifies the chain's consensus (either ECDSA signature or validity proof), verifies bridge constraints, and produces an Aggchain Proof that AggSender includes in its certificate. See [State Transition Proof](/interoperability/agglayer/core-concepts/state-transition-proof/) for the full context of when this is needed. ## Component Dependencies ## Individual component pages Certificate generation, epoch-based operation, and the certificate lifecycle. GER propagation, direct injection vs. committee mode, and the v0.3.5 security change. Bridge event monitoring, data indexing, event types, and integration with AggSender and Bridge Service. The two Merkle trees maintained, event sources, Merkle proof generation, and integration with AggSender and AggOracle. Local GER state management on the L2 side, indexing workflow, and how it supports claim verification. State transition proof generation for chains requiring mathematical verification of internal operations. # L1InfoTreeSync Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/l1infotree-sync How L1InfoTreeSync monitors Ethereum L1 and maintains the L1 Info Tree and Rollup Exit Tree for proof generation and certificate building ## What L1InfoTreeSync Does L1InfoTreeSync monitors Ethereum L1 and maintains two local Merkle trees that other AggKit components use for proof generation and certificate building: 1. **L1 Info Tree**: An append-only tree tracking historical Global Exit Root updates from the Global Exit Root contract 2. **Rollup Exit Tree**: An updatable tree tracking rollup state submissions from the Rollup Manager contract Different AggKit operations need different types of L1 data. The L1 Info Tree provides historical GER context for claim verification proofs. The Rollup Exit Tree provides rollup state data for certificate construction. **Key responsibilities:** * Maintaining the L1 Info Tree and Rollup Exit Tree from L1 events * Generating Merkle proofs for cross-chain verification * Handling blockchain reorganizations and maintaining data integrity * Respecting configurable finality requirements (latest, safe, finalized) ## Why L1 State Indexing Matters Cross-chain operations require accurate L1 state for several functions: 1. **Proof generation**: Merkle proofs must reference correct historical L1 states 2. **Certificate building**: AggSender needs L1 data to construct valid certificates 3. **Claim verification**: Cross-chain claims must be verified against settled L1 state 4. **Reorg handling**: L1 reorganizations must be detected and handled Rather than querying L1 on demand for each operation, L1InfoTreeSync maintains a local index that makes proof generation fast and efficient. ## Architecture ## How L1InfoTreeSync Works ### Event Processing L1InfoTreeSync monitors two types of events from Ethereum L1 and processes them into separate tree structures: ### The Two Tree Types **L1 Info Tree (append-only):** * **Source**: `UpdateL1InfoTree` events from the Global Exit Root contract * **Structure**: Append-only tree that grows with each Global Exit Root update * **Purpose**: Provides historical GER data for claim verification * **Use case**: When claim proofs must demonstrate inclusion in a specific historical state **Rollup Exit Tree (updatable):** * **Source**: `VerifyBatches` events from the Rollup Manager contract * **Structure**: Updatable tree where L2 chains can update their submitted state * **Purpose**: Tracks which L2 chains have submitted state and their current exit roots * **Use case**: When AggSender builds certificates that need rollup state context ### Tree Structures #### L1 Info Tree #### Rollup Exit Tree ## Integration with Other Components ### AggSender Integration L1InfoTreeSync provides the L1 data that AggSender includes in certificates: **Data provided:** * Current L1 Info Tree root and leaf data * Merkle proofs for imported bridge exits * L1 block finality information * Historical Global Exit Root data ### AggOracle Integration L1InfoTreeSync feeds GER detection into the AggOracle pipeline: # L2GERSync Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/aggkit/components/l2ger-sync How L2GERSync indexes Global Exit Root updates on the L2 side and provides fast local GER state access for claim verification and Bridge Service APIs ## What L2GERSync Does L2GERSync manages Global Exit Root synchronization on the L2 side. When AggOracle injects a new GER into the L2 Global Exit Root Manager contract, L2GERSync captures the resulting event and stores the GER data in a local index. This local index provides fast access to current and historical GER state for several operations: 1. **Claim verification**: Bridge claims must be verified against the current GER 2. **Proof generation**: Merkle proofs require accurate GER state 3. **API responses**: Bridge Service APIs need fast GER data access 4. **Historical queries**: Applications may need access to historical GER transitions Without L2GERSync, each of these operations would require onchain queries or external service calls, adding latency and external dependencies. ## Architecture ## How L2GERSync Works ### GER Synchronization Workflow L2GERSync automatically detects your contract's capabilities at startup and configures itself for compatibility with the deployed version of the L2 GER Manager contract. # Architecture Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/architecture Agglayer's technical architecture: the Agglayer node, Pessimistic Proof, Unified Bridge, and State Transition Proof and how they coordinate ## Overview Agglayer provides a cross-chain interoperability framework built around four main components: the Agglayer node, the Pessimistic Proof, the Unified Bridge, and the State Transition Proof. Together they enable secure, verifiable cross-chain transactions between heterogeneous blockchain networks. ## High-Level Architecture Agglayer Overall Data Flows *Figure 1: Agglayer Overall Data Flows - showing transaction sequencing, settlement, and L1-L2 synchronization processes* ## Core Components ### 1. Agglayer Node The Agglayer Node is a Rust-based service responsible for processing and verifying zero-knowledge (ZK) proofs from chains connected to the Agglayer. **Key Functions:** * **Zero-knowledge proof verification**: The node receives and verifies cryptographic proofs from connected chains before sending them to L1 * **Certificate management**: Handles certificates that attest to the state transitions of connected chains * **Orchestration of epochs**: Manages state updates in a structured manner through epochs ### 2. Pessimistic Proof The pessimistic proof mechanism ensures that any withdrawal claims made to the Agglayer are backed by legitimate deposits in the Unified Bridge. It uses a novel zero-knowledge proof system implemented in Rust, leveraging the SP1 zkVM and the Plonky3 proving system. **Key Functions:** * **Security validation**: Ensures that each chain connected to the Agglayer remains as secure as if it were operating independently * **State consistency**: Provides a complete view of all token and message transfers occurring across the Agglayer * **Fraud prevention**: Prevents chains from withdrawing more assets than they have legitimately received ### 3. Unified Bridge The unified bridge is responsible for maintaining the data structures related to chain states, cross-chain transactions, and the Agglayer's Global Exit Root, ensuring cross-chain transactions are indeed finalized on the L1 before they can be claimed. **Key Functions:** * **Cross-chain asset transfers**: Allows users to bridge assets between different chains * **Message passing**: Enables contract-to-contract interactions across chains * **State management and accounting**: Maintains Merkle proofs that ensure transactions are finalized before being processed on the destination chain ### 4. State Transition Proof The State Transition Proof is a two-layer verification system that validates both individual chain operations and cross-chain transfers before Agglayer accepts a state update. **How it works:** **State Transition Proof (Validity Proof)**: This layer verifies that each chain's internal state transitions are valid. Every operation within the chain is verified against the chain's execution rules, and the chain's resulting state must be consistent. Additional verification types can be added in the future without changing Agglayer's external interface. **Cross-Chain Verification (Aggchain Proof and Pessimistic Proof)**: This layer verifies that cross-chain operations, such as asset transfers between chains, are valid. It ensures that when assets move between chains, operations are atomic and secure. **Key Functions:** * **End-to-end security**: A transaction is finalized only when both its internal validity proof and its cross-chain proof are accepted * **Atomic cross-chain execution**: Guarantees that assets and messages move between chains in a single, indivisible step * **Modular extensibility**: New proof mechanisms (optimistic, fraud, etc.) can be integrated without altering Agglayer's external interface # Architecture Overview Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/pessimistic-proof/architecture How Pessimistic Proofs are generated and validated, and the financial isolation guarantees they provide ## Overview The Pessimistic Proof is a security mechanism in Agglayer that prevents compromised chains from draining funds beyond their deposits. It enforces a financial firewall between chains so that security issues on one chain cannot spread to the rest of the network. *Figure 1: Complete Pessimistic Proof generation and validation flow* ## How Pessimistic Proofs Work ### Step 0: Local Chain Preparation The local chain prepares data and sends it to Agglayer: * **Initial Network State**: The complete state of the local chain before any state transition occurs, including the Local Exit Tree (recording outbound transactions), Local Balance Tree (tracking token balances), and Nullifier Tree (tracking claimed inbound transactions). This represents the baseline state that will be modified. * **Bridge Exits**: Assets and messages being sent to other chains from the local chain, represented as a vector of BridgeExit structures containing destination information, token details, amounts, and metadata. These represent the outbound state changes that will update the Local Exit Tree and decrease Local Balance Tree balances. * **Imported Bridge Exits**: Assets and messages being claimed to the local chain from other chains, represented as ImportedBridgeExit structures with cryptographic proofs demonstrating their validity. These represent inbound state changes that will update the Nullifier Tree and increase Local Balance Tree balances. ### Step 1: Agglayer Client Data Population Agglayer Client populates the `MultiBatchHeader` using the `Certificate` data: * **Target**: Expected transitioned local chain state (`StateCommitment`) that represents what the new state should look like after applying all the bridge exits and imported bridge exits. This serves as the verification target that the computed state must match. * **Batch Header**: Packaged data with authentication information including previous state roots, all state transition data, balance proofs for affected tokens, cryptographic signatures, and the target state commitment. This comprehensive package contains everything needed for proof generation. ### Step 2: Native Rust Execution Before running expensive zkVM computation, Agglayer runs the Pessimistic Proof Program in native Rust: ```rust theme={null} // Compute new transitioned state let new_state = compute_state_transition(initial_network_state, batch_header); // Compare with expected state if new_state == batch_header.target { return Ok(PessimisticProofOutput); } else { return Err(InvalidStateTransition); } ``` **Process:** 1. Compute new transitioned state using initial state and batch header by applying all bridge exits (reducing balances, updating exit tree) and imported bridge exits (increasing balances, updating nullifier tree) to generate the new Local Balance Tree, Nullifier Tree, and Local Exit Tree roots. 2. Compare computed state with expected state in `batch_header.target` to ensure that the chain's proposed state transition matches the mathematically computed result, validating that the chain is not attempting invalid operations like spending more than available balances. 3. If equal, data is valid and state transition is correct, meaning the chain has provided legitimate state transition data that respects balance constraints and doesn't attempt double-spending or other invalid operations. 4. Return `PessimisticProofOutput` containing the verified state transition data, or error code if validation fails, ensuring that only mathematically valid state transitions can proceed to zkVM proof generation. ### Step 3: zkVM Proof Generation If native execution passes, run the same program in zkVM: * **SP1 Prover Network**: Agglayer uses Succinct's SP1 Prover Network for faster, distributed proof generation. GPU acceleration and optimized precompiles handle the Keccak-heavy computation profile of Pessimistic Proof programs efficiently. * **Same Inputs**: Identical program and inputs as native execution to ensure that the zkVM proof verifies exactly the same computation that was validated in native Rust, maintaining consistency between validation and proof generation phases. * **Proof Generation**: Creates cryptographic proof of correct execution that can be verified by anyone without re-executing the program, providing mathematical certainty that the state transition was computed correctly according to the Pessimistic Proof rules. ### Step 4: Proof Validation Agglayer validates the zk proof returned from the Prover Network: * **Proof Verification**: Verify the cryptographic proof locally using the SP1 verifier to ensure that the proof is mathematically valid and that it corresponds to the expected program execution with the correct inputs and outputs. * **Result Acceptance**: Accept pessimistic proof result if verification passes, confirming that the chain's proposed state transition is mathematically valid and respects all balance and security constraints enforced by the Pessimistic Proof program. * **State Commitment**: Update network state based on verified proof by accepting the new state roots and allowing the chain to proceed with its state transition, enabling subsequent bridge operations to build on the verified state. ## Security Guarantees ### Financial Isolation Each chain effectively has a financial "blast radius" limited to its own deposits: * **Deposit Limit**: Compromised chains cannot drain more than their current deposits because the Pessimistic Proof program mathematically enforces that outbound bridge exits cannot exceed the available token balances in the Local Balance Tree, creating a hard mathematical constraint on fund drainage. * **Containment**: Security issues cannot spread to other chains because each chain's state is validated independently through its own Pessimistic Proof, and the proof verification ensures that compromised chains cannot affect the balance trees or state transitions of other chains in the network. * **Risk Isolation**: Each chain's risk is isolated from the broader ecosystem through the financial "blast radius" concept, where the maximum possible loss from any single chain compromise is limited to the assets currently deposited on that specific chain, protecting the overall network. ### State Transition Verification * **Mathematical Verification**: All state transitions are cryptographically verified through zkVM proof generation that creates mathematical certainty about the correctness of balance updates, nullifier tree modifications, and exit tree changes, preventing any invalid state modifications. * **Proof Requirements**: State changes require valid pessimistic proofs generated through the complete validation pipeline (native execution + zkVM proof generation + verification), ensuring that only mathematically sound state transitions are accepted by the network. * **Consensus Protection**: Invalid proofs are rejected at multiple stages (native execution failure, zkVM proof generation failure, or proof verification failure), maintaining system integrity by preventing any invalid state transitions from being accepted into the network state. ### Network Protection * **Ecosystem Safety**: Broader network remains secure even with individual chain compromises because the Pessimistic Proof system isolates each chain's financial impact and prevents compromised chains from affecting the balance trees, state transitions, or security of other chains in the network. * **Continued Operation**: Other chains continue operating normally during individual chain compromises because each chain's Pessimistic Proof validation is independent, and the failure or compromise of one chain doesn't block or affect the proof generation and validation processes of other chains. * **Trust Boundaries**: Clear trust boundaries between different chains are established through separate Local Balance Trees, independent proof generation, and isolated state validation, ensuring that trust assumptions about one chain don't extend to or affect other chains in the network. ## Performance Characteristics Pessimistic Proof computation is primarily focused on state transition verification: * **75%+ Keccak Operations**: Most computation involves Keccak hash functions used for Merkle tree operations, making Keccak optimization through precompiles and hardware acceleration critical for overall performance of the Pessimistic Proof generation process. * **Merkle Tree Updates**: Efficient updates to Local Balance and Nullifier trees using Sparse Merkle Tree algorithms that only modify affected branches, significantly reducing computation compared to full tree reconstruction while maintaining cryptographic integrity. * **zkVM Optimization**: Performance varies significantly across different zkVM implementations based on their Keccak precompile efficiency, GPU acceleration support, and CPU vectorization capabilities, with SP1 chosen for production due to optimal GPU performance and prover network infrastructure. Execution Graph *Figure 2: Execution profile showing Keccak hash dominance in computation* # Benchmarks Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/pessimistic-proof/benchmarks Experimental performance analysis and benchmarks across different zkVM implementations ## Overview For experimental research and performance analysis of Pessimistic Proof across different zkVM implementations, comprehensive benchmarks have been conducted comparing SP1, RiscZero, Pico, OpenVM, and other zkVMs. **Note**: These benchmarks are for research purposes only. **Production Agglayer uses SP1 and Succinct's Prover Network exclusively.** ## Benchmark Repository For detailed performance analysis, benchmark results, and implementation comparisons across different zkVMs, visit the dedicated benchmark repository: **[Agglayer Pessimistic Proof Benchmarks](https://github.com/BrianSeong99/Agglayer_PessimisticProof_Benchmark/)** ## Repository Contents The benchmark repository includes: * **Performance comparisons**: Cycle counts and execution times across zkVMs * **Implementation details**: How Pessimistic Proof runs on different zkVMs * **Benchmark results**: Data tables and performance graphs * **Setup instructions**: How to run benchmarks locally * **Technical analysis**: Detailed breakdown of computation profiles ## Key Insights Based on the benchmark research: * **Keccak Dominance**: 75%+ of computation involves Keccak hash functions * **Performance Variation**: Significant differences between zkVM implementations * **Hardware Impact**: GPU acceleration and CPU optimizations affect performance * **Production Choice**: SP1 chosen for optimal GPU performance and reliability # Data Structures Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/pessimistic-proof/data-structures Reference for the Sparse Merkle Trees and data structures used by Pessimistic Proof: Local Balance Tree, Nullifier Tree, Bridge Exits, MultiBatchHeader, and Certificate ## Overview Pessimistic Proof computes state transitions between bridging events using three main Sparse Merkle Trees and several supporting data structures. **Core components:** * **Sparse Merkle Trees**: Local Exit Tree, Nullifier Tree, Local Balance Tree * **State transitions**: Bridge Exits, Imported Bridge Exits * **State representations**: Local State, Multi Batch Header, Proof Output ## Unified Bridge Foundation Pessimistic Proof builds on top of the Unified Bridge data structure. For complete understanding, refer to the [Unified Bridge Data Structures](/interoperability/agglayer/core-concepts/unified-bridge/data-structures/). **Key Unified Bridge Components:** * **Local Exit Tree**: Records outgoing cross-chain transactions as cryptographic commitments in a 32-level Sparse Merkle Tree, with each leaf representing a hash of bridge transaction details including destination, amount, and metadata. * **Global Exit Root**: Combines all chain states for verification by computing `hash(RollupExitRoot, MainnetExitRoot)`, providing a single unified root that represents the complete state of cross-chain activities across the entire network. * **Global Index**: Unique 256-bit reference for transactions within Global Exit Root, encoding the source network type (mainnet flag), rollup identifier, and local transaction index to enable precise transaction location across the hierarchical tree structure. Unified Bridge Tree *Figure 1: Unified Bridge data structure foundation* ## Local Balance Tree & TokenInfo The Local Balance Tree tracks all token balances on a chain using a 192-bit depth Sparse Merkle Tree. ### TokenInfo Structure ```rust theme={null} pub struct TokenInfo { /// Network which the token originates from pub origin_network: NetworkId, /// The address of the token on the origin network pub origin_token_address: Address, } ``` ### Key Layout The `TokenInfo` key uses a clever bit layout for efficient storage and lookup: * **First 32 bits**: Origin network ID where the token originally exists, enabling the system to track tokens across multiple chains while maintaining their original identity and preventing confusion between tokens with the same address on different chains. * **Next 160 bits**: Token address on the origin chain (standard Ethereum address size), ensuring that each token can be uniquely identified by combining its origin network and original contract address, even when wrapped versions exist on other chains. ### Balance Updates When assets are bridged out or claimed, the token balance in the Local Balance Tree is updated accordingly through atomic operations that ensure balance conservation and prevent overdraft conditions. Outbound bridging decreases the balance while inbound claiming increases it, with all changes verified through Merkle proof validation. Local Balance Tree *Figure 2: Local Balance Tree structure showing token balance tracking* ## Nullifier Tree The Nullifier Tree prevents double-spending and ensures transaction uniqueness across the network. Each chain maintains its own 64-bit depth Sparse Merkle Tree. ### Key Structure The Nullifier Tree key is constructed using a 64-bit identifier that uniquely identifies each claimable transaction: * **First 32 bits**: Network ID of the chain where the transaction originated, enabling the system to track which source chain a claimed transaction came from and prevent confusion between transactions from different networks. * **Last 32 bits**: Index of the bridge exit within the Local Exit Tree of the source chain (also called Local Index or depositCount), providing the exact position of the transaction within the source chain's bridge transaction history. ### Double-Spending Prevention Nullifier Tree *Figure 3: Nullifier Tree structure preventing double-spending* ## Bridge Exits Bridge Exits represent outbound transactions from a chain. ### Structure ```rust theme={null} pub struct BridgeExit { /// Enum, 0 is asset, 1 is message pub leaf_type: LeafType, /// Unique ID for the token being transferred pub token_info: TokenInfo, /// Network which the token is transferred to pub dest_network: NetworkId, /// Address which will own the received token pub dest_address: Address, /// Token amount sent pub amount: U256, /// PermitData, CallData, etc. pub metadata: Vec, } ``` ### Usage All outbound transactions from a chain are represented in a `BridgeExit` vector during pessimistic proof generation. Each `BridgeExit` contains complete transaction information needed to validate that the chain has sufficient balance for the outbound transfer and to update the Local Exit Tree with the new transaction commitment. ## Imported Bridge Exits Imported Bridge Exits represent inbound transactions to a chain. ### Structure ```rust theme={null} pub struct ImportedBridgeExit { /// The bridge exit from the source network pub bridge_exit: BridgeExit, /// The claim data pub claim_data: Claim, /// The global index of the imported bridge exit pub global_index: GlobalIndex, } ``` ### Claim Data Types ```rust theme={null} pub enum Claim { Mainnet(Box), Rollup(Box), } ``` **Separation Reason**: L1 and Rollup claims require different proof paths due to their different positions in the hierarchical tree structure: * **Mainnet**: Requires direct proof from Mainnet Exit Root to L1 Info Root since L1 transactions are recorded directly in the Mainnet Exit Tree and don't need to go through the Rollup Exit Tree aggregation layer. * **Rollup**: Requires a two-step proof path from Local Exit Root → Rollup Exit Root → L1 Info Root because L2 transactions must first prove inclusion in the L2's Local Exit Tree, then prove that the L2's Local Exit Root was properly submitted to the Rollup Exit Tree on L1. ## Local State Local State represents the complete state of a local chain. ### Structure ```rust theme={null} pub struct LocalNetworkState { /// Commitment to the BridgeExit pub exit_tree: LocalExitTree, /// Commitment to the balance for each token pub balance_tree: LocalBalanceTree, /// Commitment to claimed assets on foreign networks pub nullifier_tree: NullifierTree, } ``` ### Components * **Exit Tree**: Records all outgoing bridge transactions as a 32-level Sparse Merkle Tree, storing cryptographic commitments of `bridgeAsset` and `bridgeMessage` operations that represent assets and messages being sent to other chains. * **Balance Tree**: Tracks token balances for all assets on the chain using a 192-bit depth Sparse Merkle Tree, with TokenInfo keys enabling precise tracking of token origins and current balances for every asset type on the chain. * **Nullifier Tree**: Prevents double-spending of claimed assets by maintaining a 64-bit depth Sparse Merkle Tree that marks imported bridge exits as claimed, ensuring that each cross-chain transaction can only be processed once on the destination chain. ## Multi Batch Header The comprehensive state transition record for pessimistic proof generation. ### Structure ```rust theme={null} pub struct MultiBatchHeader { /// Network that emitted this MultiBatchHeader pub origin_network: NetworkId, /// Previous local exit root pub prev_local_exit_root: H::Digest, /// Previous local balance root pub prev_balance_root: H::Digest, /// Previous nullifier tree root pub prev_nullifier_root: H::Digest, /// List of bridge exits created in this batch pub bridge_exits: Vec, /// List of imported bridge exits claimed in this batch pub imported_bridge_exits: Vec<(ImportedBridgeExit, NullifierPath)>, /// Commitment to the imported bridge exits pub imported_exits_root: Option, /// L1 info root used to import bridge exits pub l1_info_root: H::Digest, /// Token balances with Merkle proofs pub balances_proofs: BTreeMap)>, /// Signer committing to the state transition pub signer: Address, /// Signature committing to the state transition pub signature: Signature, /// State commitment target hashes pub target: StateCommitment, } ``` ### Purpose Serves as the master input capturing the complete set of changes between old and new local states, containing all data required for pessimistic proof generation. This structure packages together the previous state roots, all state transition data (bridge exits and imported bridge exits), balance proofs, and target state commitments needed to mathematically verify that the proposed state transition is valid and secure. ## Pessimistic Proof Output The final result of Pessimistic Proof computation. ### Structure ```rust theme={null} pub struct PessimisticProofOutput { /// The previous local exit root pub prev_local_exit_root: Digest, /// The previous pessimistic root pub prev_pessimistic_root: Digest, /// The l1 info root for proving imported bridge exits pub l1_info_root: Digest, /// The origin network of the pessimistic proof pub origin_network: NetworkId, /// The consensus hash pub consensus_hash: Digest, /// The new local exit root pub new_local_exit_root: Digest, /// The new pessimistic root (balance + nullifier tree) pub new_pessimistic_root: Digest, } ``` ### Pessimistic Root Formula ``` prev_pessimistic_root = hash(prev_local_balance_root, prev_nullifier_root) new_pessimistic_root = hash(new_local_balance_root, new_nullifier_root) ``` ## Certificate A Certificate represents a state transition of a chain that gets submitted to Agglayer. ### Structure ```rust theme={null} pub struct Certificate { /// NetworkID of the origin network pub network_id: NetworkId, /// Simple increment to count the Certificate per network pub height: Height, /// Previous local exit root pub prev_local_exit_root: Digest, /// New local exit root pub new_local_exit_root: Digest, /// List of bridge exits included in this state transition pub bridge_exits: Vec, /// List of imported bridge exits included in this state transition pub imported_bridge_exits: Vec, /// Signature committed to the bridge exits and imported bridge exits pub signature: Signature, /// Fixed size field of arbitrary data for the chain needs pub metadata: Metadata, } ``` ### Validation If a certificate is invalid, any state transitions in the current epoch will be reverted, protecting the network from invalid state changes. The validation process ensures that all bridge exits have sufficient balances, all imported bridge exits have valid proofs and haven't been double-claimed, and that the cryptographic signature properly commits to all the state transition data. This atomic validation prevents partial state updates that could compromise network security. # Proof Generation Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/pessimistic-proof/proof-generation The Pessimistic Proof generation pipeline: certificate submission, native Rust validation, zkVM proof generation, and proof verification ## Overview Pessimistic Proof generation is a multi-step process that verifies state transitions mathematically before Agglayer accepts them. The process runs the proof program first in native Rust for fast validation, then in a zkVM to produce a cryptographic proof. ## Proof Generation Flow ### Complete Flow ### Step 1: Certificate Submission Local chains submit a **`Certificate`** containing comprehensive state transition data: * **Previous and new local exit roots** that define the starting and ending states of the chain's **Local Exit Tree**, providing the boundary conditions for validating the proposed state transition. * **Bridge exits** (outbound transactions) representing all assets and messages being sent from this chain to other chains, with complete destination information, token details, and amounts that will reduce the chain's **Local Balance Tree**. * **Imported bridge exits** (inbound transactions) representing all assets and messages being claimed on this chain from other chains, with **cryptographic proofs** demonstrating their validity and **Global Index** information for verification. * **Cryptographic signature** from the chain's authorized signer that commits to all the state transition data, ensuring that the certificate represents an authorized state transition approved by the chain's governance or operator. ### Step 2: Data Population Agglayer Client creates a **`MultiBatchHeader`** from the Certificate: * Adds **previous state roots** (balance, nullifier, exit) from the initial network state to establish the baseline from which state transitions will be computed and validated. * Includes **balance proofs** for affected tokens by providing **Merkle proofs** demonstrating the current balances in the **Local Balance Tree** for all tokens involved in bridge exits, ensuring that sufficient funds exist for outbound transfers. * Sets **target state commitment** representing the expected final state after applying all bridge exits and imported bridge exits, serving as the verification target that the computed state transition must match. * Adds **authentication data** including **cryptographic signatures**, signer addresses, and **L1 Info Root** references needed to validate the legitimacy and timing of the state transition request. ### Step 3: Native Execution Before expensive zkVM computation, Agglayer runs the Pessimistic Proof program in native Rust to validate the state transition: **The Process:** 1. **Apply State Transitions**: The program takes the **initial network state** and applies all the changes specified in the **batch header**. For **bridge exits**, it reduces token balances and adds new leaves to the **Local Exit Tree**. For **imported bridge exits**, it checks **nullifier uniqueness**, increases token balances, and marks transactions as claimed in the **Nullifier Tree**. 2. **Compute New State**: After applying all transitions, the program computes the new roots for all three **Merkle trees** (**Exit Tree**, **Balance Tree**, **Nullifier Tree**) and creates a new **state commitment** that represents the final state after all changes. 3. **Validate Against Target**: The computed new state is compared against the **target state** provided in the certificate. If they match, it proves that the chain's proposed state transition is **mathematically correct** and respects all balance and security constraints. 4. **Generate Output**: If validation succeeds, the program generates a **`PessimisticProofOutput`** containing both the previous and new state roots, which serves as the verified result of the state transition computation. ```rust theme={null} pub fn generate_pessimistic_proof( initial_state: &LocalNetworkState, batch_header: &MultiBatchHeader, ) -> Result { // Compute new state based on transitions let new_state = apply_state_transitions(initial_state, batch_header)?; // Verify against expected target if new_state.commitment() != batch_header.target { return Err(ProofError::InvalidStateTransition); } // Return proof output Ok(PessimisticProofOutput { prev_local_exit_root: batch_header.prev_local_exit_root, prev_pessimistic_root: compute_pessimistic_root( batch_header.prev_balance_root, batch_header.prev_nullifier_root ), new_local_exit_root: new_state.exit_tree.root(), new_pessimistic_root: compute_pessimistic_root( new_state.balance_tree.root(), new_state.nullifier_tree.root() ), // ... other fields }) } ``` ### Step 4: zkVM Execution If native execution succeeds, run the identical program in zkVM: * **Same Program**: Exact same proof generation function executed in the zkVM environment to ensure that the cryptographic proof verifies the identical computation that was validated in native Rust, maintaining consistency between validation and proof phases. * **Same Inputs**: Identical initial state and batch header data fed to the zkVM to guarantee that the proof generation uses exactly the same parameters that were validated in native execution, preventing any discrepancies between validation and proving. * **Cryptographic Proof**: Generates verifiable proof of correct execution that can be validated by anyone without re-executing the program, providing mathematical certainty that the state transition computation was performed correctly according to Pessimistic Proof rules. ### Step 5: Proof Validation Agglayer validates the returned zk proof: * **Proof Verification**: Cryptographic verification of the proof using the SP1 verifier to ensure mathematical validity and confirm that the proof corresponds to the expected program execution with correct inputs and outputs. * **Output Validation**: Ensure proof output matches expected results by comparing the PessimisticProofOutput from the zkVM execution with the results from native execution, validating consistency between both execution environments. * **State Acceptance**: Update network state if proof is valid by accepting the new state roots and allowing the chain to proceed with its state transition, enabling subsequent operations to build on the verified state. ## State Transition Logic The state transition mechanism validates and applies changes from the old local state to the new local state using bridge exits and imported bridge exits, then compares the computed results with the expected certificate data. ## Proof Verification ### Mathematical Constraints The proof generation enforces several mathematical constraints: 1. **Balance Conservation**: Total outbound amounts ≤ available balances, mathematically enforced by verifying that the sum of all bridge exit amounts for each token does not exceed the current balance stored in the Local Balance Tree, preventing overdraft conditions. 2. **Nullifier Uniqueness**: No double-claiming of imported exits, enforced by checking that each imported bridge exit's nullifier key is not already marked as claimed in the Nullifier Tree, preventing replay attacks and duplicate processing. 3. **Root Consistency**: All tree roots must be computed correctly using the standard Keccak256 hashing algorithm and Sparse Merkle Tree algorithms, ensuring that state transitions produce mathematically valid tree structures. 4. **Signature Validity**: State transitions must be properly signed by the chain's authorized signer using valid cryptographic signatures that commit to all the state transition data, ensuring that only authorized parties can propose state changes. ### Security Guarantees * **No Overdraft**: Chains cannot spend more than they have because the Pessimistic Proof program mathematically enforces balance conservation by verifying that outbound bridge exit amounts do not exceed available token balances in the Local Balance Tree, creating a hard constraint against fund drainage. * **No Double-Spending**: Each transaction can only be claimed once due to the Nullifier Tree mechanism that marks imported bridge exits as claimed and prevents re-processing, ensuring that the same cross-chain transaction cannot be used multiple times to inflate balances. * **Cryptographic Integrity**: All state changes are cryptographically verified through Merkle tree operations using Keccak256 hashing, zkVM proof generation, and signature validation, ensuring that no invalid state modifications can be accepted by the system. * **Atomic Updates**: All changes succeed or fail together through the comprehensive validation process where any failure in balance verification, nullifier checking, or signature validation causes the entire state transition to be rejected, preventing partial updates that could compromise system integrity. # Aggchain Proof Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/state-transition-proof/aggchain-proof Reference for Aggchain Proof: ECDSA and generic validity proof consensus types, data structures, verification steps, and bridge constraint details ## Overview Aggchain Proof is the verification layer in Agglayer that handles different consensus mechanisms for proving chain state transitions. It combines consensus verification with bridge constraint verification, producing a single proof that covers both a chain's internal operations and its cross-chain transfers. ## Supported Consensus Types ### ECDSA Signature (CONSENSUS\_TYPE = 0) The original consensus mechanism used in Agglayer where a **trusted sequencer** acts as a security authority, signing off on state changes to ensure they are valid and authorized. **Characteristics:** * **Trusted Sequencer Model**: A designated address signs off on state changes, acting as the primary security authority for the chain * **Simple Verification**: Like having a security guard verify and approve changes - fast and straightforward * **Fast Processing**: Minimal computational overhead with efficient signature verification * **Trust Assumption**: Relies on sequencer integrity and key security **How ECDSA Verification Works:** 1. **Message Construction**: Create standardized message combining **SHA256** of public values, **new local exit root**, and **commitment to imported bridge exits** 2. **Signature Recovery**: Use elliptic curve cryptography to recover the signer's address from the signature 3. **Authority Validation**: Compare recovered address with configured **trusted sequencer** address ```rust theme={null} // ECDSA Verification Implementation pub fn verify(&self) -> Result<(), ProofError> { let signature_commitment = keccak256_combine([ self.sha256_public_values(), new_local_exit_root.0, commit_imported_bridge_exits.0, ]); let recovered_signer = signature .recover_address_from_prehash(&B256::new(signature_commitment.0))?; if recovered_signer != self.trusted_sequencer { return Err(ProofError::InvalidSigner); } Ok(()) } ``` ### Generic Proof / Validity Proof (CONSENSUS\_TYPE = 1) Advanced consensus mechanism providing comprehensive verification of chain operations through mathematical proofs rather than trusted parties. **Characteristics:** * **Flexible Proof System**: Can work with any type of chain-specific proof system * **Mathematical Verification**: Provides comprehensive state transition validation with cryptographic certainty * **Universal Compatibility**: Like having a universal translator for different security protocols * **Enhanced Security**: Mathematical certainty about state correctness without trust assumptions **How Validity Proof Works:** 1. **State Transition Verification**: Mathematically verify every operation within the chain, including transaction processing, smart contract execution, and state machine transitions 2. **Bridge Constraint Verification**: Verify that cross-chain transfers are valid and properly integrated with internal state changes 3. **SP1 zkVM Verification**: Use **SP1 zkVM** to cryptographically verify the validity proof with mathematical certainty ```rust theme={null} // Validity Proof Verification Implementation pub fn verify(&self) -> Result<(), ProofError> { // Verify L1 head for synchronization self.verify_l1_head(l1_info_root)?; // Verify the validity proof using SP1 zkVM sp1_zkvm::lib::verify::verify_sp1_proof( &self.aggregation_vkey_hash.to_hash_u32(), &self.sha256_public_values().into(), ); Ok(()) } ``` **Use Cases:** * **Zero-Knowledge Rollups**: Chains generating **zk-SNARKs** or **zk-STARKs** for state transitions * **Custom Consensus**: Chains with unique consensus algorithms and specialized verification requirements ## Aggchain Proof Data Structure ### Witness Structure ```rust theme={null} pub struct AggchainProofWitness { /// Previous local exit root pub prev_local_exit_root: Digest, /// New local exit root pub new_local_exit_root: Digest, /// L1 info root used to import bridge exits pub l1_info_root: Digest, /// Origin network for which the proof was generated pub origin_network: u32, /// Full execution proof with its metadata pub fep: FepInputs, /// Commitment on the imported bridge exits minus the unset ones pub commit_imported_bridge_exits: Digest, /// Bridge witness related data pub bridge_witness: BridgeWitness, } ``` ### Bridge Witness ```rust theme={null} pub struct BridgeWitness { /// List of inserted GER minus the removed ones pub inserted_gers: Vec, /// Raw list of inserted GERs which includes also the ones which get removed pub raw_inserted_gers: Vec, /// List of removed GER pub removed_gers: Vec, /// List of each imported bridge exit containing global index and leaf hash pub bridge_exits_claimed: Vec, /// List of global index of each unset bridge exit pub global_indices_unset: Vec, /// State sketch for the prev L2 block pub prev_l2_block_sketch: EvmSketchInput, /// State sketch for the new L2 block pub new_l2_block_sketch: EvmSketchInput, } ``` ### Public Values Output ```rust theme={null} pub struct AggchainProofPublicValues { /// Previous local exit root pub prev_local_exit_root: Digest, /// New local exit root pub new_local_exit_root: Digest, /// L1 info root used to import bridge exits pub l1_info_root: Digest, /// Origin network for which the proof was generated pub origin_network: NetworkId, /// Commitment to the imported bridge exits indexes pub commit_imported_bridge_exits: Digest, /// Chain-specific commitment forwarded by the PP pub aggchain_params: Digest, } ``` ## Verification Process ### Step 1: Consensus Verification The system first verifies the chain's consensus proof: ```rust theme={null} // Verify the FEP proof or ECDSA signature self.fep.verify( self.l1_info_root, self.new_local_exit_root, self.commit_imported_bridge_exits, )?; ``` **For ECDSA**: Verifies signature from trusted sequencer **For Generic**: Verifies validity proof using chain-specific verification logic ### Step 2: Bridge Constraints Verification Then verifies bridge-related constraints: ```rust theme={null} // Verify the bridge constraints self.bridge_constraints_input().verify()?; ``` **Bridge Verification Components:** 1. **GER Hash Chains**: Verifies **Global Exit Root** insert/remove sequences recorded in hash chains that act as a stack using LIFO rules 2. **Claims Hash Chains**: Verifies claimed and unset claims hash chains where valid claims are added to claimed chain and invalid ones to unset chain 3. **Local Exit Root**: Verifies the **Local Exit Root** is computed correctly 4. **Imported Bridge Exits**: Verifies **`commit_imported_bridge_exits`** is constructed correctly from claimed and unset bridge events 5. **GER Inclusion**: Verifies each inserted **Global Exit Root** has valid **Merkle proof** inclusion in the **L1 Info Root** ## Execution Flow ### Complete Aggchain Proof Process ## Bridge Constraint Details ### GER Stack Management **Global Exit Root** updates are managed as a sophisticated stack structure that ensures proper sequencing and validation: * **Insertion Process**: New **Global Exit Roots** are added when bridge operations occur on any connected chain. Each insertion represents a state change in the **Unified Bridge** system and must be properly validated and sequenced to maintain network consistency. * **Removal Mechanism**: Faulty **Global Exit Roots** can be removed from the stack in rare cases where invalid state updates are detected. This removal process ensures that incorrect state updates don't propagate through the network and compromise security. * **LIFO Order**: Last-in-first-out ordering ensures proper sequence validation where the most recent **GER** updates are processed first. This ordering is critical for maintaining temporal consistency and ensuring that state updates are applied in the correct chronological order. * **Hash Chain Tracking**: All **GER** operations (both insertions and removals) are recorded in cryptographically linked hash chains that provide an immutable audit trail. These hash chains enable verification that the **GER** stack operations were performed correctly and in the proper sequence. ### Claims Processing **Bridge Exit Claims** are processed with comprehensive dual tracking that ensures security and prevents double-spending: * **Claimed Hash Chain**: Valid claims that successfully increase balances on destination chains are recorded in a cryptographically linked hash chain. Each entry in this chain represents a legitimate cross-chain transfer that has been properly verified and processed, creating an immutable record of successful bridge operations. * **Unset Hash Chain**: Invalid claims that are rejected due to insufficient proofs, double-spending attempts, or other validation failures are recorded in a separate hash chain. This tracking ensures that invalid operations are properly documented and cannot be reprocessed, maintaining system integrity. * **Atomic Processing**: All claims in a batch succeed or fail together through comprehensive validation where any single claim failure causes the entire batch to be rejected. This atomic processing prevents partial state updates that could compromise balance consistency across chains. * **Double-Spend Prevention**: Ensures each bridge exit can only be claimed once by tracking all processed claims in the **Nullifier Tree** and validating that imported bridge exits haven't been previously processed. This mechanism prevents replay attacks and maintains economic security. ### Inclusion Proof Verification **L1 Info Root Inclusion** provides cryptographic proof that **Global Exit Root** updates are legitimate: * **Merkle Proof Validation**: Each inserted **Global Exit Root** must have a valid **Merkle proof** demonstrating inclusion in the **L1 Info Tree**. These proofs use **Keccak256** hashing to create cryptographic certainty that the **GER** was properly recorded on L1. * **L1 Settlement Verification**: Proofs demonstrate that **Global Exit Root** updates were properly recorded on L1 and achieved finality, ensuring that cross-chain operations are backed by Ethereum's security guarantees and cannot be reverted. * **Leaf Index Validation**: Ensures correct positioning in the **L1 Info Tree** by validating that the **Merkle proof** corresponds to the expected leaf index. This prevents manipulation of proof paths and ensures that proofs reference the correct historical state. * **Root Verification**: Confirms that **Merkle proofs** lead to the correct **L1 Info Root** by reconstructing the proof path and validating that it produces the expected root hash. This verification ensures that proofs are based on legitimate L1 state and haven't been tampered with. # Architecture Overview Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/state-transition-proof/architecture The State Transition Proof dual proof system: Local Chain, AggProver, and Agglayer roles, and how ECDSA and generic consensus types are handled ## Overview State Transition Proof implements a dual proof system that verifies both internal chain operations and cross-chain transfers. It supports chains with different consensus mechanisms while maintaining consistent security guarantees at the Agglayer level. ## Dual Proof Architecture ### Complete Verification Flow ## System Components ### Local Chain **Purpose**: Chains connected to Agglayer (Katana, X Layer, and others) that generate state transition proofs. **Key Responsibilities:** * **State Transition Confirmation**: Validates that internal state changes are mathematically correct and follow the chain's consensus rules * **Proof Generation**: Creates either **Validity Proofs** (comprehensive state verification) or **ECDSA Signatures** (trusted sequencer authorization) depending on the chain's security model * **Certificate Submission**: Packages state transition data into certificates for Agglayer verification ### AggProver **Purpose**: Critical component that generates cryptographic proofs for state transitions and bridge operations. **Key Responsibilities:** * **Consensus Verification**: Validates either **Validity Proofs** or **ECDSA Signatures** from local chains to ensure state transitions are authorized and mathematically correct * **Bridge Constraint Validation**: Verifies **Global Exit Root** sequences, claimed/unset bridge events, **Local Exit Root** correctness, and **L1 Info Root** inclusion proofs * **Aggchain Proof Generation**: Creates comprehensive proofs that combine consensus verification with bridge validation, ensuring both internal and cross-chain operations are secure ## Verification Systems ### Internal Chain Validation **State Transition Verification:** * Validates that each chain's internal state transitions are mathematically correct through comprehensive verification of all operations within the chain * Ensures all operations follow proper execution rules and that new states are properly derived from previous states through valid state transition logic * Provides the foundation for secure cross-chain operations by ensuring individual chains are operating correctly before allowing bridge operations **Consensus Mechanisms:** * **Validity Proof**: Comprehensive verification of every operation in the chain using mathematical proofs, providing cryptographic certainty about state correctness without requiring trusted parties * **ECDSA Signature**: Trusted sequencer authorization where designated addresses validate and sign off on state changes, providing fast verification with trusted party assumptions ### Cross-Chain Validation **Aggchain Proof:** * Combines consensus verification with bridge constraint validation to ensure both internal operations and cross-chain transfers are secure and mathematically correct * Supports flexible consensus mechanisms while maintaining strict bridge security requirements, enabling different chain types to participate safely * Acts as the bridge between internal chain validation and cross-chain operation validation **Pessimistic Proof:** * Validates cross-chain asset transfers and balance conservation by ensuring chains cannot drain more funds than currently deposited, creating financial isolation between chains * Prevents compromised chains from affecting other chains in the network through mathematical constraints on fund movement * Ensures atomic cross-chain operations where all components succeed or fail together ## Consensus Flexibility ### ECDSA Consensus (CONSENSUS\_TYPE = 0) **Characteristics:** * **Trusted Sequencer**: Designated address signs state transitions * **Simple Verification**: Signature validation using elliptic curve cryptography * **Fast Processing**: Minimal computational overhead * **Trust Model**: Relies on sequencer integrity ### Generic Consensus (CONSENSUS\_TYPE = 1) **Characteristics:** * **Validity Proofs**: Comprehensive mathematical verification of state transitions * **Flexible Integration**: Supports various proof systems and zkVMs * **Enhanced Security**: Mathematical certainty about state correctness * **Modular Design**: Can integrate with different chain architectures ## Security Guarantees ### Comprehensive Validation * **Internal Security**: Every chain's internal operations are verified through either **Validity Proofs** or **ECDSA signatures** before any cross-chain operations are allowed, ensuring that only properly functioning chains can participate in bridge operations * **Cross-Chain Security**: Bridge operations are validated through multiple proof systems (**Aggchain Proof** + **Pessimistic Proof**) that ensure mathematical correctness and balance conservation across all connected chains * **Mathematical Certainty**: All verifications use cryptographic proofs and mathematical constraints to provide certainty about operation validity, eliminating reliance on trust assumptions where possible ### Isolation and Containment * **Chain Independence**: Issues in one chain cannot affect others due to isolated verification processes where each chain's state transitions are validated independently through separate proof generation and verification * **Atomic Operations**: All operations succeed or fail completely through the comprehensive validation pipeline where any failure in consensus verification, bridge constraints, or pessimistic proof validation causes the entire state transition to be rejected * **Proof Requirements**: Invalid operations cannot proceed without proper verification because the system requires valid cryptographic proofs at multiple stages (consensus + bridge + balance conservation) before accepting any state changes # Architecture Overview Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/unified-bridge/architecture The Unified Bridge architecture: onchain contracts, off-chain services, data flow, and security properties ## Overview The Unified Bridge combines onchain smart contracts, off-chain services, and cryptographic verification to enable cross-chain communication across Agglayer-connected chains. Unified Bridge Architecture *Figure 1: Complete Unified Bridge architecture showing all components and interactions* ## System Architecture ## Data Flow Architecture ### Cross-Chain Transaction Flow ## Component Interactions ### Smart Contract Layer **L1 Contracts (Ethereum):** * **RollupManager**: Coordinates L2 state submissions and manages the Rollup Exit Tree. When L2s submit their Local Exit Roots, this contract updates the aggregated rollup state and triggers Global Exit Root updates. * **GlobalExitRoot**: Maintains the unified Global Exit Root by combining Rollup Exit Root and Mainnet Exit Root. Also manages the L1 Info Tree that stores historical Global Exit Roots for L2 synchronization. * **Bridge**: Handles L1 ↔ L2 transactions and maintains the Mainnet Exit Tree. Processes asset and message bridging from L1 to connected L2s, and validates claims from L2s to L1. **L2 Contracts (Connected Chains):** * **Bridge**: Handles all cross-chain transactions for the L2, including bridging to other L2s and L1. Maintains the chain's Local Exit Tree and processes both outbound bridging and inbound claims. * **GlobalExitRootL2**: Syncs with L1 Global Exit Root updates to enable claim verification. Fetches the latest Global Exit Root from L1 to validate cross-chain transaction proofs. ### Service Layer **Bridge Services:** * **Chain Indexer**: Monitors blockchain events in real-time, parsing and organizing bridge transaction data. Each connected chain has its own indexer instance that processes `BridgeEvent` and `ClaimEvent` logs. * **Transaction API**: Provides real-time bridge transaction status and details for user interfaces. Returns transaction status, token information, source/destination chains, and deposit counts needed for proof generation. * **Proof API**: Generates Merkle proofs required for claiming bridged assets and messages. Creates `smtProofLocalExitRoot` and `smtProofRollupExitRoot` along with other verification data needed for claims. ### Security Architecture ## Bridge Operation Types ### Asset Bridging Architecture **Source Chain Process:** 1. Lock/burn tokens based on token type 2. Record transaction in Local Exit Tree 3. Emit bridge event for indexing **Destination Chain Process:** 1. Verify Merkle proofs against Global Exit Root 2. Transfer/mint tokens based on token type 3. Mark transaction as claimed ### Message Bridging Architecture **Source Chain Process:** 1. Package message data and ETH value 2. Record message in Local Exit Tree 3. Emit bridge event for indexing **Destination Chain Process:** 1. Verify Merkle proofs against Global Exit Root 2. Execute message on target contract 3. Handle ETH/WETH value transfer ## State Synchronization ### Global Exit Root Updates ### Merkle Tree Hierarchy The architecture maintains a sophisticated hierarchical Merkle tree structure that enables secure cross-chain verification: * **Local Exit Trees**: Each connected chain maintains its own 32-level Sparse Merkle Tree that records all outgoing bridge transactions. Every time a user initiates a `bridgeAsset` or `bridgeMessage` call, a new leaf is added to this tree and the root is updated. * **Rollup Exit Tree**: L1's RollupManager maintains a Sparse Merkle Tree where each leaf represents a Local Exit Root from a connected L2. When L2s submit their updated Local Exit Roots to L1, this tree is updated, creating a unified view of all L2 bridge activities. * **Mainnet Exit Tree**: L1 maintains its own Local Exit Tree (called Mainnet Exit Tree) that records all bridge transactions originating from L1 to connected L2s. This operates similarly to L2 Local Exit Trees but specifically for L1 activities. * **Global Exit Root**: A single root hash computed as `hash(RollupExitRoot, MainnetExitRoot)` that represents the complete state of all cross-chain activities across the entire network. This root is updated whenever either the Rollup Exit Root or Mainnet Exit Root changes. * **L1 Info Tree**: A historical ledger that stores every Global Exit Root update as leaves in a 32-level Sparse Merkle Tree. This enables L2s to sync with specific historical states and provides the foundation for Merkle proof verification during claims. ## Scalability Design ### Horizontal Scaling * **Multiple L2s**: The architecture supports unlimited connected chains without performance degradation. Each new L2 simply adds another leaf to the Rollup Exit Tree, and the system scales linearly with the number of connected chains. * **Parallel Processing**: Connected chains operate independently and can process bridge transactions simultaneously. There's no coordination required between chains for individual transactions, enabling true parallel execution across the network. * **Load Distribution**: Proof generation and transaction indexing are distributed across multiple service instances. Each chain can have its own indexer, and proof generation can be handled by distributed prover networks. ### Vertical Scaling * **Batch Submissions**: L2s have flexibility in how frequently they submit their Local Exit Roots to L1. They can submit immediately for each transaction or batch multiple transactions together before submitting, optimizing for gas costs and throughput. * **Efficient Proofs**: Merkle proof generation is optimized using Sparse Merkle Trees that only store non-zero values, significantly reducing storage and computation requirements. Proofs are generated on-demand and cached for frequently accessed transactions. * **State Compression**: The hierarchical tree structure provides natural compression where multiple L2 states are represented by a single Rollup Exit Root, and the entire network state is compressed into a single Global Exit Root. ## Security Properties ### Trust Model * **L1 Security**: The system inherits Ethereum's security guarantees because all cross-chain transactions must be settled and finalized on Ethereum before they can be claimed on destination chains. This means the security of cross-chain operations is backed by Ethereum's consensus mechanism and economic security. * **Cryptographic Proofs**: Every claim requires valid Merkle proofs that mathematically demonstrate the transaction was properly recorded and settled. The verification process uses cryptographic hash functions to ensure that no invalid or fraudulent claims can be processed. * **No Trusted Parties**: The system operates without requiring trust in any centralized entity. Users, claimers, and even the bridge operators cannot manipulate the system because all operations are governed by smart contract logic and cryptographic verification. ### Failure Isolation * **Chain Independence**: If one connected chain experiences issues or becomes compromised, it cannot affect the security or operation of other chains. Each chain's bridge transactions are isolated in separate Local Exit Trees, preventing cross-contamination of security issues. * **Atomic Operations**: Bridge transactions either succeed completely or fail completely - there are no partial states. If any part of a cross-chain transaction fails (such as insufficient balance or invalid proofs), the entire operation is reverted without affecting the system state. * **Proof Requirements**: Invalid operations cannot be executed because the system requires valid Merkle proofs for all claims. Without proper cryptographic proof that a transaction was settled on L1, no assets can be claimed or messages executed on destination chains. # Asset Bridging Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/unified-bridge/asset-bridging How asset bridging works in the Unified Bridge: token type handling, bridgeAsset/claimAsset function signatures, and Merkle proof verification ## Overview Asset bridging enables the transfer of tokens and native assets between Agglayer-connected chains. The Unified Bridge handles different token types with distinct mechanisms depending on whether the token originates from the source chain or is a foreign token. Asset Bridging Process *Figure 1: Complete asset bridging flow from L1 to L2* ## Supported Token Types The Unified Bridge handles different token types with specific mechanisms: | Token Type | Source Chain Action | Destination Chain Action | | -------------------------------------------- | --------------------------------- | ------------------------------------- | | **Native Gas Token** (ETH, Custom Gas Token) | Bridge contract holds tokens | Bridge contract transfers tokens | | **WETH** | Burn WETH tokens from user | Mint WETH tokens to user | | **Foreign ERC20** (Not native to source) | Burn ERC20 tokens from user | Mint wrapped tokens to user | | **Native ERC20** (Native to source) | Transfer ERC20 to bridge contract | Transfer from bridge contract to user | ## Bridge Asset Function The `bridgeAsset` function initiates asset transfers between chains. ### Function Signature ```solidity theme={null} function bridgeAsset( uint32 destinationNetwork, address destinationAddress, uint256 amount, address token, bool forceUpdateGlobalExitRoot, bytes calldata permitData ) external payable ``` ### Parameters * **`destinationNetwork`**: Network ID of the destination chain * **`destinationAddress`**: Address to receive assets on destination chain * **`amount`**: Amount of tokens to bridge * **`token`**: Token contract address (0x0 for native gas token) * **`forceUpdateGlobalExitRoot`**: Whether to update GER immediately * **`permitData`**: Raw permit data for ERC20 tokens (optional) ### Process Steps 1. **Validation**: Check destination network is not the source network 2. **Token Preparation**: Handle token based on type (lock, burn, or transfer) 3. **Event Emission**: Emit `BridgeEvent` with transaction details 4. **Tree Update**: Add transaction to Local Exit Tree as leaf node ### Token Preparation Logic The bridge handles different token types with specific mechanisms based on their origin and nature: > Note that in case `ETH` is the native token, WETHToken will be at `0x0` address. #### Native Gas Token (ETH, Custom Gas Token) ```solidity theme={null} // Bridge contract holds the tokens // The native gas token is already transferred via msg.value // No additional token transfer required ``` #### WETH Token ```solidity theme={null} // Burn WETH tokens from user's address IWETH(token).burnFrom(msg.sender, amount); ``` #### Foreign ERC20 Token (Not native to source chain) ```solidity theme={null} // If the token contract is not originally from the source network, // burn the ERC20 token from user's address IERC20(token).burnFrom(msg.sender, amount); ``` #### Native ERC20 Token (Native to source chain) ```solidity theme={null} // If the token contract is originally from the source network: // 1. Execute permit if provided if (permitData.length > 0) { IERC20Permit(token).permit(...); } // 2. Transfer tokens from user to bridge contract IERC20(token).transferFrom(msg.sender, address(this), amount); ``` ## Claim Asset Function The `claimAsset` function claims bridged assets on the destination chain. ### Function Signature ```solidity theme={null} function claimAsset( bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofLocalExitRoot, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofRollupExitRoot, uint256 globalIndex, bytes32 mainnetExitRoot, bytes32 rollupExitRoot, uint32 originNetwork, address originTokenAddress, uint32 destinationNetwork, address destinationAddress, uint256 amount, bytes calldata metadata ) external ``` ### Parameters * **`smtProofLocalExitRoot`**: Merkle proof for Local Exit Root * **`smtProofRollupExitRoot`**: Merkle proof for Rollup Exit Root * **`globalIndex`**: Global index identifying the transaction * **`mainnetExitRoot`**: Mainnet Exit Root at time of transaction * **`rollupExitRoot`**: Rollup Exit Root at time of transaction * **`originNetwork`**: Network ID of source chain * **`originTokenAddress`**: Token address on source chain * **`destinationNetwork`**: Network ID of destination chain * **`destinationAddress`**: Address to receive assets * **`amount`**: Amount of tokens to claim * **`metadata`**: Additional metadata (if any) ### Process Steps 1. **Validation**: Verify destination network matches current chain 2. **Proof Verification**: Verify Merkle proofs against Global Exit Root 3. **Duplicate Check**: Ensure transaction hasn't been claimed before 4. **Token Transfer**: Transfer tokens based on token type (see Token Transfer Logic below) 5. **Claim Record**: Mark transaction as claimed ### Token Transfer Logic Once the proof verification passes, the bridge claims tokens using different mechanisms based on the token type: | Token type | Action | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **ETH is gas token** | Bridge contract transfers the amount from itself to the destination address | | **WETH where ETH is not gas token** | Mint new WETH tokens to the destination address | | **Custom gas token** | Bridge contract transfers the amount from itself to the destination address | | **Native ERC20 Token** | If the token contract is originally from this destination network, transfer the ERC20 token from bridge contract to destination address | | **Foreign ERC20 Token, First time bridging** | Deploy a new ERC20 Token contract to host this new Foreign ERC20 Token, and mint the transfer amount to destination address | | **Foreign ERC20 Token, Contract exists** | Mint the transfer amount to destination address | ### Proof Verification Logic ```solidity theme={null} // Construct Global Exit Root bytes32 globalExitRoot = keccak256(abi.encodePacked(mainnetExitRoot, rollupExitRoot)); // Verify against synchronized GER require(globalExitRoot == getGlobalExitRoot(), "Invalid global exit root"); // Verify Merkle proofs based on origin if (originNetwork == 0) { // L1 to L2: Verify against mainnet exit root verifyMerkleProof(smtProofLocalExitRoot, mainnetExitRoot, globalIndex); } else { // L2 to L2: Verify against rollup exit root verifyMerkleProof(smtProofLocalExitRoot, rollupExitRoot, globalIndex); verifyMerkleProof(smtProofRollupExitRoot, rollupExitRoot, globalIndex); } ``` ## Bridging Flows ### L1 to L2 Bridging ### L2 to L1 Bridging ### L2 to L2 Bridging # Bridge Components Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/unified-bridge/bridge-components Reference for the Unified Bridge's onchain contracts, off-chain services, and APIs ## Overview The Unified Bridge consists of three main component categories: onchain smart contracts, off-chain services, and developer tools. Unified Bridge Components *Figure 1: Complete Unified Bridge architecture showing all components and their interactions* ## Smart Contracts The core onchain infrastructure deployed on each connected chain. ### PolygonZKEVMBridgeV2.sol **Purpose**: Main bridge contract that serves as the interface for all cross-chain transactions. **Key Functions**: * `bridgeAsset()`: Initiates asset transfers between chains by validating destination networks, handling different token types (native gas tokens, WETH, ERC20), locking or burning tokens on the source chain, and recording the transaction in the Local Exit Tree. * `bridgeMessage()`: Initiates message transfers between chains by packaging message data with optional ETH value, validating gas token conditions, and recording the message in the Local Exit Tree for later execution on the destination chain. * `claimAsset()`: Claims bridged assets on the destination chain by verifying Merkle proofs against the Global Exit Root, ensuring the transaction hasn't been claimed before, and transferring or minting the appropriate tokens to the recipient address. * `claimMessage()`: Claims bridged messages on the destination chain by verifying Merkle proofs, executing the message on the target contract (if it implements `IBridgeMessageReceiver`), and handling ETH/WETH value transfers. **Data Management**: * Maintains Local Exit Tree (LET) for the chain as a 32-level Sparse Merkle Tree, storing cryptographic commitments of all outgoing bridge transactions and updating the root with each new transaction. * Records all outgoing bridge transactions with complete transaction details including destination network, recipient address, token amounts, and metadata, creating an immutable audit trail. * Handles complex token operations including locking native tokens in escrow, burning foreign tokens, transferring native ERC20 tokens, and minting wrapped tokens on destination chains based on token origin and type. **Deployment**: Deployed on both L1 and all connected L2s ### PolygonRollupManager.sol **Purpose**: L1 contract that manages rollup state updates and coordinates L2 submissions. **Key Functions**: * `updateRollupExitRoot()`: Updates the rollup exit root when L2s submit their Local Exit Roots, validating the cryptographic proofs and ensuring the submitted state transitions are legitimate before updating the aggregated rollup state. * `verifyBatches()`: Verifies L2 batch submissions by checking zero-knowledge proofs that demonstrate the validity of state transitions, ensuring that all transactions in the batch were executed correctly according to the L2's rules. * `sequenceBatches()`: Sequences L2 batches on L1 by ordering and timestamping batch submissions, providing a canonical ordering of L2 operations that enables deterministic state reconstruction. **Data Management**: * Maintains Rollup Exit Tree (RET) as a Sparse Merkle Tree where each leaf represents a Local Exit Root from a connected L2, enabling efficient aggregation of all L2 bridge states into a single root hash. * Tracks all L2 Local Exit Roots by storing the latest submitted root from each connected chain along with metadata like submission timestamps and batch numbers for audit and synchronization purposes. * Updates Global Exit Root when RET changes by automatically triggering updates in the `PolygonZkEVMGlobalExitRootV2.sol` contract, ensuring the unified global state reflects all L2 bridge activities. **Deployment**: Deployed only on L1 ### PolygonZkEVMGlobalExitRootV2.sol **Purpose**: L1 contract that maintains the Global Exit Root (GER) and L1 Info Tree. **Key Functions**: * `updateGlobalExitRoot()`: Updates the Global Exit Root when either the Rollup Exit Root or Mainnet Exit Root changes, computing the new GER as `hash(RollupExitRoot, MainnetExitRoot)` and appending it to the L1 Info Tree for historical tracking and L2 synchronization. * `getGlobalExitRoot()`: Returns the current Global Exit Root that represents the unified state of all cross-chain activities across the entire network, used by L2s for synchronization and by users for generating claim proofs. * `getL1InfoTreeRoot()`: Returns the root of the L1 Info Tree which contains all historical Global Exit Roots, enabling L2s to sync with specific historical states and generate valid Merkle proofs for transactions from any point in time. **Data Management**: * Maintains Global Exit Root (hash of RER and MER) as the single source of truth for the entire network's cross-chain state, automatically recalculating whenever either component root changes to ensure consistency. * Maintains L1 Info Tree with historical GERs as a 32-level Sparse Merkle Tree, storing every Global Exit Root update as a timestamped leaf to enable historical state queries and proof generation for past transactions. * Provides GER synchronization for L2s by exposing the latest Global Exit Root and L1 Info Tree root, allowing L2 contracts to fetch and verify the current unified state for processing incoming cross-chain claims. **Deployment**: Deployed only on L1 ### PolygonZkEVMGlobalExitRootL2.sol **Purpose**: L2 contract that synchronizes with L1 Global Exit Root updates. **Key Functions**: * `updateExitRoot()`: Syncs with the latest Global Exit Root from L1 by calling the L1 Global Exit Root contract, fetching the current GER and L1 Info Tree root, and updating the L2's local copy to enable validation of incoming cross-chain claims. * `getGlobalExitRoot()`: Returns the current synchronized Global Exit Root stored on this L2, which is used by the bridge contract to verify Merkle proofs during claim operations and ensure claims are based on the latest global state. * `getL1InfoTreeRoot()`: Returns the synchronized L1 Info Tree root that corresponds to the current Global Exit Root, enabling the L2 to validate that claim proofs are based on legitimate historical states from the L1 Info Tree. **Data Management**: * Maintains synchronized copy of L1 GER by periodically fetching updates from the L1 Global Exit Root contract and storing them locally, ensuring the L2 has the latest unified network state for claim verification. * Maintains synchronized copy of L1 Info Tree root along with the corresponding Global Exit Root, creating a consistent state snapshot that enables proper validation of Merkle proofs during cross-chain claim operations. * Enables L2 to verify cross-chain claims by providing the necessary Global Exit Root and L1 Info Tree root data that the bridge contract uses to validate Merkle proofs and ensure claimed transactions were properly settled on L1. **Deployment**: Deployed on all connected L2s ## Bridge Service Off-chain infrastructure that provides indexing, APIs, and proof generation services. ### Chain Indexer Framework **Purpose**: EVM blockchain data indexer that parses and organizes blockchain data. **Key Features**: * **Real-time Indexing**: Continuously monitors the blockchain for bridge-related events by subscribing to new blocks and scanning for `BridgeEvent` and `ClaimEvent` logs, ensuring that all cross-chain transactions are captured immediately as they occur. * **Data Parsing**: Extracts and structures bridge transaction data from raw blockchain logs, converting hex-encoded event data into structured formats that include transaction details, token information, addresses, amounts, and timestamps. * **Event Processing**: Processes `BridgeEvent` logs (emitted during bridging) and `ClaimEvent` logs (emitted during claiming) to track the complete lifecycle of cross-chain transactions from initiation to completion. * **Database Storage**: Stores indexed data in structured databases optimized for API queries, enabling fast retrieval of transaction history, status updates, and proof generation data for user interfaces and applications. **Deployment**: One instance per connected chain **Technology**: Built on Polygon's Chain Indexer Framework ### Transaction API **Purpose**: Provides real-time bridge transaction status and details for user interfaces. **Key Endpoints**: * **Testnet**: `https://api-gateway.polygon.technology/api/v3/transactions/testnet?userAddress={userAddress}` * **Mainnet**: `https://api-gateway.polygon.technology/api/v3/transactions/mainnet?userAddress={userAddress}` **Response Data**: * Transaction status (pending, completed, failed) with real-time updates as transactions progress through the bridging and claiming phases * Token details including contract addresses, transfer amounts, token symbols, and decimals for accurate display in user interfaces * Source and destination chain information including network IDs, chain names, and block numbers where transactions were processed * Timestamps for transaction initiation, L1 settlement, and claim completion to track transaction lifecycle timing * Deposit count (Local Exit Tree index) required for Merkle proof generation during the claim process **Authentication**: Requires API key in request header **Example Usage**: ```bash theme={null} curl --location 'https://api-gateway.polygon.technology/api/v3/transactions/mainnet?userAddress=0x...' \ --header 'x-api-key: ' ``` ### Proof Generation API **Purpose**: Generates Merkle proofs required for claiming bridged assets and messages. **Key Endpoints**: * **Testnet**: `https://api-gateway.polygon.technology/api/v3/proof/testnet/merkle-proof?networkId={sourceNetworkId}&depositCount={depositCount}` * **Mainnet**: `https://api-gateway.polygon.technology/api/v3/proof/mainnet/merkle-proof?networkId={sourceNetworkId}&depositCount={depositCount}` **Parameters**: * `networkId`: Network ID registered on Agglayer that identifies the source chain (0 for Ethereum/Sepolia, 1 for Polygon zkEVM/Cardona, etc.), used to determine which Local Exit Tree contains the transaction. * `depositCount`: The leaf index from the source chain's Local Exit Tree (obtained from Transaction API response), which specifies exactly which transaction leaf to generate proofs for. **Response Data**: * `smtProofLocalExitRoot`: Merkle proof demonstrating that the specific transaction exists in the source chain's Local Exit Tree, providing the cryptographic path from the transaction leaf to the Local Exit Root. * `smtProofRollupExitRoot`: Merkle proof demonstrating that the source chain's Local Exit Root exists in the Rollup Exit Tree on L1 (only needed for L2 to L1/L2 transactions), proving the L2's state was properly submitted to L1. * `globalIndex`: The 256-bit Global Index that uniquely identifies this transaction within the entire network, encoding the source network type, rollup ID, and local index information. * `mainnetExitRoot`: The Mainnet Exit Root at the time this transaction was processed, used for constructing the Global Exit Root during claim verification. * `rollupExitRoot`: The Rollup Exit Root at the time this transaction was processed, used for constructing the Global Exit Root during claim verification. **Authentication**: Requires API key in request header ### Auto Claim Service (only on L2s) **Purpose**: Automated service that claims bridged transactions on destination chains. **Key Features**: * **Automatic Claiming**: Continuously monitors for claimable transactions across all connected chains by querying the Transaction API, detecting when transactions are ready to be claimed (L1 finalized), and automatically executing claim transactions to complete the cross-chain transfer. * **Gas Optimization**: Optimizes gas usage for claim transactions by batching multiple claims together when possible, using dynamic gas pricing based on network conditions, and implementing efficient claim strategies to minimize transaction costs. * **Error Handling**: Handles failed claims gracefully by implementing retry mechanisms with exponential backoff, logging detailed error information for debugging, and providing fallback strategies when primary claim methods fail. * **Monitoring**: Provides comprehensive monitoring and alerting for claim operations including success/failure rates, processing times, gas usage statistics, and automated notifications when manual intervention is required. **Deployment Options**: * **DApp Integration**: Deploy as part of your dApp infrastructure to provide automatic claiming for your users, eliminating manual claim steps in cross-chain workflows. * **Chain Integration**: Deploy as a chain-level service where the L2 operator runs the claiming service for all users, providing a public good that improves the overall user experience on that chain. * **Standalone Service**: Deploy as an independent claiming service that can serve multiple dApps or chains, potentially monetized through small fees or operated as a community service. **Configuration**: * Source and destination chain RPC URLs for monitoring bridge events and submitting claim transactions across all supported networks * Bridge contract addresses for each supported chain to interact with the correct bridge instances * Private keys for claiming with appropriate security measures and key rotation policies * Gas price settings including maximum gas prices, priority fees, and dynamic pricing strategies based on network congestion # Data Structures Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/unified-bridge/data-structures Reference for the Unified Bridge Merkle tree hierarchy: Local Exit Root, Rollup Exit Root, Mainnet Exit Root, Global Exit Root, and Global Index ## Overview The Unified Bridge uses a hierarchical Merkle tree structure to track and verify all cross-chain transactions. Every transaction is cryptographically verifiable, and source chain transactions must be finalized on L1 before they can be claimed on the destination chain. Unified Bridge Data Structure *Figure 1: Complete data structure hierarchy showing how Local Exit Roots, Rollup Exit Root, Mainnet Exit Root, and Global Exit Root work together* ## Local Exit Root & Local Index Each Agglayer connected chain maintains its own Local Exit Tree (LET) that records all outgoing cross-chain transactions. ### Local Exit Tree (LET) * **Structure**: A 32-level binary Sparse Merkle Tree that efficiently stores bridge transaction data. The tree uses a sparse representation, meaning it only stores non-zero values, making it highly efficient for storing mostly-empty trees with occasional transactions. * **Purpose**: Records all bridge transactions initiated on the chain as cryptographic commitments. Each outgoing `bridgeAsset` or `bridgeMessage` transaction is hashed and stored as a leaf node, creating an immutable record of all cross-chain activities originating from this chain. * **Storage**: Maintained in the `PolygonZKEVMBridgeV2.sol` contract deployed on each chain. This contract serves as both the user interface for bridge operations and the storage mechanism for the Local Exit Tree state. * **Updates**: The tree root is recalculated and updated with each new cross-chain transaction. This ensures that the Local Exit Root always represents the current state of all bridge transactions from this chain. ### Local Index (depositCount) * **Definition**: The sequential index of the leaf node in the Local Exit Tree, starting from 0 and incrementing by 1 for each new transaction. This creates a unique identifier for each bridge transaction within the chain's Local Exit Tree. * **Value**: Each leaf at this index represents a Keccak256 hash of a complete cross-chain transaction, including all transaction details like destination chain, recipient address, token amount, and metadata. * **Increment**: Automatically incremented with each new bridge transaction, ensuring that every cross-chain operation gets a unique position in the tree. This index is crucial for generating Merkle proofs during the claim process. Local Exit Tree *Figure 2: Local Exit Tree structure showing how bridge transactions are recorded as leaves* ## Rollup Exit Root The Rollup Exit Root (RER) is the Merkle root of all L2s' Local Exit Roots, maintained on L1. ### How it Works 1. **L2 Submission**: Connected L2s periodically submit their updated Local Exit Root to the `PolygonRollupManager.sol` contract on L1. This submission includes cryptographic proof that the Local Exit Root represents valid bridge transactions that have been properly sequenced and finalized on the L2. 2. **Frequency**: L2s have flexibility in submission timing - they can submit their Local Exit Root immediately after each bridge transaction for fastest finality, or batch multiple transactions together before submitting to optimize for L1 gas costs and throughput. 3. **RER Update**: The RollupManager validates the submitted Local Exit Root and updates the corresponding leaf in the Rollup Exit Tree. This creates a new Rollup Exit Root that represents the aggregated state of all connected L2s' bridge activities. 4. **GER Update**: When the Rollup Exit Root changes, it automatically triggers an update to the Global Exit Root in the `PolygonZkEVMGlobalExitRootV2.sol` contract, ensuring the unified state is always current. ### Key Contracts * **PolygonRollupManager.sol**: Manages L2 state updates on L1 by validating submitted Local Exit Roots, maintaining the Rollup Exit Tree, and coordinating with the Global Exit Root contract for unified state updates. * **PolygonZkEVMGlobalExitRootV2.sol**: Automatically updates the Global Exit Root whenever the Rollup Exit Root or Mainnet Exit Root changes, and manages the L1 Info Tree for historical GER tracking. Rollup Exit Tree *Figure 3: Rollup Exit Tree showing how L2 Local Exit Roots are aggregated* ## Mainnet Exit Root The Mainnet Exit Root (MER) tracks L1 to L2 bridge transactions, similar to how L2s track their outgoing transactions. ### How it Works 1. **L1 Bridge**: When users initiate bridge transactions from L1 to connected L2s, these transactions are recorded directly in L1's own Local Exit Tree (called Mainnet Exit Tree). This happens immediately within the same transaction that initiates the bridge operation. 2. **MER Update**: The Mainnet Exit Root is automatically updated in the `PolygonZkEVMGlobalExitRootV2.sol` contract whenever L1 bridge transactions occur. Unlike L2s, L1 doesn't need to submit its Local Exit Root separately since the Global Exit Root contract is on L1 itself. 3. **GER Update**: Any Mainnet Exit Root update immediately triggers a Global Exit Root recalculation, which then gets appended to the L1 Info Tree for L2 synchronization. ### Key Difference * **L2s**: Must submit their Local Exit Roots to L1 via the RollupManager contract, creating a two-step process where L2 transactions are first finalized locally, then submitted to L1 for global state updates. * **L1**: Updates its own Mainnet Exit Root directly within the Global Exit Root contract during the bridge transaction itself, eliminating the need for separate submission transactions. Mainnet Exit Tree *Figure 4: Mainnet Exit Tree showing how L1 bridge transactions are tracked* ## Global Exit Root The Global Exit Root (GER) is the root hash that combines both Rollup Exit Root and Mainnet Exit Root. ### Formula ``` GER = hash(RollupExitRoot, MainnetExitRoot) ``` ### L1 Info Tree The L1 Info Tree is a 32-level binary Sparse Merkle Tree that maintains all Global Exit Roots: * **Purpose**: Serves as a historical record of all Global Exit Root updates, enabling L2s to synchronize with specific points in time and generate valid Merkle proofs for claims. This tree is essential for the claim verification process. * **Height**: Uses 32 levels to provide sufficient capacity for storing Global Exit Root updates over the system's lifetime. The sparse nature means only populated leaves consume storage. * **Updates**: A new leaf is added to the tree each time the Global Exit Root changes (either from Rollup Exit Root or Mainnet Exit Root updates). Each leaf contains the new Global Exit Root value along with timestamp information. * **Sync**: L2s periodically call the `updateExitRoot` function on their `PolygonZkEVMGlobalExitRootL2.sol` contract to fetch and synchronize with the latest Global Exit Root from L1, ensuring they can validate incoming claims. ### Global Index The Global Index is a 256-bit identifier that uniquely locates each cross-chain transaction within the global system: | Bits | Purpose | Description | | -------- | ---------------- | ------------------------------------------------------------------------------------------------------ | | 191 bits | Unused | Reserved bits typically filled with zeros for cost efficiency in storage and computation | | 1 bit | Mainnet Flag | Indicates transaction origin: 0 = transaction from L2 rollup, 1 = transaction from L1 mainnet | | 32 bits | Rollup Index | Identifies the specific L2 rollup within the Rollup Exit Tree (only used when mainnet flag = 0) | | 32 bits | Local Root Index | The depositCount/leaf index within the source chain's Local Exit Tree where this transaction is stored | This structure enables efficient lookup of any transaction across the entire network by encoding the path through the hierarchical tree structure. L1 Info Tree *Figure 5: L1 Info Tree structure showing how Global Exit Roots are maintained* ## Data Flow ### Flow for L1 -> L2 Bridge Asset 1. User/Developer/Dapp initiate `bridgeAsset` call on L1 2. Bridge contract on L1 appends an exit leaf to mainnet exit tree of the L1, and update its mainnet exit root. 3. Global exit root manager appends the new L1 mainnet exit root to global exit tree and computes the new global exit root. 4. L2 sequencer fetches and updates the latest global exit root from the global exit root manager. 5. User/Developer/Dapp/Chain initiates `claimAsset` call, and also provides the smtProof. 6. Bridge contract on destination L2 chain validates the smtProof against the global exit root on its chain. If passes next step. 7. Transfer/Mint the asset to the destination address. Bridge Asset L1 to L2 ### Flow for L2 -> L1 Bridge Message 1. User/Developer/Dapp initiate `bridgeMessage` call on L2 2. Bridge contract on L2 appends an exit leaf to local exit tree of the L2, and update its local exit root on L2. 3. Sends the new local exit root to L1 to verify, once passed the L2's local exit root, aka the leaf node in the rollup exit tree will be updated, which will cause a chain of updates to Global exit root updates on L1 and also L1InfoTree updates. 4. User/Developer/Dapp/Chain initiates `claimMessage` call, and also provides the smtProof. 5. Bridge contract on destination L1 chain validates the smtProof against the global exit root on its chain. If passes next step. 6. Execute `onMessageReceived` process. Bridge Message L2 to L1 # Message Bridging Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/unified-bridge/message-bridging How message bridging works in the Unified Bridge: bridgeMessage/claimMessage function signatures, the IBridgeMessageReceiver interface, and execution flows ## Overview Message bridging enables smart contracts on different chains to communicate and trigger execution across chains. A contract on the source chain calls `bridgeMessage` with encoded function data, and the bridge executes that data on the destination contract when the message is claimed. Message Bridging Process *Figure 1: Complete message bridging flow from L2 to L1* ## Message Model ### What Message Bridging Supports * **Contract-to-contract communication**: Smart contracts can trigger function calls on other chains * **Cross-chain state updates**: Contracts can update state on destination chains * **Trustless communication**: Cryptographic verification of all cross-chain messages via Merkle proofs ### Message Structure Cross-chain messages contain: * **Destination contract**: Address of the contract to execute on the destination chain * **Function data**: Encoded function call data * **Value**: ETH value to send with the message (if any) * **Gas limit**: Maximum gas for execution on the destination chain * **Metadata**: Additional data for the message ## Bridge Message Function The `bridgeMessage` function initiates message transfers between chains. ### Function Signature ```solidity theme={null} function bridgeMessage( uint32 destinationNetwork, address destinationAddress, uint256 gasLimit, bytes calldata data ) external payable ``` ### Parameters * **`destinationNetwork`**: Network ID of the destination chain * **`destinationAddress`**: Address of the contract to execute on destination chain * **`gasLimit`**: Maximum gas for execution on destination chain * **`data`**: Encoded function call data ### Process Steps 1. **Validation**: Check destination network is not the source network 2. **Value Handling**: Handle ETH value if provided 3. **Event Emission**: Emit `BridgeEvent` with message details 4. **Tree Update**: Add message to Local Exit Tree as leaf node ### Example Usage ```solidity theme={null} // Bridge a message to call a function on destination chain bridgeMessage( 1, // destinationNetwork (L2) 0x..., // destinationAddress (contract address) 100000, // gasLimit abi.encodeWithSignature("updateValue(uint256)", 123) // data ); ``` ## Claim Message Function The `claimMessage` function claims and executes bridged messages on the destination chain. ### Function Signature ```solidity theme={null} function claimMessage( bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofLocalExitRoot, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofRollupExitRoot, uint256 globalIndex, bytes32 mainnetExitRoot, bytes32 rollupExitRoot, uint32 originNetwork, address originAddress, uint32 destinationNetwork, address destinationAddress, uint256 gasLimit, bytes calldata data ) external ``` ### Parameters * **`smtProofLocalExitRoot`**: Merkle proof for Local Exit Root * **`smtProofRollupExitRoot`**: Merkle proof for Rollup Exit Root * **`globalIndex`**: Global index identifying the message * **`mainnetExitRoot`**: Mainnet Exit Root at time of message * **`rollupExitRoot`**: Rollup Exit Root at time of message * **`originNetwork`**: Network ID of source chain * **`originAddress`**: Address that sent the message * **`destinationNetwork`**: Network ID of destination chain * **`destinationAddress`**: Address of the contract to execute * **`gasLimit`**: Maximum gas for execution * **`data`**: Encoded function call data ### Process Steps 1. **Validation**: Verify destination network matches current chain 2. **Proof Verification**: Verify Merkle proofs against Global Exit Root 3. **Duplicate Check**: Ensure message hasn't been claimed before 4. **Message Execution**: Execute the message on destination contract 5. **Claim Record**: Mark message as claimed ### Message Execution ```solidity theme={null} // Execute the message on destination contract (bool success, bytes memory returnData) = destinationAddress.call{ value: msg.value, gas: gasLimit }(data); require(success, "Message execution failed"); ``` **Important Notes:** * Messages can only be executed if the `destinationAddress` is a smart contract that implements the `IBridgeMessageReceiver` interface * If the receiving address is an EOA, the call will result as a success, meaning that the amount of ether will be transferred correctly, but the message will not trigger any execution * If the native gas token is `ETH`, then transfer `ETH` to the `destinationAddress` and execute the message * If `ETH` is not the native gas token, then mint `WETH` to the `destinationAddress` and execute the message ### IBridgeMessageReceiver Interface For a contract to receive bridged messages, it must implement the `IBridgeMessageReceiver` interface: ```solidity theme={null} interface IBridgeMessageReceiver { function onMessageReceived( address originAddress, uint32 originNetwork, bytes calldata data ) external payable; } ``` **Parameters:** * `originAddress`: Address that sent the message on the source chain * `originNetwork`: Network ID of the source chain * `data`: The message data/metadata sent from source chain ## Bridging Flows ### L1 to L2 Message Bridging ### L2 to L1 Message Bridging ### L2 to L2 Message Bridging # What is Agglayer? Source: https://docs.polygon.technology/interoperability/agglayer/core-concepts/what-is-agglayer CDK's interoperability layer: how it addresses blockchain fragmentation and what it enables for CDK chains Agglayer is the interoperability layer built into every Polygon Chain Development Kit (CDK) chain. When you deploy a CDK chain, it connects to Agglayer by default, the cross-chain capabilities described here come as part of the infrastructure, not as a separate integration step. ## The Blockchain Fragmentation Problem Today's blockchain ecosystem is fragmented by design. Ethereum provides well-established security and a deep DeFi ecosystem. Polygon offers fast transactions and low fees. Arbitrum uses optimistic rollup technology. Each chain has evolved to solve specific problems, but they exist in isolation. The result is that users must manage multiple wallets, developers rebuild the same functionality across chains, and assets remain trapped in their respective ecosystems. Moving ETH from Ethereum into a DeFi protocol on Polygon requires bridging, waiting for confirmations, dealing with wrapped tokens, and accepting the risk of multi-step cross-chain operations. ## What Agglayer Is Agglayer is CDK's built-in interoperability layer. Rather than building bridges between specific pairs of chains, it creates shared infrastructure where: * **Assets maintain their identity across chains.** No wrapped tokens. ETH on Ethereum is the same ETH you use on a connected chain. * **Operations are atomic across multiple chains.** A cross-chain transaction either succeeds on all involved chains or fails entirely, with no partial states. * **Security is enforced mathematically.** Cryptographic proofs replace trusted validators. The system assumes any chain could be compromised and builds constraints around that assumption. * **Chains remain sovereign.** Connected chains keep their own architecture, consensus rules, and governance. Agglayer provides interoperability without requiring chains to give up independence. ## How Agglayer Works ### The Three Security Gateways **Gateway 1: The Unified Bridge** The Unified Bridge is where cross-chain transactions execute. When assets or messages move between chains, the Unified Bridge handles cryptographic verification and state management using Merkle tree structures that track every cross-chain operation. Transactions are settled on Ethereum before they can be claimed on destination chains. **Gateway 2: Pessimistic Proof** Rather than assuming all chains are honest, Agglayer assumes they might be compromised. The Pessimistic Proof system mathematically enforces that even if a chain's prover is unsound, it cannot withdraw more funds than are currently deposited on that chain. A compromised chain's damage is limited to its own deposits and cannot spread to the rest of the network. **Gateway 3: State Transition Proof** Introduced in Agglayer v0.3, the State Transition Proof adds a second verification layer that validates individual chain operations before cross-chain proofs are applied. This ensures both that a chain is operating correctly internally and that cross-chain operations are secure. ### Why Distrust Is the Foundation Traditional bridges require trust: a multi-signature wallet, a validator set, or a smart contract with administrative keys. There is always a point of centralized risk. Agglayer inverts this model. It starts from the assumption that any connected chain could be compromised and then enforces mathematical constraints that bound the damage: * Compromised provers cannot drain more than their chain's deposits. * Cryptographic proofs verify every operation rather than relying on authoritative validation. * Problems on one chain cannot spread to others because each chain's state is verified independently. ## Why This Matters for CDK Chains ### For chain operators Your CDK chain participates in a shared network by default. Assets on your chain can move to and from other connected chains without bridging infrastructure you need to build or maintain. Cross-chain liquidity and connectivity come with the deployment. ### For developers building on CDK chains Applications can use the best properties of each chain in the network. High-value operations can rely on Ethereum's security. Frequent transactions can use chains optimized for throughput. Domain-specific chains can handle specialized functionality. All of this works within a single application without per-chain integration overhead. ### For users Cross-chain operations become comparable in complexity to single-chain operations. The underlying routing, proof generation, and state verification happen at the protocol level rather than requiring manual steps from the user. ## Technical Components Agglayer coordinates several systems: * **Multiple cryptographic proof systems**: SP1 zkVM, Pessimistic Proofs, State Transition Proofs * **State synchronization** across connected chains * **Mathematical verification** of every cross-chain operation * **Hierarchical Merkle tree structures** (Local Exit Trees, Rollup Exit Tree, Global Exit Root) for efficient verification For detailed coverage of each component, see [Architecture](/interoperability/agglayer/core-concepts/architecture/), [Unified Bridge](/interoperability/agglayer/core-concepts/unified-bridge/), [Pessimistic Proof](/interoperability/agglayer/core-concepts/pessimistic-proof/), and [State Transition Proof](/interoperability/agglayer/core-concepts/state-transition-proof/). # Get Started Source: https://docs.polygon.technology/interoperability/agglayer/get-started/index Build cross-chain applications on Agglayer-connected CDK chains. ## Build Cross-Chain Applications on CDK Agglayer is built into every Chain Development Kit (CDK) chain. You can work with Agglayer's cross-chain capabilities in development and production without setting up separate interoperability infrastructure. ### What you can build on CDK chains * **Atomic cross-chain operations**: Execute operations across multiple chains in a single transaction * **Unified asset management**: Build applications where users' assets work across all supported chains * **Cross-chain smart contracts**: Create contracts that trigger actions and transfer value across different networks * **Multi-chain interfaces**: Build UIs that abstract chain selection away from the user ## Next steps * **[Supported chains](/interoperability/agglayer/supported-chains/)**: See which chains are connected to Agglayer, with RPC endpoints and chain IDs. * **[Integrations](/interoperability/agglayer/integrations/)**: Use Bridge Hub or the AggKit Bridge Service to query bridge data, generate claim proofs, and automate claiming across chains. # Agglayer: secure cross-chain bridge for heterogeneous blockchains Source: https://docs.polygon.technology/interoperability/agglayer/index Agglayer is a secure cross-chain bridge that connects the liquidity and users of heterogeneous blockchains in a single interoperability protocol. Pessimistic proofs, unified liquidity, atomic cross-chain operations. Bundled with Polygon CDK. ## Overview Agglayer is a secure cross-chain bridge that connects the liquidity and users of heterogeneous blockchains in a single interoperability protocol so assets can move between them. It provides the secure foundation that the Open Money Stack uses for cross-chain payments. Agglayer is bundled with Polygon CDK: every CDK chain ships with Agglayer connectivity by default. Other chains can also integrate independently, and the network is no longer EVM-only: Miden joined as a non-EVM connected chain. **What Agglayer provides:** * **Unified liquidity**: Assets maintain their identity across connected chains. No wrapped tokens required. * **Atomic cross-chain operations**: Transactions either succeed on all involved chains or fail entirely. No partial states. * **Mathematical security**: Pessimistic proofs ensure a compromised chain cannot drain more than its own deposits. Damage is contained. * **Chain sovereignty**: Connected chains keep their own architecture and governance. Agglayer adds interoperability without requiring changes to how a chain operates. ## Explore Agglayer Understand how Agglayer works: the three security gateways, proof systems, and how chains connect to the network. Start building cross-chain applications on Agglayer-connected chains. APIs and services for integrating Agglayer bridge functionality into your applications. # API Reference Source: https://docs.polygon.technology/interoperability/agglayer/integrations/aggkit-bridge-service/api-reference REST API documentation for the AggKit Bridge Service: endpoints, parameters, and response formats. ## API Base URL All Bridge Service endpoints follow this structure: ``` {base_url}/bridge/v1/{endpoint} ``` ## Health & Status ### `GET /` - Service Health Check **Example Request:** ```bash theme={null} curl "http://localhost:5577/" ``` **Response:** ```json theme={null} { "status": "healthy", "version": "1.0.0", "network_id": 1, "uptime": "2h15m30s", "last_processed_block": 15234567 } ``` **Response Fields:** * `status`: Service health indicator * `version`: API version for compatibility checks * `network_id`: Primary network being served * `uptime`: Service runtime duration * `last_processed_block`: Latest indexed block number ### `GET /sync-status` - Component Synchronization Status **Example Request:** ```bash theme={null} curl "http://localhost:5577/bridge/v1/sync-status" ``` **Response:** ```json theme={null} { "l1_sync": { "current_block": 15234567, "synced_block": 15234560, "is_synced": true }, "l2_sync": { "current_block": 8765432, "synced_block": 8765432, "is_synced": true }, "bridge_sync": { "l1_bridges_indexed": 12345, "l2_bridges_indexed": 6789, "last_update": "2024-09-24T10:30:45Z" } } ``` ## Bridge Transaction Queries ### `GET /bridges` - Query Bridge Transactions **Parameters:** | Parameter | Type | Required | Description | Example | | --------------- | ------- | -------- | ------------------------------------ | ----------- | | `network_id` | integer | ✅ | Origin network ID (0=L1, 1=L2, etc.) | `0` | | `page_number` | integer | ❌ | Page number (default: 1) | `1` | | `page_size` | integer | ❌ | Page size (default: 100, max: 1000) | `10` | | `deposit_count` | integer | ❌ | Filter by specific deposit count | `42` | | `from_address` | string | ❌ | Filter by sender address | `0xf39F...` | | `network_ids` | array | ❌ | Filter by destination network IDs | `1,2` | **Example Request Patterns:** ```bash theme={null} # Get recent bridges for a user curl "http://localhost:5577/bridge/v1/bridges?network_id=0&from_address=0xf39F...&page_size=10" # Find specific transaction by deposit count curl "http://localhost:5577/bridge/v1/bridges?network_id=0&deposit_count=42" # Check ready-to-claim transactions curl "http://localhost:5577/bridge/v1/bridges?network_id=0&ready_for_claim=true" ``` **Response Structure:** ```json theme={null} { "bridges": [ { "tx_hash": "0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c", "deposit_count": 42, "origin_network": 0, "destination_network": 1, "origin_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "destination_address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "amount": "1000000000000000000", "block_timestamp": 1695563045, "ready_for_claim": true } ], "pagination": { "page": 1, "limit": 10, "total": 150 } } ``` **Key Response Fields:** * `tx_hash`: Bridge transaction identifier * `deposit_count`: Sequential index for proof generation * `amount`: Bridge amount in wei/token units * `ready_for_claim`: Whether transaction can be claimed * `block_timestamp`: When the bridge occurred ## Claim Tracking ### `GET /claims` - Query Claim Transactions **Key Parameters:** | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------- | | `network_id` | integer | ✅ | Network where claims occurred | | `from_address` | string | ❌ | Filter by claimer address | | `page_size` | integer | ❌ | Results per page (default: 100) | **Example Usage:** ```bash theme={null} # Check claims on a specific network curl "http://localhost:5577/bridge/v1/claims?network_id=1&page_size=10" # Find claims by specific user curl "http://localhost:5577/bridge/v1/claims?network_id=1&from_address=0x7099..." ``` **Response:** ```json theme={null} { "claims": [ { "tx_hash": "0xa9fa5418144f7c8c1b78cd0e5560d6550411667ef937b554636a613f933b3d9f", "global_index": "0x000000000000000000000000000000000000000000000000000000000000002a", "amount": "1000000000000000000", "block_timestamp": 1695563145, "bridge_tx_hash": "0x8d1b60d0eaab6f609955bdd371e8004f47349cc809ff1bee81dc9d37237a031c" } ] } ``` ## Proof Generation ### `GET /claim-proof` - Generate Claim Proof Generates the cryptographic proofs required to claim bridged assets. Pass the entire `proof` object returned to your claim contract function. **Required Parameters:** | Parameter | Type | Description | Example | | --------------- | ------- | ------------------------------------ | ------- | | `network_id` | integer | Origin network of bridge transaction | `0` | | `deposit_count` | integer | Deposit count from bridge event | `42` | | `leaf_index` | integer | L1 Info Tree index for proof | `15` | **Example Request:** ```bash theme={null} curl "http://localhost:5577/bridge/v1/claim-proof?network_id=0&deposit_count=42&leaf_index=15" ``` **Response Structure:** ```json theme={null} { "proof": { "smtProofLocalExitRoot": ["0x...", "0x...", "0x..."], "smtProofRollupExitRoot": ["0x...", "0x..."], "l1InfoTreeLeaf": { "globalExitRoot": "0x...", "blockNumber": 15234567, "timestamp": 1695563045 } } } ``` ## Additional Utilities ### `GET /l1-info-tree-index` - Get L1 Info Tree Index Returns the L1 Info Tree index required for the `leaf_index` parameter in `/claim-proof`. ```bash theme={null} curl "http://localhost:5577/bridge/v1/l1-info-tree-index?network_id=0&deposit_count=42" ``` **Response:** ```json theme={null} { "l1_info_tree_index": 15, "block_number": 15234567, "global_exit_root": "0x..." } ``` ### `GET /token-mappings` - Token Information Get token mapping information for cross-chain token relationships. **Use Case**: Display token information and cross-chain mappings in bridge interfaces. ```bash theme={null} curl "http://localhost:5577/bridge/v1/token-mappings" ``` ## Error Handling **Common Error Scenarios:** | Error Code | When It Happens | What To Do | | --------------------- | ---------------------------- | ----------------------------------------- | | `INVALID_NETWORK_ID` | Using unsupported network ID | Check supported networks via `/` endpoint | | `BRIDGE_NOT_FOUND` | Transaction not yet indexed | Wait for indexing to complete | | `PROOF_NOT_AVAILABLE` | L1 finality not reached | Wait for L1 confirmation | | `RATE_LIMIT_EXCEEDED` | Too many requests | Implement backoff and retry | **Example Error Response:** ```json theme={null} { "error": { "code": "INVALID_NETWORK_ID", "message": "Network ID 999 is not supported" } } ``` # AggKit Bridge Service Source: https://docs.polygon.technology/interoperability/agglayer/integrations/aggkit-bridge-service/index REST API for bridge data, transaction status, and proof generation without running AggKit infrastructure. **Source code:** [github.com/agglayer/aggkit](https://github.com/agglayer/aggkit) ## Overview The Bridge Service API is a REST API that provides access to bridge data from AggKit: transaction histories, claim statuses, cryptographic proofs, and network information. It exposes this data through simple HTTP endpoints, removing the need to run and maintain blockchain indexers or proof generation infrastructure. All endpoints follow the pattern: ``` {base_url}/bridge/v1/{endpoint} ``` No authentication is required for most operations. ## What the API provides * **Bridge transaction data**: Query bridge transactions by address, network, or transaction hash * **Claim status**: Check whether a bridge transaction is ready to claim * **Proof generation**: Retrieve the cryptographic proof required for claim transactions * **Network information**: Access bridge contract addresses and network configuration ## Quick start ```bash theme={null} # Check if the API is running curl "http://localhost:5577/bridge/v1/" # Get recent bridge transactions on a network curl "http://localhost:5577/bridge/v1/bridges?network_id=0&page_size=5" # Get bridges for a specific address curl "http://localhost:5577/bridge/v1/bridges?network_id=0&from_address=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # Check for transactions ready to claim curl "http://localhost:5577/bridge/v1/bridges?network_id=0&from_address=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266&ready_for_claim=true" # Generate a claim proof curl "http://localhost:5577/bridge/v1/claim-proof?network_id=0&deposit_count=42&leaf_index=15" ``` ## Reference All endpoints, parameters, and response formats. # API Reference Source: https://docs.polygon.technology/interoperability/agglayer/integrations/bridge-hub/api-reference Bridge Hub REST API: endpoints, parameters, and response formats for querying bridge transactions. The Bridge Hub API exposes interactive OpenAPI documentation at the `/docs` endpoint. You can browse endpoints and test API calls directly in the browser. The Bridge Hub API is a read-only REST API. All endpoints use the `GET` method, and all responses are returned as `application/json`. ## GET /transactions Query bridge transactions with filtering and cursor-based pagination. ### Parameters | Parameter | Type | Required | Description | | ----------------------- | ------- | -------- | -------------------------------------------------------- | | `status` | string | No | Filter by status: `BRIDGED`, `READY_TO_CLAIM`, `CLAIMED` | | `sourceNetworkIds` | string | No | Comma-separated source network IDs | | `destinationNetworkIds` | string | No | Comma-separated destination network IDs | | `fromAddress` | string | No | Filter by sender address | | `receiverAddress` | string | No | Filter by receiver address | | `limit` | integer | No | Results per page (default: 50) | | `startAfter` | string | No | Cursor for pagination | ### Example request ```bash theme={null} curl "https://bridge-hub.example.com/transactions?status=READY_TO_CLAIM&limit=2" ``` ### Example response ```json theme={null} { "data": [ { "hubUID": "tx-0001", "sourceNetwork": 1, "destinationNetwork": 2442, "transactionHash": "0xabc123...", "blockNumber": 19500000, "timestamp": 1710000000, "leafType": "ASSET", "originTokenNetwork": 1, "originTokenAddress": "0x0000000000000000000000000000000000000000", "receiverAddress": "0xdef456...", "fromAddress": "0x789abc...", "amount": "1000000000000000000", "depositCount": 42, "leafIndexForProof": 42, "globalIndex": "42", "status": "READY_TO_CLAIM", "lastUpdatedAt": 1710000300, "claimTransactionHash": null, "claimBlockNumber": null, "claimTimestamp": null } ], "nextStartAfterCursor": "eyJsYXN0SWQiOiJ0eC0wMDAxIn0=" } ``` ### Response fields | Field | Type | Description | | ---------------------- | -------------- | ---------------------------------------------- | | `hubUID` | string | Unique business key for the transaction | | `sourceNetwork` | number | Source chain ID | | `destinationNetwork` | number | Destination chain ID | | `transactionHash` | string | Source transaction hash | | `blockNumber` | number | Block number on the source chain | | `timestamp` | number | Unix timestamp of the bridge event | | `leafType` | string | `ASSET` or `MESSAGE` | | `originTokenNetwork` | number | Network ID where the token originates | | `originTokenAddress` | string | Token contract address on the origin network | | `receiverAddress` | string | Address that will receive funds on destination | | `fromAddress` | string | Sender address on the source chain | | `amount` | string | Transfer amount (BigInt encoded as string) | | `depositCount` | number | Deposit counter from the bridge event | | `leafIndexForProof` | number | Leaf index in the merkle tree | | `globalIndex` | string | Global index (encoded as string) | | `status` | string | `BRIDGED`, `READY_TO_CLAIM`, or `CLAIMED` | | `lastUpdatedAt` | number | Unix timestamp of last status update | | `claimTransactionHash` | string or null | Claim transaction hash (populated after claim) | | `claimBlockNumber` | number or null | Block number of the claim transaction | | `claimTimestamp` | number or null | Unix timestamp of the claim | ## GET /claim-proof Generate a merkle proof for claiming a bridge transaction. This endpoint proxies to the source chain's AggKit Bridge Service to retrieve the proof data needed to submit a claim on the destination chain. ### Parameters | Parameter | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------------- | | `sourceNetworkId` | integer | Yes | Source chain network ID | | `depositCount` | integer | Yes | Deposit counter from bridge event | | `leafIndex` | integer | Yes | Leaf index in merkle tree | ### Example request ```bash theme={null} curl "https://bridge-hub.example.com/claim-proof?sourceNetworkId=1&depositCount=42&leafIndex=42" ``` ### Example response ```json theme={null} { "proof_local_exit_root": ["0x...", "0x..."], "proof_rollup_exit_root": ["0x...", "0x..."], "l1_info_tree_leaf": { "l1InfoTreeIndex": 100, "rer": "0x...", "mer": "0x...", "innerBlock": 19500000, "innerTimestamp": 1710000000 }, "bridge_tx_metadata": "0x..." } ``` ### Response fields | Field | Type | Description | | ------------------------ | --------- | ------------------------------------------------- | | `proof_local_exit_root` | string\[] | Merkle proof siblings for the local exit root | | `proof_rollup_exit_root` | string\[] | Merkle proof siblings for the rollup exit root | | `l1_info_tree_leaf` | object | L1 info tree leaf data used in claim verification | | `bridge_tx_metadata` | string | Encoded metadata for the bridge transaction | ## GET /token-mappings Get token address mappings between networks. Returns the relationship between original token addresses and their wrapped counterparts on destination networks. ### Example request ```bash theme={null} curl "https://bridge-hub.example.com/token-mappings" ``` ### Example response ```json theme={null} { "data": [ { "originNetwork": 1, "originTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "wrappedTokenAddress": "0x37eAA0eF3549a5bB7D431be78a3D99BD6fd6b57D", "destinationNetwork": 2442 } ] } ``` ## GET /token-metadata Get token metadata including name, symbol, and decimals for tokens tracked by Bridge Hub. ### Example request ```bash theme={null} curl "https://bridge-hub.example.com/token-metadata" ``` ### Example response ```json theme={null} { "data": [ { "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "networkId": 1, "name": "USD Coin", "symbol": "USDC", "decimals": 6 } ] } ``` ## GET /health-check Returns the service health status. ### Example request ```bash theme={null} curl "https://bridge-hub.example.com/health-check" ``` ### Example response ```json theme={null} { "status": "success", "data": { "status": "success", "message": "All services are working correctly" } } ``` ## Pagination The Bridge Hub API uses cursor-based pagination for the `/transactions` endpoint. 1. Make an initial request with an optional `limit` parameter. 2. If more results exist, the response includes a `nextStartAfterCursor` value. 3. Pass that value as the `startAfter` parameter in your next request to retrieve the following page. ```bash theme={null} # First page curl "https://bridge-hub.example.com/transactions?limit=50" # Next page (using the cursor from the previous response) curl "https://bridge-hub.example.com/transactions?limit=50&startAfter=eyJsYXN0SWQiOiJ0eC0wMDUwIn0=" ``` Cursor-based pagination provides several benefits over offset-based approaches: * **Stable results.** Concurrent inserts or updates do not cause skipped or duplicated entries between pages. * **Consistent performance.** Fetching page 1,000 is as fast as fetching page 1, since the database seeks directly to the cursor position. * **No deep pagination issues.** There is no growing cost as you move further through the result set. ## Error handling The API uses standard HTTP status codes and returns structured error responses. ### Status codes | Code | Meaning | Description | | ---- | ------------ | ------------------------------------------ | | 200 | Success | Request completed successfully | | 400 | Bad Request | Invalid or missing query parameters | | 404 | Not Found | Requested resource does not exist | | 500 | Server Error | An unexpected error occurred on the server | ### Error response format All error responses follow a consistent structure: ```json theme={null} { "status": "error", "message": "Invalid query parameter: status must be one of BRIDGED, READY_TO_CLAIM, CLAIMED" } ``` Error messages describe the problem without exposing sensitive internal details. If you receive a `500` response, retry the request after a short delay. For persistent errors, check the `/health-check` endpoint to verify service availability. # Architecture Source: https://docs.polygon.technology/interoperability/agglayer/integrations/bridge-hub/architecture Bridge Hub system architecture: consumer internals, multi-network deployment, and database design. ## System architecture The Bridge Hub is a microservices-based system that monitors, indexes, exposes, and automatically claims cross-chain bridge transactions. It consists of four packages that work together around a shared MongoDB database. ``` ┌──────────────────────────────────────────────────────────────────┐ │ External Systems │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Aggkit │ │ Blockchain │ │ MongoDB │ │ │ │ Bridge │ │ │ │ Database │ │ │ │ service │ │ │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ └─────────┼─────────────────┼─────────────────┼────────────────────┘ │ │ │ ▼ ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Bridge Hub Packages │ │ │ │ ┌───────────────┐ │ │ │ COMMONS │ │ │ │ (Shared Types)│ │ │ └───────┬───────┘ │ │ │ │ │ ┌─────────────┼─────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌─────────┐ ┌───────────┐ │ │ │ CONSUMER │ │ API │ │AUTO-CLAIM │ │ │ │ (Indexer)│ │(Service)│ │(Claimer) │ │ │ └─────┬────┘ └────┬────┘ └─────┬─────┘ │ │ │ │ │ │ │ │ Writes │ Reads │ HTTP │ │ ▼ ▼ ▼ │ │ ┌──────────────────────────┐ ┌─────────────┐ │ │ │ MongoDB Database │ │ Blockchain │ │ │ └──────────────────────────┘ └─────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` The four packages serve distinct roles: | Package | Layer | Responsibility | | -------------- | -------------- | ------------------------------------------------------------------------------- | | **Commons** | Foundation | Shared TypeScript types, interfaces, and schemas used by all other packages | | **Consumer** | Data ingestion | Polls AggKit Bridge Service APIs to index bridge transactions into MongoDB | | **API** | Service | Exposes indexed transaction data and proxies claim proofs over a REST interface | | **Auto-Claim** | Automation | Polls the API for claimable transactions and submits claim transactions onchain | **Commons** acts as the foundation layer. It contains pure TypeScript types with no runtime code, providing type-safe contracts that the other three packages depend on. **Consumer** writes data into MongoDB. **API** reads from the same database and serves it to clients. **Auto-Claim** consumes the API over HTTP and interacts with the blockchain to finalize claims. ## Consumer internals The Consumer package runs as a single Node.js process per network. It contains two components that together run four cron jobs. ### BridgeAPIConsumer This component runs three cron jobs that fetch data from the AggKit Bridge Service: 1. **bridgesCron**: Polls AggKit for new bridge deposit transactions. Each new deposit is inserted into the `transactions` collection with a status of `BRIDGED`. The cron tracks its progress by updating `lastIndexedBridgeDepositCount` in the metadata collection. 2. **claimsCron**: Polls AggKit for claim events that have occurred onchain. When a claim is detected, the corresponding transaction is updated to `CLAIMED` and the `claimTransactionHash` and timestamp are recorded. Progress is tracked via `lastIndexedClaimBlockNumber`. 3. **mappingsCron**: Polls AggKit for token mapping events. New or updated mappings are upserted into the `mappings` collection. Progress is tracked via `lastIndexedMappingBlockNumber`. ### ClaimReadinessConsumer This component runs a single cron job: 4. **readyToClaimCron**: Checks the L1 info tree data from AggKit and compares it against transactions that are currently in `BRIDGED` status. When a transaction becomes claimable, the cron updates it to `READY_TO_CLAIM` and sets the `leafIndexForProof` field needed for merkle proof generation. ### Consumer data flow ``` Aggkit Bridge Service (per network) │ ├── /bridges API ────▶ bridgesCron ────▶ transactions collection (BRIDGED) │ ├── /claims API ─────▶ claimsCron ─────▶ transactions collection (CLAIMED) │ ├── /mappings API ───▶ mappingsCron ───▶ mappings collection │ └── /l1-info-tree ───▶ readyToClaimCron ▶ transactions collection (READY_TO_CLAIM) ``` All four cron jobs run at configured intervals within the same process. The metadata collection tracks each cron's indexing checkpoint so the consumer can resume from the correct position after a restart. ## Multi-network deployment In production, the Bridge Hub runs **one consumer instance per source network** being indexed. Each network connected to the Agglayer has a unique network ID (for example, 0 for Ethereum, 1 for Polygon zkEVM). A single shared API service reads from the database and serves all networks, while one auto-claim instance runs per destination network. The consumer does **not** directly monitor the blockchain. It polls the AggKit Bridge Service APIs to fetch already-indexed data. Each chain's AggKit Bridge Service is maintained by the chain operators and is external to the Bridge Hub deployment. ``` ┌──────────────────────────────────────────────────────────────────────────────┐ │ AGGLAYER HUB API CLUSTER │ ├──────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │ │ Aggkit │ │ netId_0 Consumer │ │ │ │ Bridge │──────▶ │ ┌───────────────────────────┐ │ │ │ │ Service │ │ │ bridgesCron │ │ │ │ │ (net 0) │ │ │ claimsCron │ │──┐ │ │ └─────────────┘ │ │ readyToClaimCron │ │ │ │ │ │ │ mappingsCron │ │ │ │ │ │ └───────────────────────────┘ │ │ │ │ └─────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │ │ │ Aggkit │ │ netId_1 Consumer │ │ │ │ │ Bridge │──────▶ │ ┌───────────────────────────┐ │ │ │ │ │ Service │ │ │ bridgesCron │ │ │ │ │ │ (net 1) │ │ │ claimsCron │ │──┤ │ │ └─────────────┘ │ │ readyToClaimCron │ │ │ │ │ │ │ mappingsCron │ │ │ │ │ │ └───────────────────────────┘ │ │ │ │ └─────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │ │ │ Aggkit │ │ netId_n Consumer │ │ │ │ │ Bridge │──────▶ │ ┌───────────────────────────┐ │ │ │ │ │ Service │ │ │ bridgesCron │ │ │ │ │ │ (net n) │ │ │ claimsCron │ │──┤ │ │ └─────────────┘ │ │ readyToClaimCron │ │ │ │ │ │ │ mappingsCron │ │ │ │ │ │ └───────────────────────────┘ │ │ │ │ └─────────────────────────────────┘ │ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────┐ │ │ │ MongoDB Database │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ Collections (per environment): │ │ │ │ │ │ • bridge_hub_api_transactions │ │ │ │ │ │ • bridge_hub_api_mappings │ │ │ │ │ │ • bridge_hub_api_metadata │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ └────────────┬───────────────────────────────┘ │ │ │ │ │ ▼ Reads from │ │ ┌────────────────────────────────────────────┐ │ │ │ API Service │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ /transactions │ │ │ │ │ │ /token-mappings │ │ │ │ │ │ /token-metadata │ │ │ │ │ │ /claim-proof (proxies to Aggkit) │ │───┐ │ │ │ └──────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ │ HTTP Calls │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ Auto-Claim Service (per dest network) │◀─┘ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ Polls /transactions │ │ │ │ │ │ Fetches /claim-proof │ │ │ │ │ │ Submits claims to blockchain │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ ``` Key deployment points: * **Consumer instances**: One per source network being indexed (`netId_0`, `netId_1`, and so on). * **Shared database**: All consumers write to the same MongoDB instance. * **Single API**: One API service reads from the database and serves all networks. * **Auto-Claim deployment**: One instance per destination network you want to auto-claim for. ## Database design Bridge Hub uses a single shared MongoDB instance. Collections are organized by environment using the naming convention `bridge_hub_api_[type]_[environment]`, where the environment suffix is omitted for mainnet, `_testnet` for testnet, and `_devnet` for development. ### Collections The database contains three collection types: 1. **transactions**: Stores all bridge transactions across all networks. Modified by `bridgesCron` (upserts), `claimsCron` (status updates), and `readyToClaimCron` (status updates). Transactions move through the statuses `BRIDGED`, `LEAF_INCLUDED`, `READY_TO_CLAIM`, and `CLAIMED`. 2. **mappings**: Stores token address mappings between Agglayer networks. Modified by `mappingsCron`. Each record maps an origin token address on one network to its wrapped token address on another. 3. **metadata**: Tracks indexing progress per network. Each cron job updates its own checkpoint field in this collection. One document exists per network ID being indexed. ### Transaction document schema ```javascript theme={null} { _id: ObjectId, // MongoDB primary key hubUID: String (unique), // Business key // Network Information sourceNetwork: Number, // Source chain ID destinationNetwork: Number, // Destination chain ID // Transaction Details transactionHash: String, // Source transaction hash blockNumber: Number, // Block number on source timestamp: Number, // Unix timestamp bridgeHash: String, // Bridge Details leafType: String, // "ASSET" or "MESSAGE" originTokenNetwork: Number, originTokenAddress: String, receiverAddress: String, fromAddress: String, amount: String, // BigInt as string depositCount: Number, // Deposit counter // Claiming Details leafIndexForProof: Number, // Index for merkle proof globalIndex: String, // Global index as string // Status Tracking status: String, // BRIDGED, LEAF_INCLUDED, READY_TO_CLAIM, CLAIMED lastUpdatedAt: Number, // Last update timestamp // Claim Information (populated after claim) claimTransactionHash: String, claimBlockNumber: Number, claimTimestamp: Number } ``` ### Indexes ```javascript theme={null} { hubUID: 1 // Unique index status: 1, // Query by status { sourceNetwork: 1, destinationNetwork: 1 }, // Filter by networks depositCount: 1, // Order by deposit count { status: 1, destinationNetwork: 1 } // Combined index for common queries } ``` ### Metadata and resume capability The metadata collection is critical for operational resilience. When a consumer instance restarts after a crash, planned maintenance, or redeployment, it reads its metadata document to find the last indexed position for each cron job. Without this checkpoint data, the consumer would need to re-index from the beginning, duplicating hours or days of work. Each metadata document tracks three resume points: * `lastIndexedBridgeDepositCount`: where `bridgesCron` should resume * `lastIndexedClaimBlockNumber`: where `claimsCron` should resume * `lastIndexedMappingBlockNumber`: where `mappingsCron` should resume On startup, each cron reads its respective checkpoint and picks up exactly where it left off. ## Data synchronization The system maintains eventual consistency through three distinct data paths: * **Write path** (Consumer to MongoDB): Consumers poll AggKit APIs and write new or updated records into MongoDB. All writes use upsert operations, making them idempotent. Duplicate events from AggKit are handled gracefully. * **Read path** (API from MongoDB): The API service reads directly from MongoDB and serves the data over REST endpoints. Because the API is stateless and read-only, it can be scaled horizontally behind a load balancer. * **Claim path** (Auto-Claim to Blockchain): The Auto-Claim service polls the API for transactions in `READY_TO_CLAIM` status, fetches merkle proofs through the API's `/claim-proof` endpoint, and submits claim transactions onchain. Claims are processed sequentially to avoid nonce conflicts. ### Consistency guarantees * Transactions are immutable once created; only status fields are updated. * Status updates are atomic at the document level. * Duplicate events are handled via upsert, so re-processing the same data is safe. * No distributed transactions are needed because all state lives in a single MongoDB instance. * There is a small window of delay between an onchain event occurring and the consumer indexing it. During this window, the API may serve slightly stale data. This is acceptable for the bridge use case, where transactions take time to become claimable regardless. # Auto-Claim Service Source: https://docs.polygon.technology/interoperability/agglayer/integrations/bridge-hub/auto-claim Automated bridge transaction claiming: configuration, deployment, and security. ## Overview The auto-claim service automates the claiming of bridge transactions on the destination chain. It polls the Bridge Hub API for transactions in `READY_TO_CLAIM` status, fetches Merkle proofs, and submits claim transactions. It handles both `ASSET` and `MESSAGE` claim types and filters out zero-amount `MESSAGE` transactions. ## How it works The auto-claim service runs a continuous polling loop on a 30-second interval. Each cycle follows this sequence: 1. Poll the Bridge Hub API for transactions with `READY_TO_CLAIM` status. 2. Filter the returned transactions by source network and claim type. 3. For each transaction: fetch the Merkle proof, compute the global index, submit the claim transaction, and wait for onchain confirmation. 4. Sleep for 30 seconds, then repeat. ``` ┌─────────────────┐ │ Poll API │ │ (30s interval) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Filter Txs │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ For Each Tx: │ │ 1. Get Proof │ │ 2. Compute Index│ │ 3. Submit Claim │ │ 4. Wait Confirm │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Sleep 30s │ └────────┬────────┘ │ └──────→ (repeat) ``` Claims are processed sequentially to avoid nonce conflicts. One auto-claim instance should run per destination network. Errors on one transaction do not affect others. The blockchain enforces claim uniqueness, so the service is safe to restart at any time without risk of double-claiming. Failed claim transactions are reverted onchain and do not result in lost funds. ## Configuration The auto-claim service is configured through environment variables. | Variable | Required | Example | Description | | ----------------------------- | -------- | --------------------------- | ---------------------------------------- | | `BRIDGE_HUB_API_URL` | Yes | `http://api:3000` | Bridge Hub API URL | | `SOURCE_NETWORKS` | Yes | `[1,137]` | Source network IDs (JSON array) | | `DESTINATION_NETWORK` | Yes | `2442` | Destination network ID | | `DESTINATION_NETWORK_CHAINID` | Yes | `2442` | Destination chain ID | | `BRIDGE_CONTRACT` | Yes | `0x...` | Bridge contract address | | `PRIVATE_KEY` | Yes | `0x...` | Wallet private key for submitting claims | | `RPC_CONFIG` | Yes | `{"2442":"https://..."}` | RPC endpoints (JSON object) | | `SENTRY_DSN` | No | `https://...@sentry.io/...` | Error tracking DSN | ## Deployment Deploy one auto-claim instance for each destination network you want to auto-claim for. ### Docker Compose Add the following service to your `docker-compose.yml`: ```yaml theme={null} autoclaim: build: context: . dockerfile: Dockerfile.autoclaim environment: NODE_ENV: production BRIDGE_HUB_API_URL: http://api:3001 SOURCE_NETWORKS: ${SOURCE_NETWORKS} DESTINATION_NETWORK: ${DESTINATION_NETWORK} DESTINATION_NETWORK_CHAINID: ${DESTINATION_NETWORK_CHAINID} BRIDGE_CONTRACT: ${BRIDGE_CONTRACT} PRIVATE_KEY: ${PRIVATE_KEY} RPC_CONFIG: ${RPC_CONFIG} SENTRY_DSN: ${SENTRY_DSN} depends_on: - api restart: unless-stopped networks: - bridge-hub ``` ### Direct execution with Bun You can also run the service directly: ```bash theme={null} # Auto-claim for destination network 2442 BRIDGE_HUB_API_URL=http://localhost:3001 \ DESTINATION_NETWORK=2442 \ DESTINATION_NETWORK_CHAINID=2442 \ SOURCE_NETWORKS=[0,1,137] \ BRIDGE_CONTRACT=0x... \ PRIVATE_KEY=0x... \ RPC_CONFIG='{"2442":"https://rpc.example.com"}' \ bun start ``` To auto-claim for multiple destination networks, run a separate instance for each: ```bash theme={null} # Instance 1: destination 2442 DESTINATION_NETWORK=2442 DESTINATION_NETWORK_CHAINID=2442 bun start # Instance 2: destination 1101 DESTINATION_NETWORK=1101 DESTINATION_NETWORK_CHAINID=1101 bun start ``` ## Security The auto-claim service requires a private key to submit onchain transactions. Follow these guidelines to protect it: * **Never log or expose private keys.** Ensure your logging configuration does not capture environment variables. * **Load keys from a secret management system** such as AWS Secrets Manager, HashiCorp Vault, or Kubernetes Secrets. Avoid hardcoding keys in configuration files. * **Use a dedicated wallet with minimal funding.** The wallet only needs enough native tokens to cover gas on the destination chain. * **Monitor for unusual activity.** Set up alerts for unexpected balance changes or transaction patterns on the claiming wallet. Claims are atomic: they either succeed or revert entirely. The blockchain enforces uniqueness, so a transaction cannot be claimed twice even if the service processes it again after a restart. ## Troubleshooting | Problem | Solution | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Transactions stuck in `READY_TO_CLAIM` | Check that the claiming wallet has sufficient gas on the destination chain. | | "API unreachable" errors | Verify `BRIDGE_HUB_API_URL` is correct and accessible from the auto-claim service. | | Nonce conflicts | Ensure only one auto-claim instance runs per destination network. | | Claims failing | Check RPC endpoint connectivity. Verify the `BRIDGE_CONTRACT` address is correct for the destination network. | # Deployment Source: https://docs.polygon.technology/interoperability/agglayer/integrations/bridge-hub/deployment Deploy Bridge Hub to production: Docker, Kubernetes, configuration, and troubleshooting. ## Prerequisites ### Infrastructure requirements Each Bridge Hub component has specific compute requirements: | Component | CPU | RAM | Notes | | ---------- | ------- | ---- | --------------- | | API | 2+ vCPU | 4 GB | Per instance | | Consumer | 2+ vCPU | 4 GB | Per network | | Auto-Claim | 1+ vCPU | 2 GB | Per destination | You also need a MongoDB instance (version 4.4 or later). A 3-node replica set is recommended for production, with at least 100 GB of storage that can grow with transaction volume. ### Software requirements * **Bun** >= 1.0.0 * **MongoDB** >= 4.4 * **Docker** (optional; for containerized deployment) * **Kubernetes** (optional; for orchestrated deployment) ### External services * **Bridge Service API** access for each network you plan to index. * **Blockchain RPC endpoints** from a reliable provider. * **Sentry account** (optional) for error tracking. ## Build From the repository root, install dependencies and create production builds: ```bash theme={null} bun install bun run build ``` This outputs production artifacts to `packages/api/dist/`, `packages/consumer/dist/`, and `packages/auto-claim/dist/`. ## Docker deployment The repository includes production-ready Dockerfiles for each service: * `Dockerfile.api`:API service (exposes port 3001) * `Dockerfile.consumer`:Consumer service * `Dockerfile.autoclaim`:Auto-Claim service Each Dockerfile uses `oven/bun:1.2-alpine` with multi-stage builds and runs as a non-root user (`bunuser`). ### Build images ```bash theme={null} docker build -f Dockerfile.api -t bridge-hub-api:latest . docker build -f Dockerfile.consumer -t bridge-hub-consumer:latest . docker build -f Dockerfile.autoclaim -t bridge-hub-autoclaim:latest . ``` ### Docker Compose Create a `docker-compose.yml` in the repository root: ```yaml theme={null} version: "3.8" services: mongodb: image: mongo:7 volumes: - mongo-data:/data/db environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} ports: - "27017:27017" restart: unless-stopped networks: - bridge-hub api: build: context: . dockerfile: Dockerfile.api ports: - "3001:3001" environment: NODE_ENV: production MONGODB_CONNECTION_URI: ${MONGODB_CONNECTION_URI} MONGODB_DB_NAME: ${MONGODB_DB_NAME} PROOF_CONFIG: ${PROOF_CONFIG} RPC_CONFIG: ${RPC_CONFIG} SENTRY_DSN: ${SENTRY_DSN} depends_on: - mongodb restart: unless-stopped networks: - bridge-hub consumer-net1: build: context: . dockerfile: Dockerfile.consumer environment: NODE_ENV: production NETWORK_ID: ${CONSUMER_NETWORK_ID} NETWORK: ${CONSUMER_NETWORK} BRIDGE_SERVICE_URL: ${BRIDGE_SERVICE_URL} BRIDGE_CONTRACT_ADDRESS: ${BRIDGE_CONTRACT_ADDRESS} MONGODB_CONNECTION_URI: ${MONGODB_CONNECTION_URI} MONGODB_DB_NAME: ${MONGODB_DB_NAME} SENTRY_DSN: ${SENTRY_DSN} depends_on: - mongodb restart: unless-stopped networks: - bridge-hub autoclaim: build: context: . dockerfile: Dockerfile.autoclaim environment: NODE_ENV: production BRIDGE_HUB_API_URL: http://api:3001 SOURCE_NETWORKS: ${SOURCE_NETWORKS} DESTINATION_NETWORK: ${DESTINATION_NETWORK} DESTINATION_NETWORK_CHAINID: ${DESTINATION_NETWORK_CHAINID} BRIDGE_CONTRACT: ${BRIDGE_CONTRACT} PRIVATE_KEY: ${PRIVATE_KEY} RPC_CONFIG: ${RPC_CONFIG} SENTRY_DSN: ${SENTRY_DSN} depends_on: - api restart: unless-stopped networks: - bridge-hub volumes: mongo-data: networks: bridge-hub: driver: bridge ``` ### Adding consumer instances for additional networks To index more than one network, duplicate the `consumer-net1` block with a unique service name and network-specific environment variables. For example: ```yaml theme={null} consumer-net137: build: context: . dockerfile: Dockerfile.consumer environment: NETWORK_ID: 137 NETWORK: mainnet BRIDGE_SERVICE_URL: ${BRIDGE_SERVICE_URL_NET137} BRIDGE_CONTRACT_ADDRESS: ${BRIDGE_CONTRACT_ADDRESS_NET137} MONGODB_CONNECTION_URI: ${MONGODB_CONNECTION_URI} MONGODB_DB_NAME: ${MONGODB_DB_NAME} depends_on: - mongodb restart: unless-stopped networks: - bridge-hub ``` ### Common Docker Compose commands ```bash theme={null} # Start all services in the background docker-compose up -d # Follow logs from all services docker-compose logs -f # Follow logs from a specific service docker-compose logs -f api # Check service status docker-compose ps # Restart a single service docker-compose restart api # Stop all services docker-compose down ``` ## Kubernetes deployment ### Namespace and ConfigMap ```yaml theme={null} apiVersion: v1 kind: Namespace metadata: name: bridge-hub --- apiVersion: v1 kind: ConfigMap metadata: name: bridge-hub-config namespace: bridge-hub data: MONGODB_DB_NAME: "bridge_hub" NODE_ENV: "production" ``` ### API Deployment ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: bridge-hub-api namespace: bridge-hub spec: replicas: 3 selector: matchLabels: app: bridge-hub-api template: metadata: labels: app: bridge-hub-api spec: containers: - name: api image: bridge-hub-api:latest ports: - containerPort: 3000 env: - name: MONGODB_CONNECTION_URI valueFrom: secretKeyRef: name: bridge-hub-secrets key: mongodb-uri - name: MONGODB_DB_NAME valueFrom: configMapKeyRef: name: bridge-hub-config key: MONGODB_DB_NAME resources: requests: memory: "2Gi" cpu: "1000m" limits: memory: "4Gi" cpu: "2000m" livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: bridge-hub-api namespace: bridge-hub spec: selector: app: bridge-hub-api ports: - port: 80 targetPort: 3000 type: LoadBalancer ``` ### Apply and manage resources ```bash theme={null} # Apply all manifests kubectl apply -f k8s/namespace.yaml kubectl apply -f k8s/configmap.yaml kubectl apply -f k8s/secrets.yaml kubectl apply -f k8s/api-deployment.yaml kubectl apply -f k8s/consumer-deployment.yaml kubectl apply -f k8s/auto-claim-deployment.yaml # Check pod status kubectl get pods -n bridge-hub # Follow API logs kubectl logs -f -n bridge-hub deployment/bridge-hub-api # Scale the API horizontally kubectl scale deployment bridge-hub-api --replicas=5 -n bridge-hub ``` ## Configuration reference Never commit secrets to the repository. Use a secret management system such as AWS Secrets Manager, HashiCorp Vault, or Kubernetes Secrets. ### API package | Variable | Required | Example | Description | | ------------------------ | -------- | --------------------------------- | -------------------------- | | `MONGODB_CONNECTION_URI` | Yes | `mongodb://user:pass@host:27017` | MongoDB connection string | | `MONGODB_DB_NAME` | Yes | `bridge_hub` | Database name | | `RPC_CONFIG` | Yes | `{"mainnet":{"1":"https://..."}}` | RPC endpoints by network | | `PROOF_CONFIG` | Yes | `{"mainnet":{"1":"https://..."}}` | Proof generation endpoints | | `PORT` | No | `3000` | HTTP port (default: 3000) | | `NODE_ENV` | No | `production` | Environment mode | | `SENTRY_DSN` | No | `https://...@sentry.io/...` | Error tracking DSN | ### Consumer package | Variable | Required | Example | Description | | --------------------------- | -------- | --------------------------------------- | ------------------------------------- | | `NETWORK_ID` | Yes | `1` | Network identifier | | `NETWORK` | Yes | `mainnet` | Network name (mainnet/testnet/devnet) | | `BRIDGE_SERVICE_URL` | Yes | `https://bridge-api.polygon.technology` | Bridge Service API URL | | `BRIDGE_CONTRACT_ADDRESS` | Yes | `0x...` | Bridge contract address | | `MONGODB_CONNECTION_URI` | Yes | `mongodb://user:pass@host:27017` | MongoDB connection string | | `MONGODB_DB_NAME` | Yes | `bridge_hub` | Database name | | `ETROG_UPDATE_BLOCK_NUMBER` | No | `0` | Starting block for indexing | | `SENTRY_DSN` | No | `https://...@sentry.io/...` | Error tracking DSN | ### Auto-Claim package | Variable | Required | Example | Description | | ----------------------------- | -------- | --------------------------- | -------------------------------- | | `BRIDGE_HUB_API_URL` | Yes | `http://api:3000` | Bridge Hub API URL | | `SOURCE_NETWORKS` | Yes | `[1,137]` | Source network IDs (JSON array) | | `DESTINATION_NETWORK` | Yes | `2442` | Destination network ID | | `DESTINATION_NETWORK_CHAINID` | Yes | `2442` | Destination chain ID | | `BRIDGE_CONTRACT` | Yes | `0x...` | Bridge contract address | | `PRIVATE_KEY` | Yes | `0x...` | Wallet private key for gas funds | | `RPC_CONFIG` | Yes | `{"2442":"https://..."}` | RPC endpoints (JSON) | | `SENTRY_DSN` | No | `https://...@sentry.io/...` | Error tracking DSN | ## Adding a new network Start a new consumer with the network's configuration: ```bash theme={null} NETWORK_ID=42161 \ NETWORK=mainnet \ BRIDGE_CONTRACT_ADDRESS=0x... \ BRIDGE_SERVICE_URL=https://aggkit-42161.example.com \ MONGODB_CONNECTION_URI=mongodb://... \ bun start ``` In Docker Compose, add a new `consumer-net` service block. In Kubernetes, create a new Consumer Deployment manifest. Add the new network's RPC endpoint to `RPC_CONFIG` and its proof endpoint to `PROOF_CONFIG`, then restart the API instances so they pick up the change. Append the new network ID to the `SOURCE_NETWORKS` JSON array, then restart Auto-Claim instances. Confirm the pipeline is working end to end: * **Consumer indexing**: Query the MongoDB metadata collection for the new network's checkpoint. * **API serving data**: Call `GET /transactions?sourceNetworkIds=42161` and confirm results appear. * **Auto-Claim detection**: Monitor Auto-Claim logs to verify it picks up new transactions. ## Troubleshooting | Symptom | Likely cause | Solution | | --------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Services crash with MongoDB connection errors | MongoDB is unreachable or credentials are wrong | Verify MongoDB is running (`mongosh $MONGODB_CONNECTION_URI`). Check network connectivity with `telnet mongo-host 27017`. Review credentials in your environment variables. | | API queries return no data | Consumer is not running or has not indexed yet | Confirm the Consumer process is active. Check that MongoDB contains documents (`db.transactions.count()`). Review API logs for errors and verify your query parameters. | | Transactions stuck in `READY_TO_CLAIM` | Auto-Claim cannot submit transactions | Check that the wallet has sufficient gas on the destination chain. Verify `BRIDGE_HUB_API_URL` is reachable from the Auto-Claim service. Test RPC endpoint connectivity. Confirm the private key is correct. | | Excessive memory usage | Unbounded batch sizes or tight poll intervals | Review Consumer batch size settings and increase poll intervals. Add memory limits in Docker or Kubernetes resource specs. Check logs for memory leak indicators. | # Bridge Hub Source: https://docs.polygon.technology/interoperability/agglayer/integrations/bridge-hub/index Multi-chain bridge transaction indexing, API, and auto-claim system for Agglayer integrators. ## Overview Bridge Hub is a multi-chain indexing and claiming system for Agglayer bridge transactions. It polls each chain's AggKit Bridge Service, indexes transaction data into MongoDB, and exposes the results through a unified REST API. Bridge Hub is designed for integrators who need a single interface to query and manage bridge transactions across all chains connected to the Agglayer. ## Bridge Hub vs. AggKit Bridge Service | | AggKit Bridge Service | Bridge Hub | | --------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | **Scope** | Single chain | Multi-chain | | **Runs on** | Chain operator infrastructure | Integrator infrastructure | | **Persistence** | In-memory | MongoDB | | **Exposes** | Raw bridge data | Indexed data + auto-claim | | **Components** | 1 service | 4 packages | | **Repo** | [agglayer/agglayer-rs](https://github.com/agglayer/agglayer-rs) | [0xPolygon/agglayer-bridge-hub-api](https://github.com/0xPolygon/agglayer-bridge-hub-api) | | **Audience** | Chain operators | dApp developers, bridge UIs, integrators | ## Data flow CDK chain → AggKit Bridge Service → Bridge Hub Consumer → Bridge Hub API → Bridge UI / Auto-Claim ## Packages | Package | Layer | Description | | -------------- | -------------- | ----------------------------------------------------------------------------------------- | | **Commons** | Foundation | Shared TypeScript types, interfaces, and schema definitions used across all packages | | **Consumer** | Data ingestion | Polls AggKit Bridge Service APIs per chain and writes transaction data to MongoDB | | **API** | Service | REST API with OpenAPI documentation for querying transactions and generating claim proofs | | **Auto-Claim** | Automation | Polls the API for claimable transactions and submits claim transactions onchain | ## Transaction lifecycle A user initiates a bridge transaction on the source chain. The Consumer detects the deposit through the AggKit Bridge Service and saves the transaction to MongoDB with status `BRIDGED`. The Consumer monitors L1 info tree updates and determines when the transaction becomes claimable. It updates the transaction status to `READY_TO_CLAIM` and sets the leaf index needed for proof generation. The Auto-Claim service (or an external caller) fetches a merkle proof from the API and submits a claim transaction on the destination chain. Once confirmed, the Consumer detects the claim event and updates the status to `CLAIMED`. ## Technology stack * [Bun](https://bun.sh): JavaScript runtime * [Hono](https://hono.dev): Web framework for the REST API * [viem](https://viem.sh): TypeScript Ethereum library for blockchain interactions * [MongoDB](https://www.mongodb.com/): Document database for transaction storage * [Lerna](https://lerna.js.org/): Monorepo package management ## Next steps System design, consumer internals, and database schema. Configuration, environment variables, and production setup. REST endpoints, request parameters, and response schemas. Automated claiming service setup and operation. # Integrations Source: https://docs.polygon.technology/interoperability/agglayer/integrations/index Tools and APIs for integrating with Agglayer bridge infrastructure. ## Integrations Agglayer provides APIs and services for integrating cross-chain bridge functionality into your applications. Multi-chain bridge transaction indexer and API. Aggregates data from all connected chains into a single queryable interface with auto-claim support. REST API for accessing bridge data, transaction status, and proof generation without running infrastructure. # Supported Chains Source: https://docs.polygon.technology/interoperability/agglayer/supported-chains Chains connected to Agglayer: chain IDs, rollup IDs, RPC endpoints, block explorers, and gas tokens. ## Overview The following chains are connected to Agglayer. Each chain has a unique rollup ID assigned during registration. Chain types: * **PP (Pessimistic Proof)**: Sovereign mode secured by pessimistic proofs. No ZK prover required. * **Validium**: ZK-secured execution with offchain data availability. * **zkEVM**: Fully onchain ZK rollup. * **ALGateway**: Connected through the Agglayer Gateway. * **Outpost**: Non-CDK chain connected to Agglayer. ## Mainnets | Chain | Chain ID | Rollup ID | Type | Gas Token | RPC | Explorer | | ---------------------------------------------------------------------------------- | -------- | --------- | --------- | --------- | ------------------------------------------ | --------------------------------------------------------------------- | | [Ethereum](https://ethereum.org) | 1 | 0 | L1 | ETH | `https://eth.llamarpc.com` | [etherscan.io](https://etherscan.io) | | [Katana](https://docs.katana.network/) | 747474 | 20 | ALGateway | ETH | `https://rpc.katanarpc.com` | [katanascan.com](https://katanascan.com/) | | [Pentagon Games](https://pentagon.games/) | 3344 | 16 | Validium | PC | `https://rpc.pentagon.games` | [explorer.pentagon.games](https://explorer.pentagon.games/) | | [Silicon.network](https://docs.silicon.network) | 2355 | 10 | Validium | ETH | `https://rpc.silicon.network` | [scope.silicon.network](https://scope.silicon.network) | | [X Layer](https://web3.okx.com/xlayer/docs/developer/build-on-xlayer/about-xlayer) | 196 | 3 | PP | OKB | `https://rpc.xlayer.tech` | [oklink.com/x-layer](https://www.oklink.com/x-layer) | | [Ternoa](https://docs.ternoa.network) | 752025 | 13 | zkEVM | CAPS | `https://rpc-mainnet.zkevm.ternoa.network` | [explorer.ternoa.com](https://explorer.ternoa.com/) | | [Wirex Pay](https://partner.wirexpaychain.com/docs/getting-started) | 31415 | 8 | zkEVM | ETH | `https://rpc.wirexpaychain.com` | [blockscout.wirexpaychain.com](https://blockscout.wirexpaychain.com/) | ## Testnets | Chain | Chain ID | Rollup ID | Type | Gas Token | RPC | Explorer | | ------------------------------------- | ---------- | --------- | --------- | --------- | ---------------------------------- | ------------------------------------------------------------------------------------------ | | [Bokuto](https://docs.katana.network) | 737373 | 37 | ALGateway | ETH | `https://rpc-bokuto.katanarpc.com` | [tenderly.co/explorer/katana-bokuto](https://dashboard.tenderly.co/explorer/katana-bokuto) | | [Lumia](https://docs.lumia.org/) | 2030232745 | 35 | zkEVM | LUMIA | `https://beam-rpc.lumia.org` | [beam-explorer.lumia.org](https://beam-explorer.lumia.org/) | # Overview Source: https://docs.polygon.technology/interoperability/overview Cross-chain infrastructure on Polygon: Agglayer for chain-level interoperability, Intents for application-level cross-chain execution. Polygon's interoperability stack operates at two distinct levels. Agglayer connects chains at the infrastructure level, enabling shared liquidity and atomic cross-chain transactions. Intents (Trails) operate at the application level, letting developers accept any token from any chain without managing routes, bridges, or gas themselves. The two are complementary, not competing. Agglayer is the foundation that makes cross-chain movement secure and unified. Intents are how app developers consume that capability without dealing with its complexity. ## Agglayer Agglayer is an interoperability protocol that connects EVM chains so assets can move between them without wrapping, and operations can be atomic across chain boundaries. The core design principle is cryptographic containment: a pessimistic proof system ensures that if a connected chain is compromised, it cannot drain more than its own deposits into the shared pool. Damage cannot propagate. Connected chains retain their own architecture and governance, Agglayer adds interoperability without requiring structural changes to how a chain operates. CDK chains connect to Agglayer by default. Other chains integrate independently. Agglayer is relevant when you are a chain operator or builder, or when you are building an application that requires direct interaction with Agglayer's bridging and proof infrastructure. Architecture, security model, and how chains connect. Connect a chain or build cross-chain applications on Agglayer. ## Intents Intents are a developer abstraction for cross-chain execution. Instead of managing routing, bridging, swapping, and gas across chains, developers declare the desired outcome, "deliver 100 USDC on Polygon", and the system handles everything required to get there. Trails, Polygon's intent infrastructure, works across all EVM chains, including but not limited to Agglayer-connected ones. A user can start from any token on any chain; the system routes, swaps, and bridges to deliver exactly what was specified. The user signs once; no further interaction is required. Intents are relevant when you are building a product that needs to accept payments or deposits from users regardless of what chain or token they hold. The typical use cases are cross-chain payments, onramps, and multi-chain fund flows in consumer apps. How Trails works: intent addresses, execution, and settlement. Drop-in React component and headless SDK for integrating Trails. ## Which to use | You are... | Reach for... | | ------------------------------------------------------------- | --------------------- | | A chain operator connecting to Agglayer | Agglayer | | Building cross-chain apps on connected chains | Agglayer integrations | | Accepting any token from any chain in your app | Intents (Trails) | | Building an onramp or cross-chain payment flow | Intents (Trails) | | Both: a product on a CDK chain accepting cross-chain payments | Both | # What is the Open Money Stack? Source: https://docs.polygon.technology/oms/overview The Open Money Stack is Polygon's stablecoin payments infrastructure: fiat on-ramps and off-ramps, wallet infrastructure, compliance, stablecoin orchestration, and onchain settlement in one open, vertically integrated platform. The Open Money Stack is Polygon's open, end-to-end infrastructure for global money movement. It connects fiat access, wallets, compliance, routing, and settlement into one integration, designed to plug into existing systems and move money in seconds. Need regulated rails, custodial infrastructure, compliance services, or a managed chain deployment? [Contact Polygon](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=callout\&utm_campaign=oms_access). ## The problem with fragmented stacks Money should move reliably, at low cost. That requires predictable settlement without surprise deductions or delays. Most institutions building stablecoin payment flows today take the same path: choose a compliance vendor, a wallet provider, a bridge, an off-ramp, and a chain. When they have what they like, they stitch these solutions together. Each integration requires independent maintenance. In practice, this works, right up to the moment that volume grows or something goes wrong. Then teams must debug an outage that lives somewhere between three vendors and a dozen systems. ## A different architecture The Open Money Stack is **open and vertically integrated**: every layer is built to hand off cleanly to the next, with no lock-in. And it's composable. If an institution only wants to use one aspect of the Open Money Stack, say Polygon Chain for settlement or just the wallet infrastructure, they can pick and choose. **Use all of it, or only the components you need.** ## Settlement at the bottom of the stack Most payments orchestration companies don't own the infrastructure they route on. They aggregate across vendors they don't control, adding margin at every layer. When settlement breaks, they call their vendor. Polygon owns the settlement layer. The OMS is built on Polygon Chain: not as a dependency, but as infrastructure Polygon operates. **\$54B in stablecoin transfer volume**, **159M unique wallet addresses**, **6.4B total transactions**, and an average transaction cost of **\$0.002**, with live integrations by Revolut, Stripe, Flutterwave, and more. The economics compound at scale: costs improve as volume grows, rather than degrading through intermediary margin stacking. ## Unrolling the stack 1-click wallet creation with zero-config auth, passkeys, Smart Sessions, and enterprise-grade security. Custodial and non-custodial options both supported. Licensed fiat on- and off-ramps covering bank transfer, debit card, and cash at 50,000+ retail locations. KYC, AML, and compliance built in. 1-click transactions on any chain with any token. Deep unified liquidity across every connected network, with routing and bridging handled automatically. Polygon Chain for public settlement and Polygon Chain Development Kit (CDK) for dedicated rollup rails with 20,000+ TPS, compliance controls, and native Agglayer connectivity. Enterprise payments infrastructure for stablecoins and tokenized deposits. Native USDC with no wrapping, no bridges, and no hidden deductions. x402 for pay-per-use APIs. ERC-8004 for onchain agent identity. Infrastructure for autonomous agent commerce without human approval at every step. ## Composable primitives Underneath the layers, OMS is a suite of low-level APIs. Each one is a single money-movement primitive, and you combine them in different ways to build a product. A remittance app combines Receive, Convert, and Send. A dollar account combines Hold, Identify, and Ramp. Use one primitive on its own, or chain several into a flow. Move funds onchain to any wallet or address, plus bank and cash payout rails. Accept funds via cash-in, onchain transfers, deposit addresses, or virtual accounts. Quote and execute fiat/crypto and stablecoin swaps with deep liquidity. Store balances across currencies and chains in custodial or non-custodial wallets. Convert between fiat and crypto across bank rails (ACH, same-day ACH, wire, RTP), cards, international banks, and cash at retail. Move assets across chains with canonical and third-party bridge routes. Exchange tokens at quoted rates with slippage control and onchain settlement. Put idle balances to work: staking, lending, and yield strategies. Compose primitives into flows: splits, escrow, approvals, and policy gates. KYC and KYB verification routed to the right vendor for the customer's region. Stack the primitives, ship the product. See the [use cases](/payments/overview) for end-to-end patterns, or the [API reference](/api-reference/overview) for the full surface. ## Why integration changes the economics Historically, each of these layers existed independently: * Fiat access from one vendor * Wallet infrastructure from another * A bridge from a third * Settlement from whichever chain you prefer Every seam adds operational surface area. The Open Money Stack reduces that surface area. With the full OMS, a complete payment flow looks like this: 1. **Funds enter** through regulated fiat rails 2. **They settle** into a smart contract wallet instantly 3. **Orchestration** routes across borders and networks as needed 4. **Polygon finalizes** the transfer in under two seconds 5. **The recipient off-ramps** into local currency through compliant infrastructure Integration does not eliminate flexibility. Institutions can still extend, customize, and interoperate. They can pick and choose what they need. But they are not required to assemble foundational plumbing themselves. **Integrate once. Customize as desired. Move money end-to-end, globally, 24/7.** ## Who it's for * **Payment platforms and fintechs**: replace the patchwork of wallets, ramps, compliance, routing, and settlement vendors with one open stack at better economics * **Fintechs and neobanks**: add stablecoin payment rails with custodial or non-custodial wallets, compliant on-ramps, and instant settlement in one integration * **Enterprise payments teams**: reduce cross-border costs, unlock 24/7 settlement, and expand globally without assembling per-corridor bank relationships * **Banks and financial institutions**: layer stablecoin settlement onto existing infrastructure without rebuilding core systems * **Enterprises and marketplaces**: automate global payouts with programmable, auditable money flows and pay out counterparties anywhere instantly ## Next steps See how financial institutions are using the Open Money Stack. Pick a use case and follow a guided path into the docs. # ERC-8004 on Polygon Source: https://docs.polygon.technology/payment-services/agentic-payments/agent-integration/erc8004 Reference and explanation for ERC-8004: onchain Identity, Reputation, and Validation registries for autonomous agents. **ERC-8004** defines a lightweight, onchain trust layer for autonomous agents using three registries: **Identity**, **Reputation**, and **Validation**. The standard is designed to work alongside existing agent protocols (A2A, MCP). Payments are out of scope for ERC-8004; however, payment proofs (e.g., from **x402**) can be referenced in reputation data. This page covers what ERC-8004 standardizes, how it relates to A2A, MCP, and x402, and where to find the Polygon mainnet and Amoy deployments. ## Why ERC-8004? Modern agent stacks focus on communication and capability exposure, not on open discovery and trust across organizational boundaries. Google's **A2A** provides agent authentication, capability advertisement via Agent Cards, and orchestration, but does not standardize reputation or validation. Anthropic's **MCP** connects LLM apps to external tools and data, and similarly leaves trust and discovery to individual applications. ERC-8004 adds the missing piece: standard onchain registries that any chain can host as singletons. It links agents to MCP/A2A endpoints through an onchain identity, with optional trust signals. *** ## What ERC-8004 standardizes (at a glance) * **Identity Registry** - an **ERC-721 + URIStorage** registry that mints an *Agent ID* (`agentId`) and points its `tokenURI` to a JSON registration file (e.g., on IPFS/HTTPS) listing A2A/MCP endpoints, DIDs, ENS, wallets, etc. Ownership of the NFT = ownership of the agent entry. * **Reputation Registry** - an interface for clients to submit **feedback** (score `0-100`, optional tags, and an optional off-chain file/URI + hash). Off-chain files may include **payment proofs** to correlate economics with feedback. * **Validation Registry** - a request/response log for **independent validators** (e.g., stake-based re-execution, zkML verifiers, TEEs) to post attestations about an agent's work. Results can be queried onchain. Payments are **orthogonal** to ERC-8004; the spec shows how **x402** proofs can be referenced in reputation data but does not dictate any settlement flow. ## Polygon Deployments ERC-8004 registries are deployed on **Polygon mainnet** and **Polygon Amoy**. ### Polygon Mainnet * **IdentityRegistry:** [0x8004A169FB4a3325136EB29fA0ceB6D2e539a432](https://polygonscan.com/address/0x8004A169FB4a3325136EB29fA0ceB6D2e539a432) * **ReputationRegistry:** [0x8004BAa17C55a88189AE136b182e5fdA19dE9b63](https://polygonscan.com/address/0x8004BAa17C55a88189AE136b182e5fdA19dE9b63) ### Amoy * **IdentityRegistry:** [0x8004ad19E14B9e0654f73353e8a0B600D46C2898](https://amoy.polygonscan.com/address/0x8004ad19E14B9e0654f73353e8a0B600D46C2898) * **ReputationRegistry:** [0x8004B12F4C2B42d00c46479e859C92e39044C930](https://amoy.polygonscan.com/address/0x8004B12F4C2B42d00c46479e859C92e39044C930) * **ValidationRegistry:** [0x8004C11C213ff7BaD36489bcBDF947ba5eee289B](https://amoy.polygonscan.com/address/0x8004C11C213ff7BaD36489bcBDF947ba5eee289B) *** ## Read-Only Examples The snippets below use `viem` to query the Amoy contracts. They are deterministic and read-only, and work before any agents are minted. Note that `tokenURI` and `ownerOf` only resolve for existing `agentId` values. ```ts theme={null} import { createPublicClient, http } from "viem"; import { polygonAmoy } from "viem/chains"; import "dotenv/config"; const IDENTITY = "0x8004ad19E14B9e0654f73353e8a0B600D46C2898" as const; const REPUTATION = "0x8004B12F4C2B42d00c46479e859C92e39044C930" as const; const VALIDATION = "0x8004C11C213ff7BaD36489bcBDF947ba5eee289B" as const; const erc721View = [ { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ type:"string" }] }, { type: "function", name: "symbol", stateMutability: "view", inputs: [], outputs: [{ type:"string" }] }, { type: "function", name: "tokenURI", stateMutability: "view", inputs: [{ type:"uint256" }], outputs: [{ type:"string" }] }, ]; const validationView = [ { type:"function", name:"getValidationStatus", stateMutability:"view", inputs:[{type:"bytes32"}], outputs:[{type:"address"},{type:"uint256"},{type:"uint8"},{type:"bytes32"},{type:"uint256"}] }, ]; async function main() { const pub = createPublicClient({ chain: polygonAmoy, transport: http() }); // Identity registry metadata const name = await pub.readContract({ address: IDENTITY, abi: erc721View, functionName: "name" }); const symbol = await pub.readContract({ address: IDENTITY, abi: erc721View, functionName: "symbol" }); console.log({ name, symbol }); // e.g., "Agent Identity", "AGNT" // Sample agentId=1 (will throw if not yet minted) // const uri = await pub.readContract({ address: IDENTITY, abi: erc721View, functionName: "tokenURI", args:[1n] }); // console.log({ uri }); // Check a validation status by requestHash (bytes32) // const reqHash = "0x" + "00".repeat(32) as `0x${string}`; // const status = await pub.readContract({ address: VALIDATION, abi: validationView, functionName: "getValidationStatus", args:[reqHash] }); // console.log({ status }); } main(); ``` ### Where these come from in the spec * Identity uses ERC-721 + URIStorage (hence tokenURI and ownership semantics). * Validation exposes getValidationStatus/getSummary to query results. You can use more [modern interfaces and standards backwards compatible with ERC-721](https://eips.ethereum.org/EIPS/eip-6220) to access the same values if you wish. ### How ERC-8004 Relates to A2A, MCP, and x402 A2A is the coordination layer (authentication, agent cards, lifecycle). ERC-8004 adds discovery and trust so agents from different organizations can find and assess each other. MCP is the tool/data layer. The ERC-8004 Identity record can link to MCP endpoints, allowing agent wallets or marketplaces to enumerate capabilities consistently. x402 is the payment layer. ERC-8004 is payment-agnostic. You can optionally embed payment proofs (such as x402 transaction hashes) in off-chain feedback files referenced by the Reputation registry. ## Design Goals and Current Status ERC-8004 is a Standards Track ERC proposal created August 13, 2025, currently under public review. The standard introduces pluggable trust models (reputation, crypto-economic validation, zk/TEE attestations) with security proportional to the value at risk. The registries are designed to be per-chain singletons for straightforward discovery. Active discussion topics include: handling onchain vs. off-chain data, encouraging multiple independent reputation providers, and keeping payments decoupled while allowing payment proofs to be referenced. Development discussion takes place in the [ERC-8004 Builders](https://t.me/ERC8004) Telegram group and the [Ethereum Magicians forum thread](https://ethereum-magicians.org/t/erc-8004-trustless-agents/25098). # Agentic Payments Introduction Source: https://docs.polygon.technology/payment-services/agentic-payments/agent-integration/intro Conceptual overview of agentic payments and how autonomous agents transact on Polygon. # Agentic Payments Introduction **Agentic Payments** are payments initiated and completed by autonomous software entities (agents) without direct human action at every step. Instead of requiring a person to click "confirm transaction", an agent can negotiate prices, sign intents, and pay onchain in the background, using predefined policies or earned balances. This shifts onchain activity from user-driven to intent-driven. An agent doesn't just send tokens: it executes a purpose, such as subscribing to data, paying per API call, or settling micro-invoices in real time. By encoding payment logic inside agents and standardizing protocols like [x402](https://x402.dev), Polygon allows any intelligent system to become an autonomous economic participant. *** ## What is an Agent? An **agent** is an autonomous program that can perceive, decide, and act on behalf of a user or a system. Agents combine reasoning models (LLMs, decision trees) with access to data, APIs, and onchain actions. They can interpret natural language commands, interact with contracts, and coordinate with other agents without exposing private keys or depending on centralized custody. On Polygon, agents work with a suite of tooling including **AgentKit**, **Model Context Protocol (MCP)**, and **Unified APIs** to perform secure blockchain operations. This ecosystem makes it possible for an AI assistant, a trading bot, or a DAO delegate to act as an onchain entity: context-aware, policy-bounded, and continuously learning. *** ## How Agentic Payments Work Instead of traditional wallet interactions, payments are executed via **intents** or **facilitated flows** such as x402. An agent can detect that an API call costs \$0.002 in USDC, confirm the requirement, and complete the payment automatically, all in milliseconds. Because they are keyless and infrastructure-agnostic, agentic payments work across environments: from local LLMs to decentralized marketplaces. This makes microtransactions, dynamic subscriptions, and per-use pricing viable for both human-facing apps and AI agents. *** ## Standards and Protocols Polygon supports two complementary standards for agentic payments: * **[x402](/payment-services/agentic-payments/x402/intro/)**: An HTTP-based protocol that uses the 402 Payment Required status code to gate API access behind onchain payments. Clients pay per request; no subscription or API key required. * **[ERC-8004](/payment-services/agentic-payments/agent-integration/erc8004/)**: An onchain trust layer for autonomous agents, providing Identity, Reputation, and Validation registries so agents from different organizations can discover and assess each other. # Agentic Services Source: https://docs.polygon.technology/payment-services/agentic-payments/agentic-services A directory of x402-enabled APIs that can be discovered and called by AI agents using pay-per-call USDC payments on Polygon. Most APIs require an account, an API key, and a billing relationship before a single request can be made. Agentic Services removes that setup: each call is paid in USDC at the moment it's made, with no registration and no subscription. The marketplace lists x402-enabled APIs across search, inference, scraping, automation, and communication categories. Buyers pay per request; sellers publish by wrapping an existing endpoint and setting a price. Visit the marketplace: [agentic-services.polygon.technology](https://agentic-services.polygon.technology) ## Overview Agentic Services is built on the x402 payment protocol. Each listed API is wrapped in an x402 layer that issues a 402 challenge, collects a USDC payment signature, settles onchain through a Polygon facilitator, and forwards the request to the upstream API. The catalog is also available as a machine-readable Markdown file (`/SKILL.md`) and a JSON endpoint (`/api/discover/routes`), so AI agents can discover routes and pay autonomously without human configuration. ## Features Browse APIs by category with pricing, network, and example requests on each listing. Settle each call in USDC on Polygon. No subscriptions, no prepaid credits. Sign in with email and pay from an embedded wallet. No extensions, no gas to manage. Send live requests from the browser, inspect responses, and view settlement details. Fetch the catalog as Markdown (`/SKILL.md`) or JSON (`/api/discover/routes`). Wrap an existing REST endpoint, set a price, and list it as an x402 service. ## Payment flow When a buyer calls a service in the catalog, the marketplace's x402 wrapper challenges, verifies, and settles the payment before forwarding to the upstream API. The flow is the standard x402 v2 exchange; the marketplace just hosts the resource server and the catalog.
x402 payment flow
1 Client Service GET /endpoint
2 Service Client 402 Payment Required with price and accepts block (USDC, eip155:137, payTo)
3 Client Service Retries with EIP-3009 PAYMENT-SIGNATURE header
4 Service Facilitator POST /verify (signature + requirements)
5 Facilitator Polygon Settles USDC onchain
6 Service Forwards verified request to the upstream API, returns response
7 Service Client 200 OK with body and PAYMENT-RESPONSE receipt header
Using the `@x402/fetch` SDK, this entire round-trip is handled automatically: the SDK reads the 402, signs the EIP-3009 authorization with the client wallet, retries the request, and surfaces the response and receipt. For protocol details, see [x402 How It Works](/payment-services/agentic-payments/x402/guides/how-it-works). ## Available services The marketplace includes services for: * Web search * News retrieval * Web scraping * Browser automation * LLM inference * AgentMail services The live catalog is available at [agentic-services.polygon.technology](https://agentic-services.polygon.technology). ## Get started Browse available services and test API calls directly from the browser. Learn how to integrate x402 payments into an agent or application. Learn how to expose an API as an x402-enabled service. Understand the payment challenge, authorization, and settlement process. # ERC-8004 agent identity Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/agent-identity Register an onchain agent identity and manage reputation through the ERC-8004 registries on Polygon mainnet. Six commands cover the ERC-8004 lifecycle: register a new agent identity, query its registered payment wallet, fetch metadata by key, read or write reputation. ERC-8004 sits separately from x402, payments are independent, but a registered identity gives counterparties a place to look up an agent's wallet, attestations, and feedback before transacting with it. All `agent` commands operate on **Polygon mainnet only**, regardless of the wallet's default chain. *** ## How it works
Identity lifecycle
1 Operator CLI polygon-agent agent register --name ...
2 IdentityRegistry Mints an agentId, emits Registered event
3 Counterparty CLI polygon-agent agent reputation --agent-id ...
4 ReputationRegistry CLI Returns score and tag breakdown
5 Counterparty ReputationRegistry polygon-agent agent feedback --value 95 --broadcast
The smart wallet pays gas for `register` and `feedback`. Read commands (`reputation`, `reviews`, `wallet`, `metadata`) do not need broadcast. *** ## Contracts | Registry | Address | | ------------------ | -------------------------------------------- | | IdentityRegistry | `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` | | ReputationRegistry | `0x8004BAa17C55a88189AE136b182e5fdA19dE9b63` | For the full standard, see [ERC-8004 on Polygon](/payment-services/agentic-payments/agent-integration/erc8004). *** ## Commands ### agent register ```bash theme={null} polygon-agent agent register \ --name "my-agent" \ --agent-uri https://agent.example.com/card.json \ --metadata "category=data,version=1.0" ``` Mints an ERC-721 identity in the IdentityRegistry. After broadcast, retrieve the assigned `agentId` from the Registered event in the transaction's Logs tab on Polygonscan. | Flag | Default | Purpose | | -------------------------- | ------- | -------------------------------- | | `--wallet` | `main` | Wallet that pays gas | | `--name` | none | Agent name | | `--agent-uri` (or `--uri`) | none | URI pointing to an Agent Card | | `--metadata` | none | Key=value pairs, comma-separated | | `--broadcast` | `false` | Execute the transaction | ### agent wallet ```bash theme={null} polygon-agent agent wallet --agent-id 42 ``` Returns the payment wallet registered for the agent. ### agent metadata ```bash theme={null} polygon-agent agent metadata --agent-id 42 --key category ``` Reads one metadata value by key. ### agent reputation ```bash theme={null} polygon-agent agent reputation --agent-id 42 polygon-agent agent reputation --agent-id 42 --tag1 reliability ``` Aggregated reputation score for the agent. Optional `--tag1` and `--tag2` filter by category. ### agent reviews ```bash theme={null} polygon-agent agent reviews --agent-id 42 polygon-agent agent reviews --agent-id 42 --tag1 quality --include-revoked ``` Enumerates all feedback entries. `--include-revoked` shows entries that have been revoked by their authors. ### agent feedback ```bash theme={null} polygon-agent agent feedback \ --agent-id 42 \ --value 95 \ --tag1 reliability \ --endpoint "https://api.example.com/v1" \ --broadcast ``` Submits a feedback score. Useful for counterparties evaluating an agent after a completed interaction. Submitter is the calling smart wallet. | Flag | Default | Purpose | | ------------------- | -------- | ----------------------- | | `--wallet` | `main` | Wallet that pays gas | | `--agent-id` | required | Target agent | | `--value` | required | Feedback score | | `--tag1` / `--tag2` | none | Category tags | | `--endpoint` | none | Endpoint reviewed | | `--feedback-uri` | none | URI to off-chain detail | | `--broadcast` | `false` | Execute the transaction | *** ## Worked example ```bash theme={null} # Register a new agent polygon-agent agent register \ --name "data-aggregator" \ --agent-uri https://example.com/agent.json \ --metadata "category=data,sla=99.9" # ⚡ Dry run polygon-agent agent register \ --name "data-aggregator" \ --agent-uri https://example.com/agent.json \ --broadcast # Registered: tx 0xabc... → https://polygonscan.com/tx/0xabc... # Note: open the Logs tab on Polygonscan, find the Registered event, # and copy agentId (used for all subsequent agent commands). # Query reputation before transacting polygon-agent agent reputation --agent-id 142 # Score: 87 (32 reviews) ``` *** ## Implementation Standard, contracts, and integration concepts. Topology and safety model. Conceptual overview of agentic flows on Polygon. Wallet needs gas to register and submit feedback. # Balances and funding Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/balances-and-funding Check balances across one or many chains and fund the smart wallet through the Trails widget. `balances` reports holdings for one chain or many. `fund` returns a Trails widget URL that lets a finance operator fund the smart wallet in fiat (card, ACH, wire) or in crypto from another wallet. The CLI never constructs funding URLs manually, `fund` always uses the official Trails surface so the recipient address is correct. Keep at least **0.1 USDC or 0.1 POL** in the smart wallet at all times for gas. The CLI auto-selects USDC over native POL when both are available. *** ## How it works
Funding flow
1 Operator CLI polygon-agent fund --wallet treasury
2 CLI Trails Returns funding URL (wallet.polygon.technology)
3 Finance Trails Pays via card, ACH, wire, or crypto
4 Trails Smart wallet USDC lands at the wallet address
5 Operator CLI polygon-agent balances → confirms credit
The Trails widget is the same surface end customers use elsewhere in the Polygon stack. It handles KYC if required and supports off-ramp in the reverse direction. *** ## Commands ### balances ```bash theme={null} polygon-agent balances --wallet main --chain polygon polygon-agent balances --chains polygon,base,arbitrum ``` | Flag | Default | Purpose | | ---------- | ------- | ---------------------------------------------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Single chain (defaults to wallet's chain) | | `--chains` | none | Comma-separated chain list, up to 20. Overrides `--chain`. | When `--chains` lists two or more chains, output is multi-chain JSON keyed by chain. Single-chain output is a markdown table. ### fund ```bash theme={null} polygon-agent fund --wallet main --chain polygon ``` | Flag | Default | Purpose | | ---------- | ------- | ------------------- | | `--wallet` | `main` | Wallet name to fund | | `--chain` | none | Chain to fund on | Returns the Trails widget URL as a clickable link. Hand the URL to a finance approver who completes the funding step in the browser. The recipient address is always the wallet's onchain address; the CLI never accepts a manual destination here. Always run `polygon-agent fund` to get the URL. Constructing one manually risks sending funds to the wrong address. *** ## Worked example ```bash theme={null} # Get the funding URL polygon-agent fund --wallet treasury --chain polygon # → https://wallet.polygon.technology/fund?wallet=0xABC...&chain=polygon # Confirm receipt after the finance team completes the on-ramp polygon-agent balances --wallet treasury --chain polygon # → 100.00 USDC, 0.5 POL # Check across multiple chains polygon-agent balances --wallet treasury --chains polygon,base,arbitrum ``` *** ## Implementation Create the wallet before funding it. The underlying funding surface. Move funds out of the wallet once they arrive. # Polygon Agent CLI Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/index Command-line toolkit for agents on Polygon: smart wallets, Trails for funding and swaps and yield, x402 micropayments, ERC-8004 identity, and Polymarket. The Polygon Agent CLI is one binary that gives an agent everything it needs to transact on Polygon: a wallet to spend from, fiat funding through Trails, cross-chain swaps and yield through Trails, x402 micropayments, ERC-8004 identity, and Polymarket trading. Every write command is dry-run by default. The same surface works for a developer testing flows from a laptop and an agent runtime running headless in production. Configuration lives at `~/.polygon-agent/`. Keys are encrypted at rest with AES-256-GCM. Sessions last six months. **Who this is for:** * Agent developers building on Polygon who want one toolkit instead of stitching together five SDKs * Enterprise teams running agent fleets with bounded spend and audit requirements * Anyone testing x402, Trails, or ERC-8004 flows interactively before going to production *** ## How it works
Wallet topology
1 Operator CLI polygon-agent setup → creates EOA (auth only)
2 Operator OMS Wallet wallet create → smart wallet (the spending wallet)
3 Smart wallet Trails Funded in fiat or crypto via `polygon-agent fund`
4 Smart wallet Spends: transfers, swaps, yield, x402, identity, prediction markets
5 CLI Dry-runs every write; broadcasts only with `--broadcast`
The CLI keeps two wallets per project: an EOA that authenticates with OMS (never funded), and a smart wallet that holds the operating balance and signs every transaction. Spending limits, contract allowlists, and DeFi permissions are scoped to the smart wallet at creation. The operator never funds or exposes the EOA. *** ## Install ```bash theme={null} npm install -g @polygonlabs/agent-cli ``` The binary is `polygon-agent`. Re-running the install command updates to the latest version. ```bash theme={null} polygon-agent setup --name "my-project" polygon-agent wallet create ``` `setup` provisions the EOA and OMS access key, written to `~/.polygon-agent/builder.json`. `wallet create` opens a browser approval flow and pairs a smart wallet using a six-digit code. *** ## Supported chains | Value | Network | | ------------------- | -------------------- | | `polygon` (default) | Polygon mainnet | | `amoy` | Polygon Amoy testnet | | `mainnet` | Ethereum mainnet | | `arbitrum` | Arbitrum One | | `optimism` | Optimism | | `base` | Base | ERC-8004 agent commands (`agent register`, `agent feedback`, etc.) operate on **Polygon mainnet only**, regardless of `--chain`. *** ## Safety model Every write command (`send`, `send-token`, `send-native`, `swap`, `deposit`, `withdraw`, `agent register`, `agent feedback`, and the Polymarket trading commands) runs in dry-run unless you pass `--broadcast`. The one exception is `x402-pay`, which auto-pays the 402 and submits the payment as part of the request, there is no `--broadcast` flag. Keep at least **0.1 USDC or 0.1 POL** in the smart wallet at all times for gas. The CLI auto-selects USDC over POL when both are available. *** ## Sections `setup`, `wallet create | import | list | address | remove`, spending limits, browser approval. `balances` (single + multi-chain), `fund` (Trails widget URL), gas reserve rules. `send`, `send-native`, `send-token` with dry-run + broadcast. `swap` (Trails), `deposit` and `withdraw` (Aave v3, Morpho, ERC-4626). `x402-pay` auto-pays 402 responses; prerequisite flow. `agent register`, `reputation`, `feedback`, `reviews`. Mainnet only. Prediction-market trading: `markets`, `clob-buy`, `sell`, `positions`. Source, releases, and contribution guide. # Polymarket trading Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/polymarket Trade Polymarket prediction markets from the CLI: list markets, buy and sell YES/NO tokens via CLOB, manage positions. The CLI ships with first-class Polymarket support: discover markets by volume or keyword, fund a Polymarket proxy wallet from the smart wallet, place CLOB orders for YES/NO outcomes, and list open positions and orders. Polymarket uses a separate EOA key for CLOB signing, imported once via `set-key`. The proxy wallet needs a one-time `approve` call before the first buy. For an enterprise context, this is the surface for hedging operational risk (regulatory events, supply-chain disruptions, demand shocks) at small dollar values within a clear cap. *** ## How it works
Trade flow
1 Operator CLI polymarket set-key \ (one-time)
2 Operator CLI polymarket approve --broadcast (one-time)
3 Operator CLI polymarket markets --search ...
4 Smart wallet Proxy wallet Funds the proxy wallet, then buys YES/NO tokens via CLOB
5 Operator CLI polymarket positions / orders / sell / cancel
The proxy wallet is Polymarket's per-EOA execution wallet. `clob-buy` funds it from the smart wallet first, then submits the buy order. `sell` and order management work directly against the proxy. *** ## One-time setup ```bash theme={null} polygon-agent polymarket set-key 0xYOUR_EOA_PRIVATE_KEY polygon-agent polymarket proxy-wallet # 0xProxyAddress (the destination for funding) polygon-agent polymarket approve --broadcast ``` `set-key` stores the EOA private key encrypted in `~/.polygon-agent/`. `approve` is a one-time onchain transaction that authorizes the proxy wallet to spend USDC on Polymarket's CLOB; without it, the first buy fails. *** ## Commands ### markets ```bash theme={null} polygon-agent polymarket markets polygon-agent polymarket markets --search "election" --limit 50 ``` | Flag | Default | Purpose | | ---------- | ------- | ----------------------- | | `--search` | none | Filter by question text | | `--limit` | `20` | Number of results | | `--offset` | `0` | Pagination offset | Lists active markets sorted by volume. ### market ```bash theme={null} polygon-agent polymarket market ``` Returns full detail for one market identified by `conditionId`. ### set-key ```bash theme={null} polygon-agent polymarket set-key ``` Imports an EOA private key for Polymarket signing. Stored AES-256-GCM encrypted. Required before any trading commands. ### proxy-wallet ```bash theme={null} polygon-agent polymarket proxy-wallet ``` Returns the Polymarket proxy wallet address derived from the active EOA. Use this address as the funding destination. ### approve ```bash theme={null} polygon-agent polymarket approve --broadcast polygon-agent polymarket approve --neg-risk --broadcast ``` | Flag | Default | Purpose | | ------------- | ------- | ----------------------------- | | `--neg-risk` | `false` | Approve negative-risk markets | | `--broadcast` | `false` | Execute the transaction | One-time approval before the first `clob-buy`. Pass `--neg-risk` to also approve negative-risk markets. ### clob-buy ```bash theme={null} polygon-agent polymarket clob-buy YES 25 --broadcast polygon-agent polymarket clob-buy NO 100 --price 0.42 --broadcast ``` Buy YES or NO tokens via CLOB. Positional args are conditionId, outcome (YES or NO), and USDC to spend. | Flag | Default | Purpose | | ------------- | ------- | ------------------------------------------------ | | `--wallet` | `main` | Smart wallet to fund the proxy from | | `--price` | none | Limit price 0–1 (GTC); omit for market order | | `--fak` | `false` | Fill-and-kill instead of fill-or-kill | | `--skip-fund` | `false` | Skip wallet→proxy funding (proxy already funded) | | `--broadcast` | `false` | Execute the transaction | ### sell ```bash theme={null} polygon-agent polymarket sell YES 50 --broadcast polygon-agent polymarket sell NO 50 --price 0.55 --broadcast ``` Sell YES or NO tokens. Positional args are conditionId, outcome, and number of shares. | Flag | Default | Purpose | | ------------- | ------- | -------------------------------------------- | | `--price` | none | Limit price 0–1 (GTC); omit for market order | | `--fak` | `false` | Fill-and-kill instead of fill-or-kill | | `--broadcast` | `false` | Execute the transaction | ### positions, orders, cancel ```bash theme={null} polygon-agent polymarket positions polygon-agent polymarket orders polygon-agent polymarket cancel ``` `positions` lists open positions for the proxy wallet. `orders` lists open CLOB orders for the active EOA. `cancel` cancels a specific order by ID. *** ## Worked example ```bash theme={null} # One-time per machine polygon-agent polymarket set-key 0xEOA_KEY polygon-agent polymarket approve --broadcast # Find a market polygon-agent polymarket markets --search "fed cuts" --limit 5 # Buy YES with $25 USDC at market price polygon-agent polymarket clob-buy 0xCONDITION_ID YES 25 # ⚡ Dry run, funds proxy with 25 USDC, places market buy polygon-agent polymarket clob-buy 0xCONDITION_ID YES 25 --broadcast # Funded proxy + placed buy # Tx: 0xabc... → https://polygonscan.com/tx/0xabc... # Track polygon-agent polymarket positions polygon-agent polymarket orders ``` *** ## Implementation Topology, safety model, supported chains. Smart wallet is the funding source for the proxy. Move USDC into the smart wallet before trading. Confirm USDC available before placing orders. # Setup and wallets Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/setup-and-wallets Initialize a project, create a smart wallet with spending limits, and manage wallet lifecycle. Two commands set up a working CLI: `polygon-agent setup` creates an EOA and authenticates with OMS, then `polygon-agent wallet create` provisions the smart wallet that holds the operating balance. The smart wallet is where spending limits live: per-token caps, contract allowlists, and DeFi permissions are scoped at creation. The EOA never holds funds. Wallet sessions last six months. After expiry, re-run `wallet create` to refresh the session. *** ## How it works
Wallet creation flow
1 Operator CLI polygon-agent wallet create --usdc-limit 50
2 CLI Browser Approval URL + 6-digit code shown in terminal
3 Operator Browser Opens URL, reviews limits, enters 6-digit code
4 OMS Wallet CLI Returns session via encrypted relay (X25519)
5 CLI Writes encrypted session to `~/.polygon-agent/wallets/<name>.json`
The browser approval step is one-time per session. After approval, the CLI runs autonomously for six months against the configured spending limits. *** ## Commands ### setup ```bash theme={null} polygon-agent setup --name "my-project" ``` | Flag | Default | Purpose | | --------- | ------- | -------------------------------------- | | `--name` | none | Project name written to `builder.json` | | `--force` | `false` | Recreate config even if already set up | Creates an EOA and access key. Stored encrypted at `~/.polygon-agent/builder.json`. Run this once per machine per project. ### wallet create ```bash theme={null} polygon-agent wallet create \ --name main \ --chain polygon \ --usdc-limit 50 \ --native-limit 0.5 ``` | Flag | Default | Purpose | | ----------------------------- | --------- | --------------------------------------------------------------- | | `--name` | `main` | Wallet name (multiple wallets supported) | | `--chain` | `polygon` | Chain to deploy on | | `--print-url` | `false` | Print approval URL and exit (headless) | | `--timeout` | `300` | Seconds to wait for browser approval | | `--native-limit` | none | POL spending cap | | `--usdc-limit` | none | USDC spending cap | | `--usdt-limit` | none | USDT spending cap | | `--token-limit` | none | Other token cap, repeatable: `--token-limit WETH:0.1` | | `--contract` | none | Whitelist a contract address, repeatable | | `--defi` | `false` | Whitelist DeFi contracts (swaps, yield) in addition to defaults | | `--usdc-to` / `--usdc-amount` | none | One-off USDC transfer at creation | | `--access-key` | none | Override the stored OMS project access key | For yield workflows, pass `--defi` at creation so the smart wallet can interact with Aave v3, Morpho, and ERC-4626 vaults without re-approval. To allow specific contracts only, list them with repeated `--contract` flags. ### wallet import ```bash theme={null} polygon-agent wallet import --code 123456 --rid polygon-agent wallet import --ciphertext '@./session.ciphertext' ``` Imports a session created on another machine, either via the 6-digit code + relay ID flow or via an exported ciphertext blob. Useful for moving a wallet between an operator machine and a server. ### wallet list, address, remove ```bash theme={null} polygon-agent wallet list polygon-agent wallet address --name main polygon-agent wallet remove --name old-wallet ``` `list` enumerates all wallets. `address` prints the smart-wallet address for the named wallet. `remove` deletes the local session; the onchain wallet is unaffected. *** ## Headless approval For server environments without a browser, use `--print-url`: ```bash theme={null} polygon-agent wallet create --print-url --usdc-limit 100 ``` The CLI prints the approval URL and exits immediately. Open the URL on any machine, enter the 6-digit code shown in terminal output, then run `polygon-agent wallet import --code --rid ` on the server to pick up the session. *** ## Worked example ```bash theme={null} # One-time per machine polygon-agent setup --name "treasury" # Smart wallet with caps polygon-agent wallet create \ --name treasury \ --chain polygon \ --usdc-limit 1000 \ --usdt-limit 1000 \ --defi # Confirm polygon-agent wallet list polygon-agent wallet address --name treasury ``` After approval, the wallet can spend up to 1,000 USDC and 1,000 USDT plus interact with any DeFi contract whitelisted by `--defi`. *** ## Implementation Topology, install, supported chains, safety model. Check balances and fund the smart wallet. smart contract wallet primitives. # Swaps and yield Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/swaps-and-yield DEX swaps, cross-chain bridging, and Trails earn pools (Aave v3, Morpho, ERC-4626), all under the smart wallet. Three commands cover treasury movement beyond simple transfers. `swap` routes through the Trails DEX API, including cross-chain swaps via `--to-chain`. `deposit` parks idle balances in Trails earn pools (Aave v3, Morpho, ERC-4626 vaults). `withdraw` exits those positions, with auto-discovery if you know only the asset and protocol. The CLI picks the highest-TVL pool by default; pass `--protocol` to constrain. All three are dry-run by default. Pass `--broadcast` to execute. *** ## How it works
Treasury sweep flow
1 Operator CLI polygon-agent swap --from USDT --to USDC ...
2 CLI Trails Routes swap quote and gas across DEX aggregators
3 Smart wallet Polygon Swap executes; output token received
4 Operator CLI polygon-agent deposit --asset USDC --amount 1000
5 Trails Aave / Morpho Deposits into highest-TVL pool by default
`--defi` at `wallet create` time gives the smart wallet permission to interact with Aave v3, Morpho, and ERC-4626 vaults. Without `--defi`, deposit and withdraw fail with a session-rejection error before any funds move. *** ## Commands ### swap ```bash theme={null} polygon-agent swap --from USDC --to WETH --amount 100 polygon-agent swap --from USDC --to USDC --amount 100 --to-chain base --broadcast ``` DEX swap routed through Trails. Pass `--to-chain` to bridge during the swap. | Flag | Default | Purpose | | ------------- | -------- | --------------------------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Source chain | | `--from` | required | Source token symbol | | `--to` | required | Destination token symbol | | `--amount` | required | Amount to swap | | `--slippage` | none | Tolerance 0–0.5 (e.g. `0.005` for 0.5%) | | `--to-chain` | none | Destination chain for cross-chain swap | | `--broadcast` | `false` | Execute the transaction | ### deposit ```bash theme={null} polygon-agent deposit --asset USDC --amount 1000 polygon-agent deposit --asset USDC --amount 1000 --protocol aave --broadcast ``` Deposit into a Trails earn pool. With no `--protocol`, the CLI auto-selects the highest-TVL pool for the asset. `--protocol aave` or `--protocol morpho` constrains the choice. | Flag | Default | Purpose | | ------------- | -------- | -------------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Chain | | `--asset` | `USDC` | Asset symbol | | `--amount` | required | Amount to deposit | | `--protocol` | none | Filter: `aave` or `morpho` | | `--broadcast` | `false` | Execute the transaction | ### withdraw ```bash theme={null} # By auto-discovery polygon-agent withdraw --asset USDC --protocol aave --amount max # By explicit position address polygon-agent withdraw --position 0xPool... --amount 500 ``` Withdraw from an Aave v3 aToken position or an ERC-4626 vault. Use `--amount max` for a full exit. | Flag | Default | Purpose | | ------------- | -------- | ------------------------------------------------------ | | `--wallet` | `main` | Wallet name | | `--chain` | none | Chain | | `--position` | none | aToken or vault share address | | `--asset` | none | Asset symbol (use with `--protocol` to auto-discover) | | `--protocol` | none | `aave` or `morpho` | | `--amount` | required | Underlying amount (Aave) or `max` / partial (ERC-4626) | | `--broadcast` | `false` | Execute the transaction | *** ## Worked example ```bash theme={null} # Sweep idle USDC into Aave v3 polygon-agent deposit --asset USDC --amount 5000 --protocol aave # ⚡ Dry run, pool: 0x794a..., projected APY 4.8% polygon-agent deposit --asset USDC --amount 5000 --protocol aave --broadcast # Deposited 5000.00 USDC to Aave v3 # Tx: 0xabc... → https://polygonscan.com/tx/0xabc... # Pull funds back when an initiative kicks off polygon-agent withdraw --asset USDC --protocol aave --amount max --broadcast # Withdrew 5,012.34 USDC from Aave v3 (principal + yield) # Tx: 0xdef... ``` *** ## Implementation Pass `--defi` at wallet creation to enable swaps and yield. Plain token transfers; no DEX routing. The underlying DEX and earn-pool router. # Transfers Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/transfers Send native tokens or ERC-20s from the smart wallet. Dry-run by default; broadcast with --broadcast. Three commands cover transfers: `send` auto-detects between native and ERC-20 based on `--symbol`, `send-native` is the explicit native path, and `send-token` is for ERC-20s by symbol or contract address. Every transfer is dry-run by default, the CLI prints the planned transaction and exits without broadcasting. Add `--broadcast` only when the plan is correct. Use dry-run to review the recipient, amount, fee, and chain before any funds move. For batch payouts, run dry-runs across the entire batch first; broadcast the batch only after every dry-run passes. *** ## How it works
Transfer flow
1 Operator CLI polygon-agent send-token ... (no --broadcast)
2 CLI Prints dry-run summary: amount, symbol, recipient, fee
3 Operator CLI Re-runs with --broadcast
4 Smart wallet Polygon Submits transaction
5 CLI Operator Prints tx hash + explorer link
The spending-limit caps set at `wallet create` time bound every transfer. A transfer that exceeds its cap fails at dry-run; no funds move and the wallet does not need to re-approve. *** ## Commands ### send ```bash theme={null} polygon-agent send --to 0xRecipient --amount 10 --symbol USDC ``` `send` is the convenience command: pass `--symbol` to send ERC-20s, omit it to send the native token. | Flag | Default | Purpose | | ------------- | -------- | --------------------------------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Chain (defaults to wallet's chain) | | `--to` | required | Recipient address | | `--amount` | required | Amount to send | | `--symbol` | none | Token symbol for ERC-20 (omit for native) | | `--token` | none | Token contract address (overrides `--symbol`) | | `--decimals` | none | Token decimals (use with `--token`) | | `--broadcast` | `false` | Execute the transaction | ### send-native ```bash theme={null} polygon-agent send-native --to 0xRecipient --amount 0.5 ``` Explicit native-token transfer. The `--direct` flag bypasses the Sequence ValueForwarder for cases where the smart wallet needs to send native directly. | Flag | Default | Purpose | | ------------- | -------- | ----------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Chain | | `--to` | required | Recipient address | | `--amount` | required | Amount to send | | `--direct` | `false` | Bypass ValueForwarder | | `--broadcast` | `false` | Execute the transaction | ### send-token ```bash theme={null} polygon-agent send-token --symbol USDC --to 0xRecipient --amount 100 ``` Explicit ERC-20 transfer. Use `--symbol` for known tokens or `--token --decimals` for arbitrary contracts. | Flag | Default | Purpose | | ------------- | -------- | ---------------------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Chain | | `--symbol` | none | Token symbol (e.g. `USDC`, `WETH`) | | `--token` | none | Token contract address | | `--decimals` | none | Token decimals (with `--token`) | | `--to` | required | Recipient address | | `--amount` | required | Amount to send | | `--broadcast` | `false` | Execute the transaction | *** ## Worked example ```bash theme={null} # Step 1: Dry-run to confirm the plan polygon-agent send-token --symbol USDC --to 0xVendor --amount 25 # ⚡ Dry run # Send 25.00 USDC to 0xVendor on polygon # Fee: 0.0001 USDC # Wallet balance after: 75.00 USDC # Step 2: Broadcast once verified polygon-agent send-token --symbol USDC --to 0xVendor --amount 25 --broadcast # Sent 25.00 USDC to 0xVendor on polygon # Tx: 0xabc... → https://polygonscan.com/tx/0xabc... ``` *** ## Implementation Topology and safety model. Confirm balance before and after. Cross-chain transfers, swaps, and yield positions. Spending limits set the bounds on transfers. # Pay an x402 endpoint Source: https://docs.polygon.technology/payment-services/agentic-payments/cli/x402-pay Call an x402-protected resource from the CLI. The 402 is auto-paid and the request is retried as part of the same call. `x402-pay` calls an x402-protected URL on behalf of the smart wallet. When the server returns `402 Payment Required`, the CLI signs the payment using the wallet's session, retries the request, and returns the response body. There is no `--broadcast` flag, payment and the request are one operation, so settlement happens as part of the call. Use this command to test that a seller's x402 integration is live, to consume paid APIs from scripts, or to demonstrate agent-buyer flows interactively. *** ## How it works
x402-pay flow
1 Operator CLI polygon-agent x402-pay --url ...
2 CLI API Initial request (no payment header)
3 API CLI 402 Payment Required with accepts block
4 CLI Signs payment with wallet session, retries with PAYMENT-SIGNATURE
5 API Facilitator Verifies and settles USDC on Polygon
6 API CLI 200 OK with body + PAYMENT-RESPONSE receipt
There is no separate verify-then-execute step. The CLI receives the 402, signs against the wallet's session, retries, and prints the response body plus the settlement receipt in one call. *** ## Prerequisites Before calling `x402-pay`, confirm three things: 1. **Wallet exists.** `polygon-agent wallet list`. If missing, run `polygon-agent wallet create`. 2. **USDC balance.** `polygon-agent balances --chain polygon`. The wallet needs enough USDC to cover the price plus gas (keep \~0.1 USDC reserve). 3. **The endpoint actually requires x402.** Curl the URL once; if it returns 200 without a payment header, x402 isn't gating it. For known endpoints, the [x402 directory](/payment-services/agentic-payments/x402/directory) lists active sellers. *** ## Command ```bash theme={null} polygon-agent x402-pay --url https://api.example.com/v1/lookups/company polygon-agent x402-pay \ --url https://api.example.com/v1/inference \ --method POST \ --body '{"prompt":"hi"}' \ --header "X-Trace-Id:abc" ``` | Flag | Default | Purpose | | ---------- | -------- | -------------------------------------------- | | `--wallet` | `main` | Wallet name | | `--chain` | none | Chain to pay on (defaults to wallet's chain) | | `--url` | required | Endpoint URL | | `--method` | `GET` | HTTP method | | `--body` | none | Request body (JSON string) | | `--header` | none | Additional header `Key:Value`, repeatable | There is **no `--broadcast` flag**. `x402-pay` always settles as part of the call; running the command transfers USDC. *** ## Worked example ```bash theme={null} polygon-agent x402-pay --url https://api.example.com/v1/weather/current # 402 Payment Required → signing payment (0.0005 USDC) # 200 OK # Response body: # { "city": "Tokyo", "temperature_c": 24.5, ... } # Settlement: 0xabc... → https://polygonscan.com/tx/0xabc... ``` For a seller-side integration that returns 402 in front of the route, see the [x402 Quickstart for Sellers](/payment-services/agentic-payments/x402/guides/quickstart-sellers). *** ## Implementation Protocol overview and facilitator list. Programmatic buyer integration without the CLI. Protocol mechanics, accepts block, receipts. Confirm USDC balance before calling. # Cross-corridor vendor payouts Source: https://docs.polygon.technology/payment-services/agentic-payments/solutions/cross-corridor-payouts Pay vendors and contractors in their preferred chain and currency from a single USDC treasury. Bridging is one command per recipient. A platform paying suppliers, contractors, or marketplace sellers across borders typically holds dozens of banking relationships and a half-dozen currency accounts. Each new corridor adds another set of operational dependencies. The Agent CLI collapses that to a single USDC treasury on Polygon plus one command per recipient. Same-chain payouts go through `send-token`; cross-chain payouts go through `swap --to-chain`. Recipients who want fiat get a Trails off-ramp URL. Each disbursement carries dry-run + broadcast, so finance reviews the entire batch before any funds move. Each broadcast produces an onchain receipt that maps one-to-one with a row in your payout schedule. **Who this is for:** * AP managers running weekly or monthly multi-corridor disbursements * Marketplaces paying international sellers in their local rails * Gig and payroll platforms with contractors across regions * Payouts networks settling to vendors on different chains *** ## How it works
Per-recipient payout
1 AP CLI Loads schedule: recipient, address, chain, amount
2 CLI Dry-runs every row: send-token (same chain) or swap --to-chain (cross-chain)
3 Finance AP Reviews total variance, signs off on the batch
4 Smart wallet Trails Broadcasts batch with `--broadcast`; routes cross-chain via Trails
5 Trails Recipient USDC lands on the destination chain (or off-ramps to fiat)
6 CLI AP Returns per-recipient transaction hash for GL mapping
Each row in the schedule maps to one CLI command. Same-chain rows use `send-token`. Cross-chain rows use `swap --from USDC --to USDC --to-chain `. Recipients that want fiat receive a Trails widget URL via `polygon-agent fund --wallet `; they complete the off-ramp themselves. *** ## Get started Same-chain payouts use `send-token`. Cross-chain payouts use `swap --to-chain` to bridge through Trails. Recipients who want fiat receive a Trails widget URL via `fund`. Every command is dry-run by default; broadcast the batch only after finance signs off. ### Install ```bash theme={null} npm install -g @polygonlabs/agent-cli polygon-agent setup --name "ap-treasury" polygon-agent wallet create --usdc-limit 100000 ``` No extra flags are needed for transfers and cross-chain swaps. Size `--usdc-limit` to cover a typical disbursement batch. ### Drive it from an LLM ```text theme={null} Read https://agentconnect.polygon.technology/SKILL.md and pay this week's vendor schedule: 500 USDC to 0xVendorA on Base, 250 USDC to 0xVendorB on Arbitrum, and 1000 USDC to 0xVendorC on Polygon. Dry-run the full batch first, then broadcast once I confirm. Report each transaction hash and the final balance on every chain. ``` ### Run the commands directly ```bash theme={null} # Same-chain payout polygon-agent send-token --to 0xVendor --symbol USDC --amount 250 polygon-agent send-token --to 0xVendor --symbol USDC --amount 250 --broadcast # Cross-chain payout (Polygon → Base) polygon-agent swap --from USDC --to USDC --amount 500 \ --chain polygon --to-chain base polygon-agent swap --from USDC --to USDC --amount 500 \ --chain polygon --to-chain base --broadcast # Confirm balances across both chains polygon-agent balances --chains polygon,base ``` Full reference: [Transfers](/payment-services/agentic-payments/cli/transfers) and [Swaps and yield](/payment-services/agentic-payments/cli/swaps-and-yield). *** ## Implementation `send-token` reference: flags, dry-run, broadcast. `swap --to-chain` for cross-chain payouts. Check balance before the batch; share `fund` URL with fiat-preferring recipients. The underlying cross-chain router. # Monetize an existing REST API Source: https://docs.polygon.technology/payment-services/agentic-payments/solutions/monetize-existing-api Open a per-call revenue channel against your existing API for agent buyers. Settle in USDC on Polygon. A SaaS or data company already serves a paying customer base through API keys and contracts. Agent buyers are a new segment that does not sign contracts, does not register, and does not fit a seat or seat-overage plan. With x402 middleware in front of the routes you want priced, an agent making a fresh request sees `402 Payment Required`, signs a payment, and gets the response back. Each settled call lands USDC in a wallet you control. The motion is additive. Your existing customers see no change. Agent traffic becomes a measurable line item without a procurement cycle. **Who this is for:** * SaaS platforms looking to open an agent-buyer channel without disrupting enterprise contracts * Data and API companies that want a per-call self-serve tier alongside their existing plans * Product and revenue leaders evaluating whether agentic commerce fits their API surface *** ## How it works
Per-call monetization
1 Agent API Calls a priced route without a payment header
2 API Agent 402 Payment Required with price and accepts block
3 Agent API Retries with PAYMENT-SIGNATURE header
4 API Facilitator Verifies signature, settles USDC on Polygon
5 Facilitator Seller wallet USDC settles to payout address
6 API Agent 200 OK with response body and PAYMENT-RESPONSE receipt
Middleware runs after your existing auth. Calls with a valid API key flow through the existing path unchanged. Unauthenticated calls fall through to the x402 path. The Polygon-hosted facilitator (`x402.polygon.technology` for mainnet, `x402-amoy.polygon.technology` for Amoy) handles verification and settlement, so you do not run that infrastructure. *** ## Get started Add x402 v2 middleware in front of the routes you want priced. The Polygon-hosted facilitator handles verification and settlement, so you do not run that infrastructure. Existing API-key traffic flows through your existing path unchanged. ### Install ```bash theme={null} bun install @x402/express @x402/core @x402/evm express ``` ```bash theme={null} bun install @x402/next @x402/core @x402/evm ``` ```bash theme={null} bun install @x402/hono @x402/core @x402/evm hono @hono/node-server ``` Full middleware setup, including configuration and the facilitator client, is in the [x402 Quickstart for Sellers](/payment-services/agentic-payments/x402/guides/quickstart-sellers). ### Price a route Each priced route declares an `accepts` block. Amounts are in the smallest unit of the currency (USDC has six decimals, so `1000` = \$0.001). ```json theme={null} { "x402Version": 2, "accepts": [ { "scheme": "exact", "network": "eip155:137", "maxAmountRequired": "1000", "description": "Single call to /v1/lookups/company" } ] } ``` ### Test as a buyer Verify the loop end-to-end with the Polygon Agent CLI: ```bash theme={null} polygon-agent x402-pay --url https://your-api.example.com/v1/lookups/company ``` *** ## Implementation Add x402 v2 middleware to Express, Next.js, or Hono. Protocol mechanics, accepts block shape, receipts. Test the loop end-to-end as an agent buyer. Configure middleware against the Polygon-hosted facilitator. # Per-inference billing for AI APIs Source: https://docs.polygon.technology/payment-services/agentic-payments/solutions/per-inference-billing Price model inference on actual token usage with the x402 v2 upto scheme. Polygon's mainnet facilitator runs v2 today. A model host or inference proxy serves AI agents at variable cost: short responses cost cents, long ones cost dollars, and the cost is only known after the work is done. A flat per-call price either overcharges short calls or underprices long ones. With the x402 v2 `upto` scheme, the buyer authorizes a maximum spend per call; the server settles for the actual amount, capped at that maximum. Each settlement reflects the real tokens generated, compute time, or output size. This is the right primitive for inference billing, and it is supported on Polygon's mainnet and Amoy facilitators today. **Who this is for:** * Hosted-model providers and inference proxies opening an agent-buyer channel * AI platform teams replacing token-bucket subscriptions with usage-based pricing * Commercial leaders at AI companies who need per-call billing without overcharging *** ## How it works
upto scheme flow
1 Agent API POST /v1/chat/completions
2 API Agent 402 with scheme: upto, maxAmountRequired (per-call ceiling)
3 Agent Facilitator Authorizes max via Permit2 (no funds move yet)
4 API Runs inference, measures actual usage (tokens, time, bytes)
5 API Facilitator Settles actual amount, ≤ authorized max
6 API Agent 200 OK with completion + PAYMENT-RESPONSE receipt
The `upto` scheme has two phases: at **verification** time, `maxAmountRequired` is the ceiling the buyer authorizes; at **settlement** time, the server passes the actual amount it computed from real usage. Replay protection comes from Permit2 nonces. Authorizations carry `validAfter` and `deadline` bounds so unsettled authorizations expire safely. Polygon's mainnet (`x402.polygon.technology`) and Amoy (`x402-amoy.polygon.technology`) facilitators run x402 v2, so `upto` works today. *** ## Get started Add x402 v2 middleware with the `upto` scheme in front of your inference route. The buyer authorizes a maximum spend; your server settles the actual amount after measuring real usage. Polygon's mainnet and Amoy facilitators run v2. ### Install ```bash theme={null} bun install @x402/express @x402/core @x402/evm express ``` ```bash theme={null} bun install @x402/next @x402/core @x402/evm ``` ```bash theme={null} bun install @x402/hono @x402/core @x402/evm hono @hono/node-server ``` Full middleware setup, including configuration and the facilitator client, is in the [x402 Quickstart for Sellers](/payment-services/agentic-payments/x402/guides/quickstart-sellers). ### Declare a maximum per call Return an `accepts` block with `scheme: "upto"` and a ceiling. `maxAmountRequired` is the maximum the buyer authorizes. USDC has six decimals, so `50000` = \$0.05. ```json theme={null} { "x402Version": 2, "accepts": [ { "scheme": "upto", "network": "eip155:137", "maxAmountRequired": "50000", "description": "Inference on model-7b-instruct, up to 4096 output tokens" } ] } ``` After running inference, compute the real cost from your measured units and submit the actual amount to the facilitator's `/settle` endpoint. The actual amount must be less than or equal to `maxAmountRequired`. ### Test as a buyer ```bash theme={null} polygon-agent x402-pay --url https://api.example.com/v1/chat/completions \ --method POST \ --body '{"model":"model-7b-instruct","messages":[{"role":"user","content":"hi"}]}' ``` *** ## Implementation Add x402 v2 middleware to Express, Next.js, or Hono. Verification vs. settlement phases, accepts shape, receipts. Point middleware at the v2 facilitator on mainnet or Amoy. Test as a buyer with `x402-pay`. # Polymarket hedging of operational risk Source: https://docs.polygon.technology/payment-services/agentic-payments/solutions/polymarket-hedging Take small-dollar Polymarket positions to hedge regulatory, market, or event-driven risk from the CLI. A business faces real exposure to events it cannot control: a regulatory decision that would force a product change, a rate move that swings demand, a sporting outcome that drives ad inventory pricing. Traditional risk desks cannot hedge most of these because the instruments are not on regulated venues. Polymarket's CLOB does have them. The Agent CLI ships first-class Polymarket support, discover markets, fund a proxy wallet, place CLOB buys and sells, list positions, cancel orders, under the same dry-run + broadcast safety model as every other write command. This is small-dollar tactical hedging. Treat it the way a finance team treats a corporate hedging program: bounded notional, explicit policy, recorded receipts. **Who this is for:** * Strategy and risk teams piloting event-driven hedges that traditional desks do not offer * Companies with measurable exposure to regulatory, election, or macro outcomes * Operations teams hedging supply-chain, demand, or weather risk against published markets *** ## How it works
Hedge cycle
1 Operator CLI polygon-agent polymarket markets --search "rate cut"
2 CLI Operator Lists active markets by volume with conditionId
3 Operator CLI polymarket clob-buy \ YES 25 (dry-run)
4 Smart wallet Proxy wallet On broadcast: funds proxy, places CLOB buy
5 Operator CLI polymarket positions / orders to monitor
6 Operator CLI polymarket sell ... when the position resolves or rotates
Polymarket uses a separate EOA key for CLOB signing, imported once via `polymarket set-key`. The proxy wallet needs a one-time `polymarket approve --broadcast` before the first buy. Both are one-time setup steps; subsequent hedges are a single `clob-buy` followed by `positions` and `sell` when the position rotates. *** ## Get started Two one-time steps before the first hedge: import an EOA private key for CLOB signing via `polymarket set-key`, then run `polymarket approve --broadcast` to authorize the proxy wallet. After that, the cycle is `markets` to discover, `clob-buy` to enter, `positions` to monitor, `sell` to exit. ### Install ```bash theme={null} npm install -g @polygonlabs/agent-cli polygon-agent setup --name "hedging-desk" polygon-agent wallet create --usdc-limit 5000 # One-time Polymarket setup polygon-agent polymarket set-key 0xYOUR_EOA_PRIVATE_KEY polygon-agent polymarket approve --broadcast ``` The smart wallet funds the Polymarket proxy on each buy. Size `--usdc-limit` to match the maximum notional you intend to hedge. ### Drive it from an LLM ```text theme={null} Read https://agentconnect.polygon.technology/SKILL.md and find the top 5 Polymarket markets on Fed rate decisions resolving in the next 90 days. Show me the YES/NO prices and the implied probabilities, then place a $50 YES hedge on the market most aligned with a rate cut. ``` ### Run the commands directly ```bash theme={null} # Discover markets by keyword polygon-agent polymarket markets --search bitcoin --limit 5 # Take a YES position with $25 USDC at market price (dry-run, then broadcast) polygon-agent polymarket clob-buy YES 25 polygon-agent polymarket clob-buy YES 25 --broadcast # Monitor and exit polygon-agent polymarket positions polygon-agent polymarket sell YES 50 --broadcast ``` Full reference: [Polymarket](/payment-services/agentic-payments/cli/polymarket). *** ## Implementation Full command reference: `markets`, `clob-buy`, `sell`, `positions`, more. The smart wallet funds each Polymarket buy. Keep USDC available before placing orders. Move USDC into the smart wallet before the first hedge. # Agentic SaaS metering Source: https://docs.polygon.technology/payment-services/agentic-payments/solutions/saas-metering Add a per-action x402 channel to a seat-priced SaaS product. Keep enterprise contracts intact; bill agent traffic by usage. A SaaS product is sold by seat with annual contracts and metered overage. Agent customers do not fit a seat model: they consume in bursts, will not sign contracts, and need to pay only for actions taken. By layering x402 middleware after existing auth on the same API, agents can transact per action while seat customers see no change. Both channels charge the same per-action units, so finance gets one ledger sliced by channel. Three channels run in parallel against the same product surface. **Who this is for:** * SaaS platforms with an API surface consumed by customer agents * Pricing and product leaders evaluating per-action billing for the agent segment * Finance and operations teams designing one ledger across multiple billing motions *** ## How it works
Layered billing channels
1 Caller API POST /v1/documents/process
2 API Valid API key + in plan? → serve, increment seat usage
3 API Valid API key + over plan? → charge overage on existing path
4 API Agent No API key? → 402 with per-action price
5 Agent Facilitator Retries with PAYMENT-SIGNATURE; facilitator verifies and settles
6 API Ledger Write billable event tagged with channel (seat / overage / x402)
The same action taxonomy you already meter for overage billing also drives x402 pricing. Each priced action maps to a `maxAmountRequired` in USDC. Finance pulls one report; channel becomes a column, not a separate ledger. For variable-cost actions (output-size or compute-driven), use the x402 v2 `upto` scheme described in [Per-inference billing](/payment-services/agentic-payments/solutions/per-inference-billing). *** ## Get started Layer x402 v2 middleware after your existing auth. Seat customers and overage customers flow through the existing path. Unauthenticated calls fall through to x402 and pay per action using the same action taxonomy you already meter. ### Install ```bash theme={null} bun install @x402/express @x402/core @x402/evm express ``` ```bash theme={null} bun install @x402/next @x402/core @x402/evm ``` ```bash theme={null} bun install @x402/hono @x402/core @x402/evm hono @hono/node-server ``` Full middleware setup, including configuration and the facilitator client, is in the [x402 Quickstart for Sellers](/payment-services/agentic-payments/x402/guides/quickstart-sellers). ### Price a metered action Each priced action declares its own `accepts` block. Reuse your existing action taxonomy so finance can pull one ledger sliced by channel. ```json theme={null} { "x402Version": 2, "accepts": [ { "scheme": "exact", "network": "eip155:137", "maxAmountRequired": "100000", "description": "Document processed" } ] } ``` ### Test as a buyer ```bash theme={null} polygon-agent x402-pay --url https://api.example.com/v1/documents/process \ --method POST \ --body '{"document":"..."}' ``` *** ## Implementation Add the middleware behind your existing auth. For variable-cost actions, use the `upto` scheme. The general pattern this scenario specializes. Protocol mechanics and receipts. # Treasury yield sweep Source: https://docs.polygon.technology/payment-services/agentic-payments/solutions/treasury-yield-sweep Sweep idle USDC into Aave v3, Morpho, or ERC-4626 vaults from the CLI. Withdraw on a schedule before the next disbursement run. A treasury wallet rarely uses all its balance at once. Between payouts, x402 spend, and operating reserve, a meaningful slice sits idle. Idle USDC earning zero is a cost. The Agent CLI sweeps that idle balance into Trails earn pools (Aave v3, Morpho, ERC-4626 vaults) in one command, and pulls it back the same way before the next disbursement cycle. Dry-run by default, broadcast when finance signs off. The CLI handles pool discovery (highest TVL by default), session-scoped contract permissions (set once at wallet creation with `--defi`), and the underlying Trails routing. No multisig coordination, no DEX UI, no signature-collection step. **Who this is for:** * Treasurers at platforms running agent fleets with weekly or monthly disbursement cycles * Crypto-native businesses holding USDC working capital * Operations teams looking for predictable yield on operating reserves *** ## How it works
Sweep and exit cycle
1 Operator CLI polygon-agent deposit --asset USDC --amount 50000 (dry-run)
2 CLI Trails Selects highest-TVL pool, returns projected APY
3 Operator CLI Re-runs with --broadcast after finance review
4 Smart wallet Aave / Morpho Deposits, accrues yield, holds aToken or vault share
5 Operator CLI polygon-agent withdraw --asset USDC --amount max --broadcast
The wallet must be created with `--defi` (or with explicit `--contract` flags listing the pool and the asset token) so the smart wallet can interact with Aave v3, Morpho, and ERC-4626 vaults without re-approval. Without that, deposit and withdraw fail at dry-run with a session-rejection error. *** ## Get started Two CLI commands drive the cycle: `deposit` into the highest-TVL pool by default (override with `--protocol aave` or `--protocol morpho`), and `withdraw --amount max` for a clean exit before the next disbursement run. Both are dry-run by default. ### Install ```bash theme={null} npm install -g @polygonlabs/agent-cli polygon-agent setup --name "treasury" polygon-agent wallet create --defi --usdc-limit 50000 ``` The `--defi` flag is required at wallet creation so the smart wallet can interact with Aave v3, Morpho, and ERC-4626 vaults without re-approval. Without it, `deposit` and `withdraw` fail at dry-run. ### Drive it from an LLM ```text theme={null} Read https://agentconnect.polygon.technology/SKILL.md and deposit my idle USDC into the highest-TVL lending vault on Polygon and report the APY and pool address. Then set up a daily cron job to automatically re-evaluate and deposit into the best vault each morning. ``` ### Run the commands directly ```bash theme={null} # Sweep idle USDC (dry-run first, review APY and pool, then broadcast) polygon-agent deposit --asset USDC --amount 5000 --protocol aave polygon-agent deposit --asset USDC --amount 5000 --protocol aave --broadcast # Exit before the next disbursement run polygon-agent withdraw --asset USDC --amount max --protocol aave --broadcast ``` Full reference: [Swaps and yield](/payment-services/agentic-payments/cli/swaps-and-yield). *** ## Implementation Full reference for `deposit`, `withdraw`, and `swap` against Trails earn pools. Pass `--defi` at wallet creation to enable yield operations. The underlying earn-pool router. # Facilitator Addresses Source: https://docs.polygon.technology/payment-services/agentic-payments/x402/analytics Reference list of x402 Polygon mainnet and Amoy facilitator addresses for analytics and payment verification. You can also track an up-to-date list of facilitators and their addresses at [facilitators.x402.watch](https://facilitators.x402.watch). ## Polygon Amoy The Amoy testnet facilitator ([x402-amoy.polygon.technology](https://x402-amoy.polygon.technology)) runs **x402 v2**. Signer addresses: ```text theme={null} Address: 0x5a30808427e4C50Abe7430EEA7722CA88379A6a1 Address: 0x7155A302A792C2aD555F2B66E5bC2709044D12dF Address: 0x17070da58e348b27098e1a04e848AC8faA66DA62 Address: 0x812ab2C611D5103a0907028226B756466017FcF8 Address: 0x863f5153F416f0186C762Eeb5Ce7721c694f344e Address: 0x680B2227bde0858A269B4a3FE24eaaFC3C3E1e96 Address: 0x8D7e970Cb3c0865d46C0C5bd3a1c1203a9d12aBc Address: 0x06853A2390c500D351d470d5c191794404AC6FA4 ``` ## Polygon (mainnet) The mainnet facilitator ([x402.polygon.technology](https://x402.polygon.technology)) runs **x402 v2**. Signer addresses: Polygon maintains a rotation of three sets of eight addresses: ``` x402_1.keys:Address: 0x29df60c005506AA325d7179F6e09eB4b4875dAde x402_1.keys:Address: 0xF09A94831C18566781f70937f0996B96EfE691C8 x402_1.keys:Address: 0x42618f623Ec19beFf78dE9DbBFB653BfEaC05D09 x402_1.keys:Address: 0x3202643514D128FF0B4625D2682c0244CF58131c x402_1.keys:Address: 0x11DA3fe5ADA6f5382Ebe972f14C3585DA4E65AeA x402_1.keys:Address: 0x135DfE729F9bbd7F88181E1B708d7506fd499140 x402_1.keys:Address: 0xDcb0Ac359025dC0DB1e22e6d33F404e5c92A1564 x402_1.keys:Address: 0x99EFc08BB42282716fB59D221792f5207f714C9d ``` ``` x402_2.keys:Address: 0xbE5115800247405f020197BF473eBFd085a2C635 x402_2.keys:Address: 0x5eAb3D78264Dab340340d6a37Ff0836464Ae5773 x402_2.keys:Address: 0xE5D4197eFd5D03E3f30cBf11C0fF63Eb95a0A656 x402_2.keys:Address: 0xfac8Edb989f1ba7F9dBb7A1233542D4e1fD6144F x402_2.keys:Address: 0xaFdbfaCb5ed691bf0bCFA660901f299ce9775489 x402_2.keys:Address: 0x1e48Ed59a502D0B324CdAf83362865b3ff49ABa2 x402_2.keys:Address: 0xA1dcBDC2C34577ACD4A1152A98807B2f281A112e x402_2.keys:Address: 0x9e281D4e26E1a4e7C27014E2ca8Cee7F2D44fa52 ``` ``` x402_3.keys:Address: 0x76FCb8ae3365A487E6EA235386C1cf3AbADeDA60 x402_3.keys:Address: 0x9523B120C75640469f1D16490Da0388928229452 x402_3.keys:Address: 0x153F3A70e4400c211d9B482b62aD721Bb02F96F6 x402_3.keys:Address: 0xd5dD012019C58882Dd507A8b3fCBB7b62e9a24c3 x402_3.keys:Address: 0xfff23108338C218F895d75980E14688218D4E92a x402_3.keys:Address: 0xF744e153Ef63f7EEe4a58e0F13761D16C2125EE3 x402_3.keys:Address: 0x0a8B10FE8Bd3072351600Adef4796F3F7aF72Ab0 x402_3.keys:Address: 0x971b4079A618F72Fa0F1792b07ed5923dfBF3500 ``` ## ThirdWeb ``` 0x80c08de1a05Df2bD633CF520754e40fdE3C794d3 ``` ## Questflow ``` 0x4a288FA07fC40F701e4fd2620F0a14338e12a4D7 ``` ## PayAI ``` 0xc6699d2aadA6c36Dfea5C248DD70f9CB0235cB63 ``` ## x402.rs ``` 0xD8Dfc729cBd05381647EB5540D756f4f8Ad63eec ``` ## Corbits ``` 0x06F0BfD2C8f36674DF5cdE852c1eeD8025C268C9 ``` # x402 How It Works Source: https://docs.polygon.technology/payment-services/agentic-payments/x402/guides/how-it-works How the x402 v2 protocol turns an HTTP 402 response into a verified onchain payment, with one round-trip and no signup. x402 turns the HTTP 402 status code into a real payment channel. A client requests a paid resource, the server responds with a price and a payment requirement, the client signs and retries with a payment header, the server verifies (locally or through a facilitator) and settles onchain, then returns the resource with a receipt. The whole exchange is two HTTP round-trips and no signup, API key, or contract. Polygon's mainnet and Amoy facilitators run **x402 v2**. If you have a V1 integration, see [Migration: V1 to V2](./migration-v1-to-v2). *** ## How it works
x402 payment flow
1 Client Resource Server GET /endpoint
2 Resource Server Client 402 Payment Required + PaymentRequirements (accepts array)
3 Client Resource Server Retries with `PAYMENT-SIGNATURE` header
4 Resource Server Facilitator POST /verify (signature + requirements)
5 Facilitator Polygon Settles USDC onchain
6 Resource Server Client 200 OK + body + `PAYMENT-RESPONSE` receipt header
Steps 2 and 3 collapse into a single call if the client already knows the route's price ahead of time (for example, from a manifest). The signed payment header travels with the first request. *** ## Protocol resources | Resource | Role | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Client | Requests a resource and signs the payment (browser, agent, SDK) | | Resource Server | Protects endpoints, issues 402, verifies payment payloads | | Facilitator | Optional verification + onchain settlement layer (Polygon: `x402.polygon.technology` mainnet, `x402-amoy.polygon.technology` Amoy) | | Payment Scheme | Defines chain, token, network, format (`exact` or `upto`) | | `PaymentRequirements` | `accepts` array in the 402 body: scheme, network, amount, description | | `PAYMENT-SIGNATURE` header | Signed payment payload on the retry request | | `PAYMENT-RESPONSE` header | Base64-encoded receipt with the settlement transaction hash | *** ## Server side (sellers) Protect routes with v2 middleware that pairs an `x402ResourceServer` with a facilitator client. The first unpaid request returns a 402 with an `accepts` array: ```json theme={null} { "x402Version": 2, "accepts": [ { "scheme": "exact", "network": "eip155:137", "maxAmountRequired": "1000", "description": "Access to premium API" } ], "error": null } ``` `maxAmountRequired` is in the smallest unit of the currency. USDC has six decimals, so `1000` = \$0.001. On retry, verify the `PAYMENT-SIGNATURE` against the facilitator and respond `200 OK` with a `PAYMENT-RESPONSE` header carrying the receipt. For setup, see the [x402 Quickstart for Sellers](./quickstart-sellers). *** ## Client side (buyers) The buyer sends the initial request, reads the 402, picks one `accepts` entry, signs against its wallet, and retries with the payment header. * Create an `x402Client`, register `ExactEvmScheme` for `"eip155:*"`. * Use `wrapFetchWithPayment` or `wrapAxiosWithPayment` to handle 402s transparently. * The signed payload becomes a base64 string on the retry: ```http theme={null} PAYMENT-SIGNATURE: ``` * Read the resulting receipt via `x402HTTPClient.getPaymentSettleResponse()` after a successful response. For setup, see the [x402 Quickstart for Buyers](./quickstart-buyers). *** ## Facilitator (optional) The facilitator exposes verification and settlement endpoints: ```http theme={null} POST /verify { "x402Version": 2, "paymentHeader": "", "paymentRequirements": { ... } } ``` Some facilitators settle each payment instantly; others batch settlements for high-volume sellers. Polygon's facilitators settle instantly on mainnet and Amoy. | Network | Facilitator URL | | --------------- | -------------------------------------- | | Polygon mainnet | `https://x402.polygon.technology` | | Polygon Amoy | `https://x402-amoy.polygon.technology` | *** ## Design goals * **HTTP-native.** Uses standard status codes and headers. No additional protocol layer. * **Chain and token agnostic.** Works across any chain or stablecoin. * **Minimal integration.** Middleware on the server side; a few wrappers on the buyer side. * **Micropayments.** Low friction and low cost, suitable for per-request pricing. * **Autonomous agents.** AI systems can transact without per-transaction human approval. *** ## Implementation Add x402 v2 middleware to Express, Next.js, or Hono. Pay x402-protected endpoints programmatically. Upgrade an existing V1 integration to v2. Signer addresses for the Polygon mainnet and Amoy facilitators. # Migration: x402 V1 to V2 Source: https://docs.polygon.technology/payment-services/agentic-payments/x402/guides/migration-v1-to-v2 Guide for upgrading Polygon x402 integrations from V1 to V2: packages, headers, network identifiers, and middleware patterns. This guide helps you migrate from **x402 V1 to V2** on Polygon. The V2 protocol introduces standardized CAIP-2 network identifiers, updated HTTP headers, and a modular `@x402/*` package layout. Polygon facilitators on [mainnet](https://x402.polygon.technology) and [Amoy](https://x402-amoy.polygon.technology) run **x402 v2**. V1 clients and middleware will not work against these endpoints. For Go, Python, and full upstream details, see the [official migration guide](https://docs.x402.org/guides/migration-v1-to-v2). ## Overview | Aspect | V1 | V2 | | --------------- | ------------------------------------------ | ------------------------------------------------------------------------ | | Payment header | `X-PAYMENT` | `PAYMENT-SIGNATURE` | | Response header | `X-PAYMENT-RESPONSE` | `PAYMENT-RESPONSE` | | Network format | String (`polygon-amoy`, `polygon`) | CAIP-2 (`eip155:80002`, `eip155:137`) | | Version field | `x402Version: 1` | `x402Version: 2` | | Packages | `x402-fetch`, `x402-axios`, `x402-express` | `@x402/fetch`, `@x402/axios`, `@x402/express`, `@x402/core`, `@x402/evm` | ## For buyers ### Before (V1) ```ts theme={null} import { wrapFetchWithPayment } from "x402-fetch"; import { createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { polygonAmoy } from "viem/chains"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const walletClient = createWalletClient({ account, chain: polygonAmoy, transport: http(), }); const fetchWithPayment = wrapFetchWithPayment(fetch, walletClient); const response = await fetchWithPayment("https://api.example.com/paid-endpoint"); ``` ### After (V2) ```ts theme={null} import { wrapFetchWithPayment } from "@x402/fetch"; import { x402Client } from "@x402/core/client"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { privateKeyToAccount } from "viem/accounts"; const signer = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`); const client = new x402Client(); client.register("eip155:*", new ExactEvmScheme(signer)); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("https://api.example.com/paid-endpoint"); ``` ### Key changes 1. **Package rename:** `x402-fetch` → `@x402/fetch` 2. **Wallet setup:** Use `x402Client` with `.register()` instead of passing a chain-bound `walletClient` 3. **Environment variable:** `PRIVATE_KEY` → `EVM_PRIVATE_KEY` 4. **Receipt header:** Read `PAYMENT-RESPONSE` via `x402HTTPClient.getPaymentSettleResponse()`, not `decodeXPaymentResponse()` on `x-payment-response` ## For sellers ### Before (V1) ```ts theme={null} import { paymentMiddleware } from "x402-express"; app.use( paymentMiddleware( "0xYourAddress", { "GET /weather": { price: "$0.001", network: "polygon-amoy", config: { description: "Weather data" }, }, }, { url: "https://x402-amoy.polygon.technology" } ) ); ``` ### After (V2) ```ts theme={null} import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const payTo = "0xYourAddress"; const facilitatorClient = new HTTPFacilitatorClient({ url: "https://x402-amoy.polygon.technology", }); app.use( paymentMiddleware( { "GET /weather": { accepts: [ { scheme: "exact", price: "$0.001", network: "eip155:80002", payTo, }, ], description: "Weather data", mimeType: "application/json", }, }, new x402ResourceServer(facilitatorClient).register( "eip155:80002", new ExactEvmScheme() ) ) ); ``` ### Key changes 1. **Package rename:** `x402-express` → `@x402/express` 2. **Configuration structure:** Route config uses an `accepts` array with explicit `scheme`, `network`, and `payTo` 3. **Network format:** `polygon-amoy` → `eip155:80002`, `polygon` → `eip155:137` 4. **Resource server:** Create `x402ResourceServer` with a facilitator client and register schemes with `.register()` 5. **Middleware signature:** Pass `(routes, server)` instead of `(wallet, routes, facilitatorConfig)` ## Network identifier mapping | V1 name | V2 CAIP-2 ID | Chain ID | Description | | -------------- | ----------------- | -------- | ------------------------ | | `polygon-amoy` | `eip155:80002` | 80002 | Polygon Amoy testnet | | `polygon` | `eip155:137` | 137 | Polygon PoS mainnet | | `base-sepolia` | `eip155:84532` | 84532 | Base Sepolia testnet | | `base` | `eip155:8453` | 8453 | Base mainnet | | `ethereum` | `eip155:1` | 1 | Ethereum mainnet | | `sepolia` | `eip155:11155111` | 11155111 | Ethereum Sepolia testnet | ## Package migration reference | V1 package | V2 package(s) | | -------------- | ------------------------- | | `x402` | `@x402/core` | | `x402-express` | `@x402/express` | | `x402-axios` | `@x402/axios` | | `x402-fetch` | `@x402/fetch` | | `x402-hono` | `@x402/hono` | | `x402-next` | `@x402/next` | | (built-in) | `@x402/evm` (EVM support) | Install examples: ```bash theme={null} # Buyers npm install @x402/fetch @x402/core @x402/evm viem # Sellers (Express) npm install @x402/express @x402/core @x402/evm express ``` ## Header changes If you implement custom HTTP handling, update header names: ```ts theme={null} // V1 const payment = req.header("X-PAYMENT"); res.setHeader("X-PAYMENT-RESPONSE", responseData); // V2 const payment = req.header("PAYMENT-SIGNATURE"); res.setHeader("PAYMENT-RESPONSE", responseData); ``` ## Troubleshooting ### Payment verification failures * Use CAIP-2 network identifiers (`eip155:80002` or `eip155:137`), not `polygon-amoy` or `polygon` * Verify your `payTo` address is correct * Confirm the facilitator URL matches the network: * Amoy: `https://x402-amoy.polygon.technology` * Mainnet: `https://x402.polygon.technology` ### "Cannot find module" errors Ensure you installed all V2 packages: ```bash theme={null} npm install @x402/axios @x402/core @x402/evm # Sellers npm install @x402/express @x402/core @x402/evm ``` ### Mixed V1/V2 compatibility During migration, update both buyers and sellers to V2 when targeting Polygon facilitators. Legacy V1 network strings and headers will fail against v2-only endpoints. ## Next steps * [x402 Quickstart for Buyers](./quickstart-buyers.mdx) * [x402 Quickstart for Sellers](./quickstart-sellers.mdx) * [Using the Polygon Facilitator](./using-polygon-facilitator.mdx) * [Official migration guide](https://docs.x402.org/guides/migration-v1-to-v2) # x402 Quickstart for Buyers Source: https://docs.polygon.technology/payment-services/agentic-payments/x402/guides/quickstart-buyers Tutorial: set up an x402 v2 buyer client on Polygon to automatically pay for and access paywalled API endpoints. This tutorial walks through setting up an **x402 v2** buyer client on Polygon. By the end, your client will automatically detect 402 Payment Required responses, pay in USDC, and retrieve the unlocked resource. Polygon facilitators on [Amoy](https://x402-amoy.polygon.technology) and [mainnet](https://x402.polygon.technology) run **x402 v2**. Use the `@x402/*` packages shown below. If you have an existing V1 integration, see [Migration: V1 to V2](./migration-v1-to-v2.mdx). This tutorial uses test credentials and local endpoints for clarity. In production, never expose private keys or facilitator URLs publicly. ## Prerequisites Before you begin, ensure you have: * A crypto wallet with USDC on Polygon Amoy (or mainnet for production) * [Node.js](https://nodejs.org/en) 18+ with npm or [Bun](https://bun.sh/) * A service that requires payment via x402, or a local seller from the [Quickstart for Sellers](./quickstart-sellers.mdx) For Go or Python client examples, see the [official x402 buyer quickstart](https://docs.x402.org/getting-started/quickstart-for-buyers). ## Install dependencies Install the x402 v2 client packages and [viem](https://viem.sh/) for signing: > Wherever `bun` is used, replace it with `npm` or your preferred package manager. ```bash theme={null} bun install @x402/fetch @x402/core @x402/evm viem dotenv ``` ```bash theme={null} bun install @x402/axios @x402/core @x402/evm axios viem dotenv ``` ## Create a wallet signer Create a signer from your private key. In x402 v2, the client selects the correct network from the 402 response; you do not bind a chain-specific wallet client. ```ts theme={null} import { privateKeyToAccount } from "viem/accounts"; import "dotenv/config"; const privateKey = process.env.EVM_PRIVATE_KEY; if (!privateKey) throw new Error("EVM_PRIVATE_KEY not set in .env"); const signer = privateKeyToAccount( privateKey.startsWith("0x") ? (privateKey as `0x${string}`) : (`0x${privateKey}` as `0x${string}`) ); console.log("Wallet address:", signer.address); ``` Fund this wallet with test USDC on Amoy before calling paid endpoints. See the [Polygon faucet](/tools/gas/matic-faucet/) for test POL. ## Make paid requests automatically `@x402/fetch` extends the native `fetch` API to handle 402 responses and payment headers for you. ```ts theme={null} import { wrapFetchWithPayment } from "@x402/fetch"; import { x402Client, x402HTTPClient } from "@x402/core/client"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { privateKeyToAccount } from "viem/accounts"; import "dotenv/config"; const signer = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}` ); const client = new x402Client(); client.register("eip155:*", new ExactEvmScheme(signer)); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const RESOURCE_URL = process.env.RESOURCE_URL || "http://127.0.0.1:4021/weather"; (async () => { const response = await fetchWithPayment(RESOURCE_URL, { method: "GET" }); const data = await response.json(); console.log("Response:", data); if (response.ok) { const httpClient = new x402HTTPClient(client); const paymentResponse = httpClient.getPaymentSettleResponse((name) => response.headers.get(name) ); console.log("Payment settled:", paymentResponse); } })(); ``` `@x402/axios` adds a payment interceptor to Axios so requests retry with payment headers automatically. ```ts theme={null} import { x402Client, wrapAxiosWithPayment, x402HTTPClient } from "@x402/axios"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { privateKeyToAccount } from "viem/accounts"; import axios from "axios"; import "dotenv/config"; const signer = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}` ); const client = new x402Client(); client.register("eip155:*", new ExactEvmScheme(signer)); const api = wrapAxiosWithPayment( axios.create({ baseURL: process.env.RESOURCE_BASE_URL || "http://127.0.0.1:4021", }), client ); (async () => { const response = await api.get("/weather"); console.log("Response:", response.data); const httpClient = new x402HTTPClient(client); const paymentResponse = httpClient.getPaymentSettleResponse((name) => response.headers[name.toLowerCase()] ); console.log("Payment settled:", paymentResponse); })(); ``` The client: 1. Sends the request 2. Receives **402 Payment Required** with payment requirements 3. Pays in USDC via the seller's facilitator (Polygon Amoy: `https://x402-amoy.polygon.technology`) 4. Retries with a `PAYMENT-SIGNATURE` header 5. Returns the unlocked response and a `PAYMENT-RESPONSE` receipt header ## Payment schemes Most Polygon endpoints use the **`exact`** scheme (fixed price per request). Servers may also advertise **`upto`** (usage-based) or **`batch-settlement`** (high-volume batched micropayments). Register the matching scheme when the server requires it. See [x402 payment schemes](https://docs.x402.org). ## Error handling ```ts theme={null} try { const response = await fetchWithPayment(url, { method: "GET" }); // Handle success } catch (error) { if (error instanceof Error && error.message.includes("No scheme registered")) { console.error("Network not supported: register the appropriate scheme"); } else if (error instanceof Error && error.message.includes("Payment already attempted")) { console.error("Payment failed on retry"); } else { console.error("Request failed:", error); } } ``` ## Reference Configuration, error codes, and guardrails. ### Schema | name | type | required | example | description | | ------------------- | ------ | -------- | ----------------------------------------- | ------------------------------- | | `EVM_PRIVATE_KEY` | string | yes | `"abcd1..."` | Wallet key for signing payments | | `RESOURCE_URL` | string | yes | `"https://api.example.com/paid-endpoint"` | Target API URL | | `RESOURCE_BASE_URL` | string | optional | `"http://127.0.0.1:4021"` | Base URL when using Axios | | USDC | token | implied | USDC on Polygon | Payment asset | The facilitator URL is configured on the **seller** side. Buyers do not set it directly; the 402 response includes payment requirements for the seller's facilitator. ### Errors | code / case | meaning | fix | | --------------------------- | --------------------------------- | -------------------------------------------------------------------------------- | | `MISSING_CONFIG` | Wallet or URL not set | Verify `.env` keys | | `No scheme registered` | Client missing scheme for network | Register `ExactEvmScheme` with `"eip155:*"` | | `Payment already attempted` | Payment failed on retry | Check wallet balance and facilitator health | | `402_LOOP` | API keeps returning 402 | Ensure seller uses x402 v2 and matching network (`eip155:80002` or `eip155:137`) | ### Do / Don't Do Guardrails | Do | Don't | | ------------------------------------------------------------- | -------------------------------------------------------------------- | | Use `@x402/fetch` or `@x402/axios` with v2 packages | Use legacy `x402-fetch` or `x402-axios` against Polygon facilitators | | Test on Amoy before mainnet | Hardcode private keys in scripts | | Read receipts via `x402HTTPClient.getPaymentSettleResponse()` | Parse `PAYMENT-RESPONSE` headers manually | | Use `wrapFetchWithPayment` or `wrapAxiosWithPayment` | Re-implement 402 logic yourself | ### References * [@x402/fetch on npm](https://www.npmjs.com/package/@x402/fetch) * [@x402/axios on npm](https://www.npmjs.com/package/@x402/axios) * [@x402/evm on npm](https://www.npmjs.com/package/@x402/evm) * [Official x402 buyer quickstart](https://docs.x402.org/getting-started/quickstart-for-buyers) * [Using the Polygon Facilitator](./using-polygon-facilitator.mdx) * [Migration: V1 to V2](./migration-v1-to-v2.mdx) # x402 Quickstart for Sellers Source: https://docs.polygon.technology/payment-services/agentic-payments/x402/guides/quickstart-sellers Tutorial: protect API endpoints with x402 v2 payment middleware to accept USDC payments on Polygon. This tutorial shows how to add **x402 v2** payment middleware to your API or service. By the end, buyers and AI agents will automatically pay in USDC when accessing your protected endpoints. Polygon facilitators on [Amoy](https://x402-amoy.polygon.technology) and [mainnet](https://x402.polygon.technology) run **x402 v2**. Use the `@x402/*` packages below. For Go or Python server examples, see the [official x402 seller quickstart](https://docs.x402.org/getting-started/quickstart-for-sellers). These snippets are for demonstration only. Store private keys and facilitator URLs in a secure vault. Never hardcode secrets. ## Prerequisites | Requirement | Example / Notes | | ---------------------- | ----------------------------------------------------------------------- | | Wallet to receive USDC | Any EVM-compatible wallet (Metamask, Rabby, Safe, etc.) | | Node.js | [Node.js 18+](https://nodejs.org/en) with npm or [Bun](https://bun.sh/) | | Existing API / server | Express, Next.js, or Hono | | Polygon network | [Amoy testnet](/pos/reference/rpc-endpoints/) or mainnet | This guide starts with **Amoy testnet** configuration. See [Running on Polygon mainnet](#running-on-polygon-mainnet) when you are ready for production. ## Install dependencies > Wherever `bun` is used, replace it with `npm` or your preferred package manager. ```bash theme={null} bun install @x402/express @x402/core @x402/evm express ``` ```bash theme={null} bun install @x402/next @x402/core @x402/evm ``` ```bash theme={null} bun install @x402/hono @x402/core @x402/evm hono @hono/node-server ``` ## Add payment middleware Integrate payment middleware with: * A **facilitator client** pointing at the Polygon Amoy facilitator * An **`x402ResourceServer`** with the EVM scheme registered * Route config using an **`accepts`** array with `scheme`, `price`, `network`, and `payTo` Full example in the [x402 repo](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/express). ```ts theme={null} import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const app = express(); const payTo = "0xYourEvmAddress"; const facilitatorClient = new HTTPFacilitatorClient({ url: process.env.FACILITATOR_URL || "https://x402-amoy.polygon.technology", }); app.use( paymentMiddleware( { "GET /weather": { accepts: [ { scheme: "exact", price: "$0.001", network: "eip155:80002", payTo, }, ], description: "Get current weather data for any location", mimeType: "application/json", }, }, new x402ResourceServer(facilitatorClient).register( "eip155:80002", new ExactEvmScheme() ) ) ); app.get("/weather", (_req, res) => { res.send({ report: { weather: "sunny", temperature: 70 }, }); }); app.listen(4021, () => { console.log("Server running at http://localhost:4021"); }); ``` Full example in the [x402 repo](https://github.com/x402-foundation/x402/tree/main/examples/typescript/fullstack/next). Use `withX402` for API routes. It settles payment only after a successful response (status below 400). ```ts theme={null} // lib/x402-server.ts import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server"; import { ExactEvmScheme } from "@x402/evm/exact/server"; export const payTo = "0xYourEvmAddress"; const facilitatorClient = new HTTPFacilitatorClient({ url: process.env.FACILITATOR_URL || "https://x402-amoy.polygon.technology", }); export const server = new x402ResourceServer(facilitatorClient).register( "eip155:80002", new ExactEvmScheme() ); ``` ```ts theme={null} // app/api/weather/route.ts import { NextRequest, NextResponse } from "next/server"; import { withX402 } from "@x402/next"; import { payTo, server } from "../../../lib/x402-server"; const handler = async (_: NextRequest) => { return NextResponse.json( { report: { weather: "sunny", temperature: 72 }, }, { status: 200 } ); }; export const GET = withX402( handler, { accepts: [ { scheme: "exact", price: "$0.001", network: "eip155:80002", payTo, }, ], description: "Access to weather API", mimeType: "application/json", }, server ); ``` Full example in the [x402 repo](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/hono). ```ts theme={null} import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { paymentMiddleware, x402ResourceServer } from "@x402/hono"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const app = new Hono(); const payTo = "0xYourEvmAddress"; const facilitatorClient = new HTTPFacilitatorClient({ url: process.env.FACILITATOR_URL || "https://x402-amoy.polygon.technology", }); app.use( paymentMiddleware( { "GET /weather": { accepts: [ { scheme: "exact", price: "$0.001", network: "eip155:80002", payTo, }, ], description: "Weather data", mimeType: "application/json", }, }, new x402ResourceServer(facilitatorClient).register( "eip155:80002", new ExactEvmScheme() ) ) ); app.get("/weather", (c) => { return c.json({ report: { weather: "sunny", temperature: 70 }, }); }); serve({ fetch: app.fetch, port: 4021 }); ``` Buyers calling protected routes receive a 402 challenge, pay via the facilitator, and then receive the response. ## Running on Polygon mainnet Once you have tested on Amoy, switch to mainnet: ### 1. Update the facilitator URL ```ts theme={null} const facilitatorClient = new HTTPFacilitatorClient({ url: "https://x402.polygon.technology", }); ``` ### 2. Update the network identifier Change `eip155:80002` (Amoy) to `eip155:137` (Polygon mainnet) in every `accepts` entry and scheme registration: ```ts theme={null} // Testnet → Mainnet network: "eip155:137", // ... server.register("eip155:137", new ExactEvmScheme()); ``` Both Polygon facilitators run **x402 v2**. See [Using the Polygon Facilitator](./using-polygon-facilitator.mdx) for signer addresses and health endpoints. ## Reference ### Schema | name | type | required | example | description | | ----------------- | ------ | -------- | ---------------------------------------- | ---------------------------------------------------- | | `payTo` | string | yes | `0xYourAddress` | Address that receives USDC | | `network` | string | yes | `eip155:80002` | CAIP-2 network ID (Amoy or `eip155:137` for mainnet) | | `scheme` | string | yes | `"exact"` | Payment scheme | | `price` | string | yes | `"$0.001"` | Cost per request in USDC | | `FACILITATOR_URL` | string | optional | `"https://x402-amoy.polygon.technology"` | Polygon facilitator endpoint | | `description` | string | optional | `"Weather data"` | Used for discoverability in x402 Bazaar | | `mimeType` | string | optional | `"application/json"` | Response content type | ### Errors | case / code | meaning | fix | | ----------------- | ----------------------------- | ----------------------------------------------------------- | | `402_LOOP` | Client cannot fulfill payment | Check facilitator URL, network ID, and wallet | | `INVALID_NETWORK` | Wrong network identifier | Use `eip155:80002` (Amoy) or `eip155:137` (mainnet) | | `BAD_CONFIG` | Missing route config | Define `accepts`, `price`, `network`, and `payTo` per route | ### Do / Don't Do Guardrails | Do | Don't | | ----------------------------------------------------------- | ----------------------------------------------------------- | | Use `https://x402-amoy.polygon.technology` for Amoy testing | Use legacy `x402-express` against Polygon facilitators | | Use `https://x402.polygon.technology` for mainnet | Expose private keys in middleware | | Test on Amoy before mainnet | Use V1 network strings like `"polygon-amoy"` or `"polygon"` | | Include `description` and `mimeType` for AI discoverability | Skip `payTo` in route config | ### References * [@x402/express on npm](https://www.npmjs.com/package/@x402/express) * [@x402/next on npm](https://www.npmjs.com/package/@x402/next) * [@x402/hono on npm](https://www.npmjs.com/package/@x402/hono) * [Official x402 seller quickstart](https://docs.x402.org/getting-started/quickstart-for-sellers) * [x402 TypeScript server examples](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers) * [Using the Polygon Facilitator](./using-polygon-facilitator.mdx) * [Migration: V1 to V2](./migration-v1-to-v2.mdx) # x402 protocol: agentic payments over HTTP Source: https://docs.polygon.technology/payment-services/agentic-payments/x402/intro x402 is the open payment protocol that brings onchain payments to the HTTP 402 Payment Required status code. Build agentic and AI agent payments for APIs and web services. Polygon-supported networks, facilitators, and quickstart. x402 is an open payment protocol that brings blockchain payments into the HTTP standard. By reusing the HTTP 402 Payment Required status code, it lets developers handle onchain and agentic payments with the same tools they already use for APIs and web services. Instead of building complex wallet integrations or subscription systems, developers can treat payments like any other part of the HTTP request/response cycle. This approach works for web2 developers experimenting with paid APIs and for web3 builders who need a lightweight way to support pay-per-use APIs, agent-to-agent transactions, and micropayments. [Read more about x402 here](https://x402.gitbook.io/x402). ## Access on Polygon Polygon supports **x402 v2** on mainnet and Amoy through the following facilitators: 1. Polygon Mainnet Facilitator: [https://x402.polygon.technology](https://x402.polygon.technology) 2. Polygon Amoy Facilitator: [https://x402-amoy.polygon.technology](https://x402-amoy.polygon.technology) Both Polygon facilitators run **x402 v2**. Use the `@x402/*` packages in the [Quickstart for Buyers](./guides/quickstart-buyers.mdx) and [Quickstart for Sellers](./guides/quickstart-sellers.mdx). Upgrading from V1? See [Migration: V1 to V2](./guides/migration-v1-to-v2.mdx). Additional facilitators compatible with Polygon: * [ThirdWeb](https://playground.thirdweb.com/payments/x402) * [x402.rs Facilitator Endpoint](https://facilitator.x402.rs/) * [Pay.AI](https://payai.network/) * [Corbits](https://docs.corbits.dev/about-x402/facilitators) * [Questflow](https://facilitator.questflow.ai/) To implement x402 with Polygon and your applications or agents, follow the tutorials and guides in these docs. Alternatively, the x402 community has created [multiple examples](https://github.com/coinbase/x402/tree/main/examples/typescript) which can be adapted for Polygon. # USDC Gateway Integration Source: https://docs.polygon.technology/payment-services/stablecoins/usdc-gateway-integration How to integrate USDC on Polygon using Circle's Gateway (CCTP) for cross-chain transfers. The following examples are demonstrations of integrations and should not be used in production. In production, store sensitive information such as API keys and private keys in a secrets manager or vault. This guide covers the Gateway approach to USDC on Polygon. Gateway USDC uses Circle's Cross-Chain Transfer Protocol (CCTP), which lets users hold a single omnichain balance and move it across chains without separate per-chain contracts. For payments that stay within Polygon, see [USDC Native Integration](/payment-services/stablecoins/usdc-native-integration/). | Approach | Description | When to use | | ------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------- | | Native USDC | Standard ERC-20 contract directly on Polygon Chain | When you only need payments within Polygon | | Gateway USDC | Circle's cross-chain system that moves USDC between blockchains (CCTP) | When your users need to send or receive USDC across chains | ## How Gateway USDC Works The Circle Gateway is a cross-chain USDC liquidity layer: * **Deposits**: Users send USDC to a Gateway Wallet contract. * **Attestations**: Circle verifies the burn event on the source chain. * **Mints**: A Gateway Minter contract releases the same amount on the destination chain. This design allows trust-minimized USDC movement between chains. ## Example: Deposit, Attest, and Mint The following example: 1. Deposits USDC into the Gateway wallet on Polygon 2. Requests an attestation from Circle's API 3. Mints the funds back via the Gateway Minter ```ts theme={null} import { createPublicClient, createWalletClient, http, parseUnits } from "viem"; import { polygon } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { fetch } from "undici"; const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; const GATEWAY_WALLET = "0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE"; const GATEWAY_MINTER = "0x2222222d7164433c4C09B0b0D809a9b52C04C205"; const erc20 = [ { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] }, { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] }, ]; const gatewayWalletAbi = [ { type: "function", name: "deposit", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [] }, ]; const gatewayMinterAbi = [ { type: "function", name: "gatewayMint", stateMutability: "nonpayable", inputs: [{ type: "bytes" }, { type: "bytes" }], outputs: [] }, ]; const account = privateKeyToAccount(process.env.PRIV_KEY as `0x${string}`); const rpc = http(process.env.POLYGON_RPC_URL); const pub = createPublicClient({ chain: polygon, transport: rpc }); const wallet = createWalletClient({ chain: polygon, transport: rpc, account }); async function gatewayTransfer() { // 1. Approve + deposit const decimals = await pub.readContract({ address: USDC, abi: erc20, functionName: "decimals" }); const amount = parseUnits("25", Number(decimals)); await wallet.writeContract({ address: USDC, abi: erc20, functionName: "approve", args: [GATEWAY_WALLET, amount] }); await wallet.writeContract({ address: GATEWAY_WALLET, abi: gatewayWalletAbi, functionName: "deposit", args: [USDC, amount] }); // 2. Request attestation from Circle Gateway API const res = await fetch("https://gateway-api.circle.com/v1/transfer", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${process.env.CIRCLE_API_KEY}` }, body: JSON.stringify({ burnIntent: { spec: { version: 1, sourceDomain: 7, // Polygon domain ID destinationDomain: 7, // same-chain "withdraw", or change for cross-chain sourceContract: GATEWAY_WALLET, destinationContract: GATEWAY_MINTER, sourceToken: USDC, destinationToken: USDC, sourceDepositor: account.address, destinationRecipient: "0xRecipient...", value: amount.toString(), }, }, }), }); const { attestationPayload, signature } = await res.json(); // 3. Submit attestation to GatewayMinter const tx = await wallet.writeContract({ address: GATEWAY_MINTER, abi: gatewayMinterAbi, functionName: "gatewayMint", args: [attestationPayload as `0x${string}`, signature as `0x${string}`], }); console.log("gatewayMint tx:", tx); } gatewayTransfer(); ``` # USDC Native Integration Source: https://docs.polygon.technology/payment-services/stablecoins/usdc-native-integration How to read balances and transfer native USDC on Polygon Chain using viem. The following examples are demonstrations of integrations and should not be used in production. In production, store sensitive information such as API keys and private keys in a secrets manager or vault. This guide covers native USDC on Polygon Chain. Native USDC behaves like any other ERC-20 token: you can read balances, approve spenders, and transfer tokens directly onchain. For cross-chain USDC transfers using Circle's CCTP, see [USDC Gateway Integration](/payment-services/stablecoins/usdc-gateway-integration/). | Approach | Description | When to use | | ------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------- | | Native USDC | Standard ERC-20 contract directly on Polygon Chain | When you only need payments within Polygon | | Gateway USDC | Circle's cross-chain system that moves USDC between blockchains (CCTP) | When your users need to send or receive USDC across chains | ## Example: Read Balance and Transfer The following example uses [viem](https://viem.sh/) to check the USDC balance and send 1 USDC on Polygon. ```ts theme={null} // pnpm add viem import { createPublicClient, createWalletClient, http, parseUnits } from "viem"; import { polygon } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // Native USDC contract on Polygon Chain (not bridged USDC.e) const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; const erc20 = [ { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] }, { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] }, { type: "function", name: "transfer", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] }, ]; const account = privateKeyToAccount(process.env.PRIV_KEY as `0x${string}`); const rpc = http(process.env.POLYGON_RPC_URL); const pub = createPublicClient({ chain: polygon, transport: rpc }); const wallet = createWalletClient({ chain: polygon, transport: rpc, account }); async function main() { const me = account.address; const decimals = await pub.readContract({ address: USDC, abi: erc20, functionName: "decimals" }); const bal = await pub.readContract({ address: USDC, abi: erc20, functionName: "balanceOf", args: [me] }); console.log("Balance:", Number(bal) / 10 ** Number(decimals), "USDC"); // Send 1 USDC const amount = parseUnits("1", Number(decimals)); const hash = await wallet.writeContract({ address: USDC, abi: erc20, functionName: "transfer", args: ["0xRecipient...", amount] }); console.log("tx:", hash); } main(); ``` The example above reads `PRIV_KEY` from an environment variable, which is the minimum required for a production deployment. In production, go further: store private keys in a dedicated secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) and never let them touch application memory directly. A more durable pattern is to delegate signing to a backend service or use a managed key management service (KMS) that signs transactions server-side, so application code never holds the raw private key. The `account` object in viem is replaceable with any signer that implements the `LocalAccount` interface, including hardware wallets and KMS-backed signers. # Cash-ins and cash-outs Source: https://docs.polygon.technology/payments/cash-ins-and-cash-outs Code-based cash deposits and withdrawals at retail locations, auto-converted between cash and crypto. OMS supports two complementary in-person cash flows: * **Cash-in**: the customer deposits physical cash at a retail location and receives crypto in the destination wallet. * **Cash-out** (cash pickup): the customer withdraws crypto from a wallet and collects physical cash at a retail counter or ATM. Both flows hand off a code at a partner retail location. Cash-in skips the upfront quote step; cash-out uses a standard quote with a cash destination. Both require the `usd` endorsement on the customer. ## Cash-in A cash-in is an in-person cash deposit flow. You create a cash-in that reserves a deposit code. The customer takes that code to a partner retail location, deposits cash, and OMS automatically converts it to crypto and delivers it to the destination wallet (`cashToCrypto`). Cash-ins skip the quote step: the customer decides how much to deposit at the counter, so no amount is fixed at creation. Pass an `indicatedAmount` on the source for an upfront pricing estimate. ### Cash-in limits | Limit | Value | | ------------------- | ---------------------------------- | | Transaction minimum | \$20 | | Transaction maximum | $500 ($1,000 at Walmart locations) | | Daily maximum | \$1,500 and 3 transactions | | Weekly maximum | \$3,500 and 12 transactions | | Monthly maximum | \$5,000 and 20 transactions | ### Create a cash-in `POST /cash-ins`. The request names the customer, the cash source location, and the crypto destination. ```json theme={null} { "customerId": "cst_...", "cash": { "locationId": "loc_...", "locationReference": "loc-ref-4521" }, "source": { "asset": "usd", "indicatedAmount": "100.00", "email": "jane@example.com" }, "destination": { "asset": "usdc", "network": "polygon", "wallet": { "id": "wlt_..." } }, "sponsorGas": true } ``` | Field | Shape | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `cash` | The deposit location: `locationId` and `locationReference`, both required. Read these from [cash locations](#supported-locations). | | `source` | Cash side: `asset` (required, `usd`), optional `indicatedAmount` for the pricing estimate, and optional `email`. | | `destination` | Crypto side: `asset`, `network`, and a `wallet` reference (`id`, `blockchainAddress`, or `externalAccount`). | | `sponsorGas` | Cover network gas on the customer's behalf. | Pass an `Idempotency-Key` header to safely retry without creating duplicates. ### Cash-in object ```json theme={null} { "id": "ci_...", "object": "cashIn", "type": "fiatToCrypto", "status": "pending", "customerId": "cst_...", "source": { "asset": "usd", "indicatedAmount": "100.00", "amountGross": "100.00", "amountNet": "99.42", "feesDeducted": { "total": "0.58" } }, "destination": { "asset": "usdc", "network": "polygon", "amountGross": "99.42", "amountNet": "99.42", "wallet": { "id": "wlt_..." } }, "cash": { "locationId": "loc_...", "locationReference": "loc-ref-4521" }, "location": { "name": "Retail Partner #4521", "address": "123 Main St, New York, NY 10001" }, "rates": { "exchangeRate": "1.00", "effectiveRate": "0.9942", "pair": "usd/usdc" }, "depositInstructions": { "code": "483291", "expiresAt": "2026-01-15T11:30:00Z", "locationName": "Retail Partner #4521", "locationAddress": "123 Main St, New York, NY 10001" }, "sponsorGas": true, "sponsorGasCost": "0", "transactionId": null, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" } ``` Display the `depositInstructions.code` in your UI. When you pass an `indicatedAmount`, the source and destination amounts carry the estimated figures and fees so you can show the customer what to expect before they visit the location. Amounts start as estimates and are finalized once the cash is deposited. The created transaction's ID appears on `transactionId` once the deposit is made. ### Cash-in flow ``` POST /cash-ins → deposit code (valid 1 hour) │ customer deposits cash at retail location │ OMS converts to crypto │ status → completed ``` The deposit code is valid for one hour. If it expires before the customer deposits, refresh it via `POST /cash-ins/{cashInId}/refresh`: there is no need to create a new cash-in. ### Cash-in status | Status | Meaning | | ------------ | ----------------------------------------------------- | | `pending` | Deposit code issued, awaiting the customer's deposit. | | `processing` | Cash deposited; conversion underway. | | `completed` | Cash converted and crypto delivered. | | `failed` | The deposit or conversion did not succeed. | | `expired` | Code expired before a deposit was made. | The `subStatus` field adds granular detail behind these values, for example `order_reserved`, `settled`, `cash_deposit_expired`, `cash_deposit_failed`, `provider_order_failed`, and `provider_order_template_error`. ### Cash-in operations | Operation | Endpoint | | -------------------- | ----------------------------------------------------------------------------------- | | Create a cash-in | `POST /cash-ins` | | Refresh deposit code | `POST /cash-ins/{cashInId}/refresh` | | Get a cash-in | `GET /cash-ins/{cashInId}` | | List cash-ins | `GET /cash-ins` (filter by customer, status, type, date range, or free-text search) | ### Simulating a cash-in (sandbox) In sandbox, drive a cash-in through its lifecycle without a real deposit using the simulation endpoints. They return `404` in production. | Operation | Endpoint | | ------------------------------------------------ | -------------------------------------- | | Authorize a simulated deposit at the barcode | `POST /cash-ins/simulate/barcode-auth` | | Commit a previously-authorized simulated deposit | `POST /cash-ins/simulate/auth-commit` | | Void a previously-authorized simulated deposit | `POST /cash-ins/simulate/auth-void` | ## Cash-out A cash-out (cash pickup) is the inverse of a cash-in: the customer holds crypto in an OMS wallet and withdraws it as physical cash. It uses a standard quote with a `cash` destination (`cryptoToCash`). OMS pulls crypto from the wallet, issues a time-limited pickup code, and the customer collects cash at a partner retail location. Set the amount on the destination side; it must be a multiple of $20.00, up to $400.00 per transaction. Cash pickup is under active development and available through early access. To enable it for your project, contact us. Share your use case and we'll reach out about cash pickup. ### Cash-out limits | Limit | Value | | --------------------- | --------------------------------------------------------------- | | Transaction increment | Multiples of \$20.00 | | Transaction minimum | \$20 | | Transaction maximum | \$400 | | Daily maximum | $1,000 in California and New Mexico; $2,000 in all other states | | Monthly maximum | \$60,000 | ### Cash-out flow ``` POST /quotes (walletOms → cash, amount on destination) │ POST /transactions { quoteId } → OMS pulls crypto from the wallet │ pickup code (time-limited) │ customer collects cash at retail location or ATM │ subStatus processing.cashPickupReady → completed.cashPickupCollected ``` ## Supported locations Cash deposits and withdrawals are processed through OMS's partner retail and ATM network across the US. List nearby locations with `GET /cash-locations`, passing the `provider`, `latitude`, and `longitude` query parameters, an optional `radius` and `limit`, and a `flow` of `cash_in` or `cash_out` to select the provider configuration. Each location returns its distance from the supplied coordinates, and each provider returns its per-transaction and per-customer daily limits. ## Related * [Cash-in guide](/api-reference/guide-cash-in): full walkthrough with request and response * [Cash pickup guide](/api-reference/guide-cash-pickup): the cash-out flow in detail * [Transactions](/payments/transactions): standard two-step flow for card and bank transfers # Compliance Source: https://docs.polygon.technology/payments/compliance How the Open Money Stack handles KYC, KYB, AML, and transaction monitoring for regulated payment products. ``` Transaction request │ ▼ 1. Identity Verification KYC · KYB · document verification · biometric liveness checks · accredited investor checks │ ▼ 2. Sanctions & Watchlist Screening OFAC SDN list · PEP screening · global sanctions · adverse media · country risk │ ▼ 3. AML Transaction Monitoring velocity limits · pattern detection · onchain analytics · risk scoring · Chainalysis / TRM │ ▼ Transaction approved SAR / CTR filing · audit trail · regulatory reporting · real-time alerts ``` The Open Money Stack includes compliance as a core layer, not a third-party add-on. KYC, KYB, AML screening, and transaction monitoring are built into the regulated fiat access infrastructure. Institutions can use the OMS compliance layer directly, or bring their own verification process where they already have one. *** ## Consumer onboarding (KYC) Every consumer transacting through the OMS must complete identity verification before their first transaction. This satisfies AML obligations across supported jurisdictions. There are two paths: ### Option 1: Use the OMS KYC process The OMS handles the full KYC flow on your behalf. Users complete identity and document verification through the built-in process. No additional identity infrastructure required. What is collected: * Government-issued ID (passport, driver's license, national ID) * Liveness check * Name, date of birth, address * OFAC and PEP screening ### Option 2: Bring your own KYC Organizations with an existing KYC process can pass verified user identities to the OMS directly. The OMS accepts KYC status from approved organizations, so users who have already been verified do not need to repeat the process. This is the preferred path for regulated fintechs, neobanks, and PSPs that already operate licensed identity verification. *** ## Business onboarding (KYB) For B2B and B2B2B flows, the OMS supports business onboarding with Know-Your-Business (KYB) verification. This is required for business customers transacting above standard thresholds or in regulated corridors. What is collected: * Legal company name and registration details * Date and jurisdiction of incorporation * Company type and tax identification number * Registered address * Ultimate Beneficial Owner (UBO): full legal name and identity verification * Business contact: phone and email KYB is structurally similar to consumer KYC but scoped to the entity and its controlling persons. The OMS handles collection, screening, and ongoing monitoring. *** ## Transaction monitoring (AML) Compliance does not end at onboarding. The OMS applies continuous transaction monitoring across all payment flows: * **Onchain screening**: every wallet address and transaction is screened against sanctions lists and risk databases before settlement * **Risk scoring**: transactions are scored in real time based on counterparty risk, geography, and behavior patterns * **OFAC and PEP checks**: applied at onboarding and re-screened on an ongoing basis * **Suspicious activity**: flagged transactions are reviewed before funds are released; reporting obligations are handled by the OMS compliance infrastructure *** ## Regulatory coverage The OMS compliance infrastructure operates money transmission directly under its own licenses: Money Transmitter Licenses in 37 US states plus exemptions in 11 more, covering 48 US states, alongside FinCEN registration as a Money Services Business. Institutions building on the OMS inherit this coverage rather than obtaining it independently. Request access to OMS compliance coverage and the related integration options. *** ## Frequently asked questions ### What is the difference between first-party and third-party transactions? **First-party:** a customer sends to or receives from an account they own. **Third-party:** a customer sends to or receives from an account owned by someone else. Business customers can do both first-party and third-party transactions. Individual customers are currently limited to first-party transactions only. *** ### Can a business customer receive funds from a bank account they don't own? Yes. Business customers support third-party on-ramps and off-ramps: | Scenario | Supported | | ----------------------------------------------------------------- | --------- | | On-ramp from a bank account owned by the same business | Yes | | On-ramp from a bank account owned by a different business | Yes | | Off-ramp to a bank account owned by the same business | Yes | | Off-ramp to a bank account owned by a different business | Yes | | Pass-through: third-party bank → wallet → third-party destination | No | Pass-through transactions, where funds arrive from one third party and are immediately forwarded to another, are not supported. *** ### Can an individual customer send funds to another person? Not currently. Individual customers are limited to first-party transactions: money must move to and from accounts they own. | Scenario | Supported | | ------------------------------------------- | --------- | | Cash-in to own wallet | Yes | | Cash-out from own wallet | Yes | | Cash-in to another person's wallet | No | | Cash-out to another person's ATM withdrawal | No | *** ### Do I need to onboard every end-user who instructs a payment? Yes. Every customer who initiates a payment instruction must be onboarded through OMS. You cannot make payments on behalf of customers who have not completed onboarding. * A business that has completed KYB can initiate payments for its own treasury. * A business cannot initiate payments on behalf of its own end-customers unless those end-customers are separately onboarded. This applies regardless of whether the payment is small or routine. *** ### Can I use KYC/KYB I've already collected? For US-resident individual customers, yes, if your organization has already verified them, we can accept that verification through a reliance KYC process rather than requiring re-verification. Your organization must meet enhanced due diligence requirements to use this. For other customer types and geographies, reliance KYC/KYB support is limited or not yet available. Contact your account team for current status. *** ### What information is required for counterparties? Counterparties do not need full KYC/KYB onboarding, but you must provide basic details: | Counterparty type | Required information | | ------------------ | ------------------------------------------------------------- | | All | Legal name, physical address | | Bank account | Account number, routing number | | Blockchain address | Custodian name (self-custodial or name of custodial provider) | *** ### What does "no nested flows" mean? Nested flows, also called "on behalf of" transactions, occur when a payment is instructed by a party that has not themselves been onboarded. OMS does not support nested flows. The entity instructing a payment must be an onboarded customer. If your product involves end-users directing payments, those end-users must be onboarded individually. **Supported:** * An onboarded organization ramps USD to USDC to fund its own operations. * An onboarded business customer initiates a payment they control. **Not supported:** * An onboarded organization initiates a payment on behalf of one of its own customers who is not onboarded to OMS. * An onboarded customer instructs a payment on behalf of their downstream customer. If your use case involves end-user-directed payments, reach out to your account team to discuss onboarding options. *** ### Which countries are blocked? See [Supported Jurisdictions](/payments/supported-jurisdictions-networks) for the full list of sanctioned and restricted jurisdictions, plus details on how restrictions are enforced per transaction. *** ## Building your own compliance stack For teams building custom payment infrastructure outside the OMS, the following providers are used in production by payment applications on Polygon. ### Identity verification (KYC/KYB) | Provider | Details | | ------------------------------------------------------- | ------------------------------------------------------ | | [Sumsub](https://docs.sumsub.com/docs/overview) | Global KYC/KYB, document verification, liveness checks | | [Onfido](https://documentation.onfido.com/) | Document and biometric verification | | [Persona](https://docs.withpersona.com/getting-started) | Modular identity platform, KYC and fraud prevention | ### Transaction monitoring (AML) | Provider | Details | | --------------------------------------------------------------- | ---------------------------------------------- | | [Chainalysis](https://www.chainalysis.com/) | Onchain transaction screening and risk scoring | | [TRM Labs](https://www.trmlabs.com/) | Blockchain risk intelligence and compliance | | [Elliptic](https://developers.elliptic.co/docs/getting-started) | Crypto asset risk management | Polygon does not endorse or provide support for third-party compliance providers. Regulatory requirements vary by jurisdiction. Consult legal counsel for your specific product. # Account model Source: https://docs.polygon.technology/payments/core-concepts/account-model OMS supports custodial, non-custodial, and agentic custody models. This page covers the account types used by the custodial model. OMS supports flexible custody models. Pick the model that fits your product; the rest of the OMS API (transactions, quotes, compliance, on-ramps and off-ramps) works the same across all three. | Model | Key custody | Built for | | ----------------------------------------------- | --------------------------- | ---------------------------------------------------- | | [Custodial](/wallets/custodial-wallets) | OMS holds the keys | Regulated fintech, neobanks, and remittance products | | [Non-custodial](/wallets/non-custodial-wallets) | The user holds the keys | Consumer apps where users control their own funds | | [Agentic](/wallets/agentic-wallets) | The agent holds scoped keys | Autonomous agents with policy-based spending limits | A single product can combine them: custodial for fiat flows and compliance, non-custodial for user-controlled on-chain assets, agentic for autonomous payments. The remainder of this page covers the account types used by the **custodial** model. For non-custodial and agentic wallets, see [OMS wallets](/wallets/overview). ## Custodial wallets The primary balance store. OMS manages private keys, with no end-user signing and no wallet SDKs required. All operations are server-to-server API calls. ``` Wallet ├── id: "wlt_..." ├── object: "wallet" ├── customerId: "cst_..." ├── asset: "usdc" ├── chain: "polygon" ├── address: "0x..." (on-chain address) ├── blockchainAsset: { ... } (protocol / chainId / tokenId) ├── type: "internal" (internal = OMS-managed) └── status: "active" (active → suspended → closed) ``` Each custodial wallet holds a single asset on one chain and has an on-chain `address`. The `type` field is `internal` for OMS-managed wallets. `blockchainAsset` resolves the on-chain identity of the asset (protocol, chain ID, and token ID). **Creating a wallet:** `POST /customers/{customerId}/wallets` with `asset` and `chain`. Returns the created wallet record. **Listing wallets:** `GET /customers/{customerId}/wallets` returns the customer's wallets, one per asset and chain. **Reading a balance:** `GET /wallets/{walletId}/balance` returns the wallet's current `balance` for its asset and chain, along with an estimated value in the requested currency. **Funding a wallet:** Send crypto directly to the wallet's `address`, or use the on-ramp flows (cash-in, virtual accounts, deposit addresses) to convert fiat. **Multiple wallets:** A customer can have more than one wallet, for different currencies, use cases, or segregation needs. At launch, OMS custodial wallets are `internal` (OMS-managed). Embedded wallets, provisioned through the same customer endpoint, are planned for a later release. ## Virtual accounts A bank account number wired to a customer's wallet. Fiat deposits auto-convert to crypto. ``` Virtual Account ├── id: "va_..." ├── customerId: "cst_..." ├── destination: { ... } (the wallet that receives the converted crypto) └── bankDetails: { domestic: { accountNumber, routingNumber, ... }, swift: { accountNumber, bic, memo, ... } } ``` Give your customers the `bankDetails` to display in your UI (null until the underlying deposit account is provisioned). A transfer to that account number delivers crypto to the configured wallet. When funds arrive, OMS creates a `fiatAccountToCrypto` transaction directly in `processing`. Virtual accounts are managed through the API: create with `POST /virtual-accounts`, list with `GET /virtual-accounts`, read and update with `GET`/`PATCH /virtual-accounts/{id}`. Deletion is asynchronous: `DELETE /virtual-accounts/{id}` returns `202` and initiates closure of the underlying deposit account, and the status finalizes to `deleted` once it closes. A sandbox `POST /virtual-accounts/{id}/simulate` endpoint exercises the inbound flow. See [Virtual accounts](/payments/virtual-accounts). ## Deposit addresses A reusable on-chain address wired to a fiat destination. On-chain deposits auto-create a transaction. ``` Deposit Address ├── id: "da_..." ├── customerId: "cst_..." ├── destination: { ... } └── depositInstructions: { address: "0x...", asset: "usdc", network: "polygon" } ``` Deposit addresses carry routing configuration, so any deposit to the inlet address is converted and delivered to the configured fiat destination. They are the pattern for "accept any amount from any sender" on-ramp flows. When funds arrive, OMS creates a `cryptoToFiatAccount` transaction directly in `processing`. Deposit addresses are managed through the API: create with `POST /deposit-addresses` (the `destination` is a registered bank external account), list with `GET /deposit-addresses`, read and update with `GET`/`PATCH /deposit-addresses/{id}`. There is no delete. `depositInstructions` is null until provisioning populates the OMS-owned inlet address, and re-pointing the `destination` to a healthy external account recovers an `inactiveActionRequired` address to `active`. Deposit addresses must be enabled for your project: contact us to enable them. A sandbox `POST /deposit-addresses/{id}/simulate` endpoint exercises the inbound flow. See [Deposit addresses](/payments/deposit-addresses). ## External accounts Off-platform bank accounts, external wallets, and debit cards, referenced on a quote by their ID. A quote's source is always an OMS wallet or a card, so banks are destinations. Each external account is referenced by an ID whose prefix identifies the account type: | Prefix | Type | | --------------- | ---------------------------------------------- | | `ext_card_` | Debit card | | `ext_bankUs_` | US bank account (ACH, same-day ACH, wire, RTP) | | `ext_bankIban_` | IBAN bank account (international, SWIFT) | | `ext_bankCa_` | Canadian bank account | | `ext_wlt_` | External wallet | Register external accounts with `POST /external-accounts`. The body specifies an `owner` (a customer, or one of the customer's counterparties), a `type` (`bankUs`, `bankIban`, `bankCanada`, `card`, or `walletExternal`), and exactly one per-type details object matching the `type`; debit cards register through this same endpoint. List with `GET /external-accounts` (requires `customerId`, with an optional `counterpartyId` filter), update `label` and `metadata` with `PATCH`, and remove with `DELETE`. When delivering to a raw on-chain wallet, a quote can also pass a `blockchainAddress` directly instead of an `ext_wlt_` ID. External accounts are not OMS-managed balances. OMS never holds funds in an external account; it is only a source or destination reference on a transaction. ## Forward-looking Embedded custodial wallets, provisioned alongside the internal wallets available today, are planned for an upcoming release. # Currencies and rails Source: https://docs.polygon.technology/payments/core-concepts/currencies-and-rails Supported assets, networks, and payment rails in OMS. Every side of a transaction specifies an **asset** (what kind of money) and, for most instruments, a **network** (how it moves). Crypto sides carry an `asset` and a `network`; fiat sides carry an `asset` (`usd` or `cad`) and a rail-specific `network`. ## Crypto assets and networks OMS custodial wallets and on-chain instruments hold stablecoins: | Asset | Description | | ------ | ----------- | | `usdc` | USD Coin | | `usdt` | Tether USD | A crypto instrument pairs the asset with a `network` (for example `polygon`). Each side also resolves a `blockchainAsset` object that carries the on-chain identity: the wire-level `protocol` (`evm`, `svm`, or `sui`), the `chainId`, and the `tokenId`. Polygon is the recommended network for most use cases: sub-2-second finality, fractions-of-a-cent transaction cost, and native USDC (no bridging or wrapping). Deposit-address inbound simulation accepts stablecoins (`USDC`, `USDT`) on a defined set of source chains. See [Deposit addresses](/payments/deposit-addresses) for the inbound flow. ## Fiat rails Fiat instruments are identified by a `type` and carry a rail-specific `network`: | Instrument | Asset | Network | Description | | ------------ | ----- | ------------ | -------------------------------------------------- | | `bankUs` | `usd` | `ach` | US dollars via ACH bank transfer | | `bankUs` | `usd` | `achSameDay` | US dollars via same-day ACH | | `bankUs` | `usd` | `wire` | US dollars via domestic wire | | `bankUs` | `usd` | `rtp` | US dollars via real-time payments | | `bankIban` | `usd` | `swift` | US dollars to an IBAN account over SWIFT | | `bankCanada` | `usd` | `swift` | US dollars to a Canadian account over SWIFT | | `bankCanada` | `cad` | `local` | Canadian dollars over local rails | | `card` | `usd` | `card` | US dollars via [debit card](/payments/debit-cards) | | `cash` | `usd` | `cash` | US dollars via physical cash at retail locations | Card rails accept a `settlementType` on the quote: `internal` (OMS custodies the crypto) or `external` (an on-chain wallet). A card buy defaults to `external`; a card sell defaults to `internal`. The field is ignored for non-card rails. Which rails a customer can use depends on the endorsements granted to that customer (`basic`, `cryptoCustody`, and `usd`). Endorsements are granted after verification and delivered via webhook. See [Entities and relationships](/payments/core-concepts/entities) for the endorsement model. ## Exchange rates and FX USD and its stablecoins convert by issuer redemption: USD to USDC or USDT is redeemed 1:1 with the issuer, so there is no market, exchange, or central-bank reference rate and no FX spread. What a quote locks is the fee schedule, not a fluctuating rate. The quote's `pricing` object expresses this: * `pricing.exchangeRate`: units of the destination asset per unit of the source asset. * `pricing.effectiveRate`: the all-in rate the customer receives after fees. * `pricing.fixedAmountSide`: which side of the trade the amount was specified on. * `expiresAt`: when the locked pricing expires. The per-side `feesDeducted` breakdown reports OMS, developer, and gas fees separately. ## Settlement times Typical production times from initiating a payment to the confirmation event. Inbound confirmations fire when the bank confirms receipt; the same timing applies to payouts. | Rail | Typical settlement | | --------------- | ---------------------------------------------------- | | ACH | Next business day (morning ET of the effective date) | | Same-Day ACH | Same business day | | Fedwire | About an hour, within the Fedwire window | | RTP | Instant, 24/7 | | SWIFT | 1 to 2 business days | | Polygon, Solana | About a minute | | Ethereum, Base | About 5 minutes | Amounts above bank-internal thresholds can trigger a compliance review before release: a hold, not a rejection. Execution is never force-split across payments. ## Cash networks OMS supports cash deposits and withdrawals through a partner retail network. A cash-in generates a code the customer presents at a retail location; OMS monitors for the deposit and auto-creates a `cashToCrypto` transaction when cash is received. Cash payouts (`cryptoToCash`) are delivered as a pickup: the amount must be a multiple of 20.00, up to a maximum of 400.00 per transaction. Query available locations with `GET /cash-locations`, passing the `provider`, the customer's `latitude` and `longitude`, and a `flow` of `cash_in` or `cash_out`. See the [Cash-ins and cash-outs](/payments/cash-ins-and-cash-outs) page for the full flow. ## Forward-looking Reference endpoints that return the live, authoritative asset, network, and rail catalog (along with per-asset limits and gas-sponsorship support) and a project configuration endpoint are planned for an upcoming release. Until then, the tables above summarize what the canonical API supports. # Entities and relationships Source: https://docs.polygon.technology/payments/core-concepts/entities The core resources in OMS and how they relate to each other. Every OMS integration is built from a small set of resources. The API-managed resources are Customers, Wallets (custodial), Counterparties, External Accounts, Virtual Accounts, Deposit Addresses, Quotes, Transactions, Cash-ins, and Webhooks. Understanding the relationships between them is the fastest way to design a correct integration. ## The entity model ``` Customer ├── Wallets (custodial stablecoin balances, created and listed under the customer) ├── Counterparties (people and businesses the customer transacts with) ├── Virtual Accounts (persistent bank account numbers) ├── Deposit Addresses (persistent onchain addresses) └── External Accounts (registered bank accounts, debit cards, and external wallets, owned by the customer or one of its counterparties) Quote → Transaction └── sourceToDestination inferred from the instruments (cryptoToCrypto, cryptoToFiatAccount, cryptoToCash, fiatAccountToCrypto, cashToCrypto) Cash-in (code-based cash deposit → auto-creates Transaction) Webhooks (API-managed subscriptions, full CRUD) ``` All financial operations belong to a `Customer`. A customer has zero or more wallets. Counterparties, virtual accounts, deposit addresses, quotes, and transactions are also scoped to a customer; external accounts belong to a customer or to one of its counterparties. Webhook subscriptions are managed through the API independently of any customer. ## Customers A customer is an identity record. Before a customer can move money, OMS must grant **endorsements** that unlock specific capabilities. | Endorsement | Unlocks | | --------------- | ------------------------------------ | | `basic` | Standard operations | | `cryptoCustody` | Crypto custody and advanced features | | `usd` | USD stablecoin operations | Customers are created via `POST /customers` with a `type` (for example `individual`) and an `endorsements` array. Endorsements are granted after KYC/KYB verification; a webhook fires as they change. ## Wallets The Payments API `wallet` resource is custodial: OMS manages the keys, and they never leave OMS infrastructure. Wallets hold stablecoin balances (USDC by default) and are the source or destination for all transactions. Wallets are created and listed under the customer: `POST /customers/{id}/wallets` (body specifies `asset` and `chain`) and `GET /customers/{id}/wallets`. Each wallet holds a single asset on one chain and has an onchain address. `GET /wallets/{id}/balance` returns that wallet's current `balance` for its asset and chain. For user-controlled keys (onchain consumer apps, agentic flows), use [OMS non-custodial wallets](/wallets/non-custodial-wallets). The two models can be combined. ## Virtual accounts A dedicated bank account number assigned to a customer. When fiat arrives via a supported bank rail, OMS automatically creates a `fiatAccountToCrypto` transaction and delivers USDC to the customer's wallet. Create one with `POST /virtual-accounts`; the body specifies the `customerId`, the `source` asset and network, a `destination` (an OMS wallet, or a registered external wallet by ID), the `accountHolder` (always `customer`), and the account `type` (`bankUs`). The response's `bankDetails` is null until the underlying deposit account is provisioned. Virtual accounts support list, get, and update (`GET /virtual-accounts`, `GET`/`PATCH /virtual-accounts/{id}`). Deletion is asynchronous: `DELETE /virtual-accounts/{id}` returns `202`, and the status finalizes to `deleted` once the underlying account closes. ## Deposit addresses A persistent onchain address. When crypto arrives, OMS automatically creates and executes a transaction that delivers fiat to a registered bank external account. Create one with `POST /deposit-addresses`; the body specifies the `customerId`, the expected source asset (`usdc` or `usdt`) and network, and a registered bank external account as the `destination`. The response's `depositInstructions` is null until provisioning populates the OMS-owned inlet address. Deposit addresses support list, get, and update (`GET /deposit-addresses`, `GET`/`PATCH /deposit-addresses/{id}`); there is no delete. Deposit addresses must be enabled for your project: contact us to enable them. ## Counterparties A person or business a customer transacts with, scoped to that customer. Create one with `POST /counterparties`; the body requires `customerId` and `name`, with optional identity fields (`entityType`, `email`, `phone`, `dateOfBirth`, `taxId`, `nationality`, `address`, `metadata`). The `address` object takes `streetAddress`, `city`, `postalCode`, and `country`, plus an optional `countryArea`. Counterparties support full CRUD. Listing requires the `customerId` query parameter (`GET /counterparties?customerId=...`). A counterparty can own external accounts; `DELETE /counterparties/{id}` returns `409` while the counterparty still owns active external accounts. ## External accounts A registered bank account, debit card, or external wallet, owned by a customer or by one of the customer's counterparties. External accounts are referenced by ID as the source for on-ramps or the destination for off-ramps. Register one with `POST /external-accounts`. The body specifies the `owner` (`{ kind: "customer", customerId }` or `{ kind: "counterparty", counterpartyId }`), a `type` (`bankUs`, `bankIban`, `bankCanada`, `card`, or `walletExternal`), and exactly one per-type details object matching the `type`. Debit cards register through this same endpoint. `GET /external-accounts` requires a `customerId` and accepts an optional `counterpartyId` filter; `PATCH` updates `label` and `metadata`; `DELETE` removes the registration. ID prefixes: `ext_card_` (debit cards), `ext_bankUs_` (US banks), `ext_bankIban_` (IBAN banks), `ext_bankCa_` (Canadian banks), `ext_wlt_` (external wallets). ## Quotes and transactions The two-step flow for most money movement: 1. `POST /quotes`: lock pricing, return full fee breakdown 2. `POST /transactions`: execute against the locked quote Quotes expire if not used within the validity window. `pricing.fixedAmountSide` indicates which side of the trade was specified. ## Cash-ins A code-based deposit flow for in-person cash at retail locations. Creates a deposit code the customer presents at a partner location. OMS converts the deposited cash to USDC and auto-creates a transaction. No upfront amount is required. ## Webhooks API-managed event subscriptions with full CRUD. Register a subscription with `POST /webhooks` (body `{ url, events }`) and OMS delivers events as state changes occur; each delivery carries the full object so your handler branches on its `status`. Subscribe to specific event types, or pass an empty `events` array (or `["*"]`) to receive all of them. Subscriptions are created, listed, updated, and deleted through the API (`GET`/`PATCH`/`DELETE /webhooks/{id}`) as well as in the dashboard. The create response returns a signing secret once for verifying the `Webhook-Signature` header. Virtual accounts, deposit addresses, and cash-ins all bypass the quote step. They go directly to `processing` because OMS cannot lock pricing before the funds arrive. # Quote system Source: https://docs.polygon.technology/payments/core-concepts/quote-system How OMS locks pricing, structures fees, and calculates exchange rates. Every transaction that involves a currency conversion or a fiat rail requires a quote. A quote locks the exchange rate and fee structure for a short window, giving you a guaranteed price before committing to execution. ## Quote lifecycle ``` POST /quotes → open └── POST /transactions → accepted └── (time passes) → expired ``` | Status | Meaning | | ---------- | ----------------------------------------------- | | `open` | Rate locked. Awaiting transaction creation. | | `accepted` | A transaction has been created from this quote. | | `expired` | Pricing window closed. Create a new quote. | ## What a quote contains A quote response includes: * **`source`**: the source instrument (a typed side identifying the wallet, bank account, or card being pulled from) * **`destination`**: the destination instrument (typed the same way, identifying where funds are delivered) * **`pricing`**: consolidated economics for both sides, the rate pair, and gas sponsorship (see below) * **`sourceToDestination`**: a composite corridor tag such as `cryptoToCrypto`, `cryptoToCash`, `cryptoToFiatAccount`, `cashToCrypto`, or `fiatAccountToCrypto` * **`expiresAt`**: when the rate lock expires ## Fee structure Economics live in a single top-level `pricing` object. Each side under `pricing.source` and `pricing.destination` carries the same shape: ```json theme={null} { "pricing": { "source": { "asset": "usd", "amountGross": "100.00", "amountNet": "99.42", "feesDeducted": { "total": "0.58", "developer": "0.20", "oms": "0.35", "gas": "0.03" } }, "destination": { "asset": "usdc" }, "pair": "usd/usdc", "exchangeRate": "1.0", "effectiveRate": "0.9942", "fixedAmountSide": "source", "sponsorGas": false, "sponsorGasCost": "0" } } ``` The core equation: `pricing.source.amountNet × pricing.exchangeRate = pricing.destination.amountGross`. **Developer fees** are configurable per integration. Set them on your OMS account or pass them in the quote request. OMS never shows your fee margin to the end user. ## Gas sponsorship Set `sponsorGas: true` on the quote request to cover network gas for your users. Gas costs move out of the transaction fee breakdown and into `pricing.sponsorGasCost` on your account, a separate, out-of-band developer cost. This is the standard pattern for custodial wallets where users should not be aware of blockchain mechanics. ## Fixed-amount quoting You can fix either side via `pricing.fixedAmountSide`: * `pricing.fixedAmountSide: "source"`: user sends an exact amount, destination is calculated * `pricing.fixedAmountSide: "destination"`: user receives an exact amount, source is calculated This maps cleanly to common UX patterns: "I want to send \$100" vs. "I want my recipient to receive exactly \$100." ## When quotes are not required Auto-created transactions from deposit addresses, virtual accounts, and cash-ins skip the quote step. OMS uses live pricing at the moment funds arrive. The same fee structure applies, but pricing is not locked in advance. # Transaction lifecycle Source: https://docs.polygon.technology/payments/core-concepts/transaction-lifecycle How a transaction moves through OMS from creation to completion, including statuses, sub-statuses, and webhook events. Every transaction in OMS moves through a predictable set of statuses. The top-level `status` is designed for programmatic branching. The optional `subStatus` provides operational detail without complicating your core logic. ## Direction Every transaction carries a `sourceToDestination` field: a composite of the source and destination instrument categories. | Value | What it does | | --------------------- | ---------------------------------------------------------------- | | `cryptoToCrypto` | USDC moves from one wallet to another, or to an external address | | `cryptoToFiatAccount` | USDC out of a wallet, fiat delivered to a bank account | | `cryptoToCash` | USDC out of a wallet, delivered as a cash pickup | | `fiatAccountToCrypto` | Fiat in from a bank account, USDC delivered to a wallet | | `cashToCrypto` | Cash in from a retail deposit, USDC delivered to a wallet | OMS infers the direction from the source and destination instruments. You do not set it explicitly. ## Status model ``` processing (initial state; the quote is already accepted) │ ├──► completed (standard flows) │ ├──► awaitingAction (non-terminal; blocked on developer, upstream, or compliance action) │ └──► processing (once the hold clears) │ └──► failed ``` The four top-level statuses are `processing`, `awaitingAction`, `completed`, and `failed`. A transaction begins at `processing` the moment it is created from a quote. `awaitingAction` is a non-terminal hold state; the `hold` object explains the reason and carries a deadline, and the transaction returns to `processing` once cleared. The `hold` object is discriminated by its `type` field: | `hold.type` | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `senderAttribution` | A deposit address received crypto from a sender not linked to a counterparty. Carries the inbound `txHash` and `matchableExternalAccountCriteria`; registering a matching `walletExternal` external account releases the hold. | | `depositAddressFrozen` | The deposit address is frozen. | | `depositAddressInactive` | The deposit address's destination external account became unusable. The `cause` object identifies the external account, its status, and the reason it became invalid. | Each hold carries `since`, an optional `deadline`, and `resolvedAt` once cleared. **Branch on `status` only.** The optional `subStatus` is a status-scoped string carrying operational detail namespaced by its parent status (for example `processing.fundsPulled`, `processing.cashPickupReady`, or `completed.cashPickupCollected`). Cash off-ramp lifecycle events surface here rather than as top-level statuses, and payout execution does too: `processing.awaitingCryptoOut`, `processing.cryptoOut`, `processing.awaitingFiatOut`, and `processing.fiatOut` track the crypto and fiat out-legs of a payout. Use `subStatus` for display and logging, not for control flow. ## Webhook events OMS fires a webhook on every meaningful state change. Each delivery carries the full transaction object, so your handler reads the `status` (and, if needed, `subStatus`) directly off the payload. Polling `GET /transactions/{id}` is rarely necessary once webhooks are configured. Subscribe to events through the Webhooks endpoints or the OMS Dashboard. A subscription is an endpoint URL plus the list of event types you want: ``` POST /webhooks { "url": "https://your-app.example.com/oms/webhooks", "events": [] } ``` An empty `events` array (or `["*"]`) subscribes to every event type. The Webhooks endpoints support full CRUD (`GET`/`PATCH`/`DELETE /webhooks/{id}`), and the create response returns a signing secret once, which you use to verify the `Webhook-Signature` header on incoming deliveries. Transaction events are namespaced by transfer type: `transaction.fiatToCrypto.*`, `transaction.cryptoToFiat.*`, and `transaction.cryptoToCrypto.*`, each with `processing`, `completed`, `failed`, and refund variants (plus `cashPickupReady` and `codeExpired` on the cash payout path). See the [webhook events catalog](/api-reference/webhook-events) for every event type and the delivery envelope. ## Auto-created transactions Deposit addresses, virtual accounts, and cash-ins bypass the quote step. OMS creates the transaction internally when funds arrive and moves it directly to `processing`. The same status model and webhook events apply. ## Idempotency All `POST` endpoints accept an `Idempotency-Key` header. Use a stable key (UUID tied to your internal order ID) to safely retry on network failure. OMS returns the same response for any subsequent request with the same key within the idempotency window. Set `Idempotency-Key` on every write request in production. It protects against double-execution without any coordination on your side. # Customers Source: https://docs.polygon.technology/payments/customers Identity records that own wallets and transactions in OMS. A customer is the top-level identity record in OMS. Every wallet belongs to a customer, and so does every transaction. No financial operation can happen without one. ## Object ```json theme={null} { "id": "cst_...", "object": "customer", "type": "individual", "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "phone": "+12125551234", "nationality": "US", "externalId": "usr_12345", "status": "active", "signedAgreement": true, "signedAgreementAt": "2026-03-20T14:15:22Z", "wallets": [ { "id": "wlt_...", "type": "custodial", "address": "0x7B3a...D9b1", "network": "polygon", "asset": "usdc", "balance": "1234.56", "estimatedValueUsd": "1234.56" } ], "endorsements": [ { "name": "basic", "status": "ACTIVE" }, { "name": "cryptoCustody", "status": "ACTIVE" }, { "name": "usd", "status": "PENDING" } ], "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" } ``` `type` is `individual`, the only type supported today. `status` is `active` or `inactive`; an inactive customer cannot create new transactions. There are no intermediate customer states: all compliance granularity lives in the endorsement statuses. PII fields you send on create or update (`birthDate`, `residentialAddress`, `ipAddress`, `identifyingInformation`) are write-only. OMS accepts them but never returns them. The `wallets` array is a simplified flat view, one entry per wallet-asset combination. Read the full wallet representation with `GET /wallets/{id}/balance`. See [Wallets](/payments/wallets) for the wallet resource. ## Create a customer `POST /customers`. The only required field is `type`; everything else is optional and can be added later with `PATCH /customers/{customerId}`. ```json theme={null} { "type": "individual", "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "phone": "+12125551234", "nationality": "US", "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" } ], "endorsements": ["cryptoCustody", "usd"] } ``` Create the customer record first, then collect the identifying fields for your KYC flow and submit them on create or with a later update. OMS handles compliance screening; your application handles identity collection. In sandbox, endorsements are auto-approved. ## Endorsements Endorsements track KYC and compliance status. Each unlocks a set of operations, and each carries its own status so you can see exactly where a customer stands. | Endorsement | Unlocks | | --------------- | ---------------------------------------------------------------------------------- | | `basic` | Baseline identity checks. Auto-included whenever another endorsement is requested. | | `cryptoCustody` | Custodial stablecoin wallets. | | `usd` | USD fiat rails, including bank and cash payouts. | Request endorsements in the `endorsements` array on create or update. If you omit the array on create, OMS defaults to `cryptoCustody` and `usd`, which auto-includes `basic`. On update, listing endorsement names adds them and triggers re-evaluation; existing endorsements are never removed. Each endorsement in the response carries a `status`, and where relevant a `requirements` object (fields that are `complete`, `pending`, `missing`, or have `issues`) and `rejectionReasons`. | Endorsement status | Meaning | | ------------------ | -------------------------------------------------- | | `INACTIVE` | Not yet requested or not started. | | `PENDING` | Under compliance review. | | `ISSUES` | Requires attention; see `requirements.issues`. | | `ACTIVE` | Approved and usable. | | `REJECTED` | Denied; see `rejectionReasons`. | | `REVOKED_ISSUES` | Previously active, now revoked pending resolution. | | `OFFBOARDED` | Terminated. | ## Balance `GET /customers/{id}/balance` returns the customer's estimated total balance aggregated across all of their wallets and assets. Pass `estimatedBalanceCurrencyCode` to value the aggregate in a currency other than USD. ```json theme={null} { "data": { "customerId": "cst_...", "estimatedBalanceValue": "1234.56", "estimatedBalanceCurrencyCode": "USD", "updatedAt": "2026-01-15T10:30:00Z" } } ``` ## Key operations | Operation | Endpoint | | ---------------------- | -------------------------------- | | Create a customer | `POST /customers` | | List customers | `GET /customers` | | Retrieve a customer | `GET /customers/{customerId}` | | Update a customer | `PATCH /customers/{customerId}` | | Delete a customer | `DELETE /customers/{customerId}` | | Get aggregated balance | `GET /customers/{id}/balance` | | List wallets | `GET /customers/{id}/wallets` | | Create a wallet | `POST /customers/{id}/wallets` | `POST`, `PATCH`, and `DELETE` requests accept an `Idempotency-Key` header. Use a stable key tied to your internal user ID to safely retry on network failure. ## Related * [Customer onboarding guide](/payments/guides/customer-onboarding): full walkthrough including KYC flow and endorsement handling * [Wallets](/payments/wallets): provision wallets for a customer * [Compliance](/payments/compliance): KYC/KYB requirements by corridor # Debit cards Source: https://docs.polygon.technology/payments/debit-cards Use a debit card as an on-ramp funding source or an off-ramp payout destination in OMS. A debit card connects to OMS in two directions. As a **funding source**, a card payment converts to crypto and tops up a customer's wallet (an on-ramp). As a **payout destination**, OMS pushes a wallet balance out to a cardholder's debit card (an off-ramp). Both run through the standard quote-and-transaction flow; only the instrument changes. Card support requires the `usd` endorsement on the customer. A card is referenced by its `ext_card_` external-account ID and always requires `asset: usd`; its network is inferred server-side. Register a card once with `POST /external-accounts`, then reference the returned `ext_card_` ID on quotes. See [Registering a card](#registering-a-card). ## Card as a funding source Reference the card as the quote `source` (pull-from-card). The customer pays with their card and OMS delivers crypto to the destination wallet. ```json theme={null} { "customerId": "cst_...", "source": { "type": "card", "details": { "id": "ext_card_...", "asset": "usd" }, "amount": "100.00" }, "destination": { "type": "walletOms", "details": { "id": "wlt_...", "asset": "usdc", "network": "polygon" } } } ``` ## Card as a payout destination Reference the card as the quote `destination` (push-to-card). OMS pushes the payout from an OMS wallet source to the card. ```json theme={null} { "customerId": "cst_...", "source": { "type": "walletOms", "details": { "id": "wlt_...", "asset": "usdc", "network": "polygon" }, "amount": "100.00" }, "destination": { "type": "card", "details": { "id": "ext_card_...", "asset": "usd" } } } ``` ## Settlement type The card rail accepts an optional `settlementType` on the quote request: * `internal`: OMS custodies the crypto. * `external`: the crypto settles to an on-chain wallet. The default depends on direction: a card buy (pull-from-card) defaults to `external`, and a card sell (push-to-card) defaults to `internal`. The field is ignored for non-card rails. ## Registering a card Register a card with `POST /external-accounts`, using `type: "card"` and the `card` detail object. Cards use the same registration endpoint as every other external account type: ```json theme={null} { "owner": { "kind": "customer", "customerId": "cst_..." }, "type": "card", "card": { "cardNumber": "4111111111111111", "cvv": "123", "expiryMonth": 4, "expiryYear": 2028, "cardProvider": "visa", "billingAddress": { "addressLine1": "123 Main St", "city": "Austin", "state": "TX", "zipCode": "78701", "country": "US" } } } ``` | Field | Required | Meaning | | --------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cardNumber` | Yes | The card number. Write-only; never returned or stored. | | `cvv` | Yes | The card verification value. Write-only; never returned or stored. | | `expiryMonth`, `expiryYear` | Yes | Expiry as integers (for example, `4` and `2028`). | | `billingAddress` | Yes | The billing address the card issuer has on file, used for AVS and fraud screening. `addressLine1`, `city`, `state`, `zipCode`, and `country` (ISO 3166-1 alpha-2) are required; `addressLine2`, `firstName`, and `lastName` are optional. | | `cardProvider` | No | The card network: `visa`, `mastercard`, `amex`, or `discover`. | | `webSessionId` | No | A fraud-screening web session identifier, forwarded to the card processor. | The billing address is a property of the card, not the customer's KYC identity: a customer may register cards with differing billing addresses, so supply it per card rather than deriving it from the customer record. The response returns only `cardNumberLast4`, `cardProvider`, `expiryMonth`, and `expiryYear`; the full card number and CVV are never stored or echoed back. A card starts `pending` and flips to `active` (usable on quotes) or `failed`. See [External accounts](/payments/external-accounts) for the shared registration and lifecycle model. ## Related * [Currencies and rails](/payments/core-concepts/currencies-and-rails): the full list of supported assets and rails * [External accounts](/payments/external-accounts): the shared model for cards, banks, and external wallets * [Transactions](/payments/transactions): the quote-and-execute flow for card and bank transfers # Deposit addresses Source: https://docs.polygon.technology/payments/deposit-addresses Reusable on-chain addresses that auto-convert incoming crypto and pay out to a bank account. A deposit address is a reusable on-chain address assigned to a customer. When crypto arrives at the address, OMS automatically creates and executes a transaction that converts it and pays out to a configured fiat destination (`cryptoToFiatAccount`). No developer action is required after the address is provisioned. Deposit addresses are persistent. OMS keeps them active until they are frozen or closed. Deposit addresses must be enabled for your project before you can create them, and the customer must be provisioned for them. To get set up, share your use case below. Tell us about your on-ramp flow and we'll enable deposit addresses for your project. ## How it works Once a deposit address is provisioned, you display its on-chain address to the customer. When a supported stablecoin arrives at that address, OMS: 1. Detects the inbound transfer on the source chain. 2. Creates a transaction with a typed `precursor` of `depositAddress`, carrying the `depositAddressId` and its deposit instructions. 3. Moves the transaction directly to `processing` (there is no quote step, because pricing cannot be locked before the funds arrive). 4. Converts the incoming crypto and delivers it to the configured fiat destination. The resulting transaction carries `sourceToDestination: "cryptoToFiatAccount"` and follows the standard [transaction lifecycle](/payments/core-concepts/transaction-lifecycle), including webhook events. ## Creating a deposit address Create a deposit address with `POST /deposit-addresses`: ```json theme={null} { "customerId": "cst_...", "expectedSourceAsset": "usdc", "expectedSourceNetwork": "ethereum", "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_...", "asset": "usd", "network": "ach", "accountHolder": "customer" } }, "label": "Inbound USDC deposits" } ``` | Field | Required | Meaning | | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `customerId` | Yes | The customer the address belongs to. | | `expectedSourceAsset` | Yes | The inbound stablecoin the address expects: `usdc` or `usdt` (lowercase). | | `expectedSourceNetwork` | Yes | The chain the address accepts funds on (for example, `ethereum` or `base`). | | `destination` | Yes | A registered bank-type [external account](/payments/external-accounts) (`bankUs`, `bankIban`, or `bankCanada`) on the standard side shape. OMS validates the `details` (asset, network, and `accountHolder`) against the resolved external account. | | `sponsorGas` | No | When `true` (the default), OMS absorbs the on-chain gas cost for the destination delivery. Only `true` is currently supported. | | `label`, `metadata` | No | A display label and free-form string metadata. | The `201` response returns the deposit address with `depositInstructions: null`. Provisioning populates the OMS-owned inlet address asynchronously, and the address moves from `pending` to `active` once the instructions are ready. ## Deposit instructions The `depositInstructions` field carries the details to display in your UI. It is null until provisioning completes: ```json theme={null} { "address": "0xabc123...", "asset": "usdc", "network": "ethereum", "expiresAt": null } ``` | Field | Meaning | | ----------- | ------------------------------------------------------------------------------------------ | | `address` | The OMS-owned on-chain inlet address for this deposit address. Give this to your customer. | | `asset` | The stablecoin the address accepts (matches `expectedSourceAsset`). | | `network` | The chain the address accepts funds on (matches `expectedSourceNetwork`). | | `expiresAt` | Reserved for a future provider-imposed inlet expiry. Null today. | Crypto sent to `address` is converted and paid out to the configured fiat destination. ## Listing and retrieving `GET /deposit-addresses` lists deposit addresses across every customer in your organization. Both filters are optional: `customerId` scopes the list to one customer, and `status` to one lifecycle state. Paginate with `limit`, `startingAfter`, and `endingBefore`; each page returns `nextCursor`, `previousCursor`, and `hasMore`. `GET /deposit-addresses/{depositAddressId}` fetches a single deposit address by ID. ## Updating `PATCH /deposit-addresses/{depositAddressId}` accepts `destination` (re-point to a different bank-type external account), `label`, `metadata`, and `sponsorGas`; any other key in the body is rejected with `400`. Re-pointing `destination` to a healthy bank external account recovers a deposit address from `inactiveActionRequired` back to `active`. A re-point on an already `active` address updates the target without a status transition. There is no delete endpoint for deposit addresses. ## Statuses | Status | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | `pending` | Created; OMS is provisioning the on-chain inlet address. `depositInstructions` is null. | | `active` | Provisioned and accepting deposits. | | `frozen` | Temporarily suspended; inbound deposits are not processed. | | `closed` | Permanently closed. | | `failed` | Provisioning failed; `failureReason` identifies the cause. | | `inactiveActionRequired` | The destination external account is no longer usable. Re-point `destination` to a healthy account to recover to `active`. | ## Simulating inbound deposits In sandbox, you can simulate an inbound transfer to test your webhook and reconciliation flows without moving real funds. This endpoint is available in non-production environments only and returns `404` in production. `POST /deposit-addresses/{depositAddressId}/simulate` with the amount to simulate: ```json theme={null} { "amount": { "value": "5000" } } ``` The `value` is the stablecoin amount in cents (greater than 0 and at most 100000). The currency and network are resolved server-side from the deposit address. The response echoes the simulated deposit and returns a synthetic `transactionHash` you can correlate against the webhook: ```json theme={null} { "depositAddressId": "da_...", "amount": { "value": "5000", "currency": "usdc" }, "network": { "...": "..." }, "transactionHash": "0x...", "status": "submitted", "submittedAt": "2026-01-15T10:32:00Z" } ``` Use the simulate endpoint to exercise the full auto-created transaction path in sandbox: the simulated inbound funds create a `cryptoToFiatAccount` transaction just as a real deposit would. ## Deposit address vs. virtual account Both are persistent auto-route configurations. The difference is which side is fiat: | | Deposit address | Virtual account | | --------------- | ---------------------- | ------------------------------- | | Incoming funds | Crypto (on-chain) | Fiat (bank rail) | | Deposit details | On-chain inlet address | Bank account number and routing | | Direction | `cryptoToFiatAccount` | `fiatAccountToCrypto` | | Destination | Fiat account | Crypto wallet | ## Related * [Deposit addresses guide](/payments/guides/deposit-addresses): walkthrough of the inbound flow and webhook handling * [Virtual accounts](/payments/virtual-accounts): the fiat equivalent, a bank account number that auto-converts to crypto * [External accounts](/payments/external-accounts): registering the bank account a deposit address pays out to * [Transaction lifecycle](/payments/core-concepts/transaction-lifecycle): statuses, sub-statuses, and webhook events for the auto-created transaction # External accounts Source: https://docs.polygon.technology/payments/external-accounts Off-platform bank accounts, external wallets, and debit cards, referenced by ID on a quote. External accounts are off-platform funding and payout endpoints: bank accounts, external wallets, and debit cards. Each is referenced by ID on a quote side. OMS never holds funds in an external account; it is only a source or destination reference on a transaction. A quote's source is always an OMS wallet or a card, so banks are always destinations. Cards can be either a source (pull-from-card) or a destination (push-to-card). External wallets are crypto destinations. An external account belongs either to a customer directly or to one of the customer's counterparties (a saved third-party payee). You register accounts with `POST /external-accounts` and reference the returned `ext_` ID on quotes. ## Supported types Each external account carries a type-specific ID prefix. The prefix tells you which instrument type to select on the quote side. | ID prefix | Type | Role on a quote | | --------------- | ---------------------------------------------- | --------------------- | | `ext_bankUs_` | US bank account (ACH, same-day ACH, wire, RTP) | Destination | | `ext_bankIban_` | IBAN bank account (international, SWIFT) | Destination | | `ext_bankCa_` | Canadian bank account | Destination | | `ext_wlt_` | External wallet (held outside OMS) | Destination | | `ext_card_` | Debit card | Source or destination | ## Referencing an external account in a transaction Reference the external account ID in the quote's `destination` (or `source`, for cards). The instrument is typed by `type`, and the `id` in `details` carries the `ext_` prefix. ### US bank destination ```json theme={null} { "customerId": "cst_...", "source": { "type": "walletOms", "details": { "id": "wlt_...", "asset": "usdc", "network": "polygon" }, "amount": "500.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } } ``` The US bank `network` is one of `ach`, `achSameDay`, `wire`, or `rtp`. The `accountHolder` field is always `customer`. IBAN (`bankIban`) and Canadian (`bankCanada`) destinations follow the same pattern with their respective `ext_` IDs. ### External wallet destination ```json theme={null} { "destination": { "type": "walletExternal", "details": { "id": "ext_wlt_...", "asset": "usdc", "network": "polygon" } } } ``` For an external wallet on a quote you supply exactly one of `id` (a registered external account, `ext_wlt_` prefix) or `blockchainAddress` (a raw on-chain address). ### Card source or destination ```json theme={null} { "source": { "type": "card", "details": { "id": "ext_card_...", "asset": "usd" }, "amount": "100.00" } } ``` A card requires `asset: usd`; its network is inferred server-side. Reference it as the `source` to pull from a card or as the `destination` to push to one. See [Debit cards](/payments/debit-cards) for both directions. ## Registering external accounts Register external accounts with `POST /external-accounts`. The body carries an `owner`, a `type`, and exactly one per-type detail object whose key matches `type`; a mismatch is rejected with `422`. Optional `label` and `metadata` fields round out the request. The `owner` selects who the account belongs to: * `{ "kind": "customer", "customerId": "cst_..." }` for an account owned by the customer. * `{ "kind": "counterparty", "counterpartyId": "ctp_..." }` for an address-book entry owned by one of the customer's counterparties. ```json US bank theme={null} { "owner": { "kind": "customer", "customerId": "cst_..." }, "type": "bankUs", "bankUs": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "label": "Primary checking" } ``` ```json IBAN bank theme={null} { "owner": { "kind": "counterparty", "counterpartyId": "ctp_..." }, "type": "bankIban", "bankIban": { "iban": "DE89370400440532013000", "BIC": "COBADEFFXXX" } } ``` ```json Canadian bank theme={null} { "owner": { "kind": "customer", "customerId": "cst_..." }, "type": "bankCanada", "bankCanada": { "institutionNumber": "003", "transitNumber": "12345", "accountNumber": "1234567" } } ``` ```json Debit card theme={null} { "owner": { "kind": "customer", "customerId": "cst_..." }, "type": "card", "card": { "cardNumber": "4111111111111111", "cvv": "123", "expiryMonth": 4, "expiryYear": 2028, "cardProvider": "visa", "billingAddress": { "addressLine1": "123 Main St", "city": "Austin", "state": "TX", "zipCode": "78701", "country": "US" } } } ``` ```json External wallet theme={null} { "owner": { "kind": "customer", "customerId": "cst_..." }, "type": "walletExternal", "walletExternal": { "blockchainAddress": "0xabc123...", "networkFamily": "evm", "custodian": "COINBASE_US" } } ``` Per-type required and optional fields: | Type | Required | Optional | | ---------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `bankUs` | `accountNumber`, `routingNumber` (nine-digit ABA) | `accountType` (`checking` or `savings`), `bankName` | | `bankIban` | `iban`, `BIC` (8 or 11 characters) | `countryCode` (derived from the IBAN's first two letters when omitted), `bankAddress` | | `bankCanada` | `institutionNumber` (three digits), `transitNumber` (five digits), `accountNumber` | `bankName` | | `card` | `cardNumber`, `cvv`, `expiryMonth` (integer), `expiryYear` (integer), `billingAddress` | `cardProvider` (`visa`, `mastercard`, `amex`, or `discover`), `webSessionId` (fraud-screening session ID) | | `walletExternal` | `blockchainAddress`, `networkFamily` (`evm` or `solana`), `custodian` | `otherCustodian` (required when `custodian` is `OTHER`) | An external wallet is registered per network family: an EVM address registered once is valid across every EVM chain. See [Debit cards](/payments/debit-cards#registering-a-card) for the card-specific details. Provisioning runs synchronously against the configured payment provider: the account starts `pending` and flips to `active` or `failed`. ### Response Sensitive fields are write-only. Full account numbers, IBANs, card numbers, and CVVs are never stored on reads or echoed back; the response carries only their last-four renderings (`accountNumberLast4`, `ibanLast4`, `cardNumberLast4`). Exactly one per-type response object is populated, selected by `type`: ```json theme={null} { "id": "ext_bankUs_...", "object": "externalAccount", "owner": { "kind": "customer", "customerId": "cst_..." }, "type": "bankUs", "category": "fiatAccount", "status": "pending", "bankUs": { "accountNumberLast4": "9012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "label": "Primary checking", "createdAt": "2026-07-07T10:30:00Z", "updatedAt": "2026-07-07T10:30:00Z" } ``` ### Released sender-attribution holds When you register a `walletExternal` account whose address matches inbound transfers held for sender attribution, the create response includes `resolvedTransactions`: the IDs of the transactions the registration submitted for release. Attribution is asynchronous; each transaction moves from `awaitingAction.awaitingSenderAttribution` to `processing` once the provider confirms settlement, so an immediate `GET` on one of the IDs may still show `awaitingAction`. The field appears only on the `POST` create response, never on reads. ## Managing external accounts * `GET /external-accounts` lists a customer's external accounts. `customerId` is required; `counterpartyId` optionally narrows the list to one counterparty's accounts. Paginate with `limit`, `startingAfter`, and `endingBefore`. * `GET /external-accounts/{externalAccountId}` fetches a single account by ID. * `PATCH /external-accounts/{externalAccountId}` updates `label` and `metadata` only. Financial and identity fields are immutable; to change a destination, register a new account and re-point to it. Any other key in the body is rejected with `400`. * `DELETE /external-accounts/{externalAccountId}` soft-deletes the account and returns `204`. ## Statuses | Status | Meaning | | ---------- | ----------------------------------------------------------------------------------------------- | | `pending` | Submitted to the provider; provisioning in progress. | | `active` | Usable on quotes. | | `rejected` | Failed create-time compliance screening; `rejectionReason` identifies the cause. | | `invalid` | Became unusable after activation (for example, following a payout return). | | `failed` | Provisioning failed or the provider rejected the account; `failureReason` identifies the cause. | | `deleted` | Soft-deleted. | ## Related * [Debit cards](/payments/debit-cards): using a card as a funding source or payout destination * [Bank transfers guide](/api-reference/guide-bank-transfers): ACH and wire transfer walkthrough * [Transactions](/payments/transactions): how to use an external account on a quote # Get started Source: https://docs.polygon.technology/payments/get-started Request access, authenticate, and run your first transaction on the Open Money Stack. OMS gives you a single API for moving money between fiat and stablecoins. This guide takes you from zero to a working transaction in five steps. OMS is in early access. Start by requesting access from the dashboard. Submit your details to get sandbox credentials. Once approved, open the OMS Dashboard and navigate to **API Keys**. Generate a new key and store the secret immediately, it is shown only once. Keys do not expire by default, support an optional enforced expiration, and can be rotated from the dashboard at any time. Treat your API key secret like a password. If it is ever compromised, revoke the key from the dashboard and generate a new one immediately. You do not send the API key directly on requests. Exchange the key and secret for a short-lived bearer token at `POST /auth/token`, then send that token on every other call. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/auth/token \ -H "Content-Type: application/json" \ -d '{ "apiKey": "{api_key}", "apiSecret": "{api_secret}" }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/auth/token \ -H "Content-Type: application/json" \ -d '{ "apiKey": "{api_key}", "apiSecret": "{api_secret}" }' ``` ```json theme={null} { "accessToken": "eyJhbGc...", "tokenType": "bearer", "expiresIn": 3600, "expiresAt": "2026-01-15T11:00:00Z" } ``` The token is valid for 60 minutes. Send the `accessToken` as a bearer token on every other request: ```text theme={null} Authorization: Bearer {accessToken} ``` Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. Replaying the same key returns the original result instead of re-executing. When a request returns `401`, the token has expired. Exchange your key for a fresh one and retry. If `POST /auth/token` returns `429`, the endpoint is rate-limited: back off before retrying using the `Retry-After` header. Every wallet, transaction, and payment route in OMS belongs to a customer record. Create one before anything else. To onboard a customer who can move USD or use cash services, send the full set of identifying fields, not just a name, and request the `endorsements` the customer needs. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/customers \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: cst-first-customer-001" \ -d '{ "type": "individual", "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "phone": "+12125551234", "birthDate": "1990-05-15", "nationality": "US", "residentialAddress": { "line1": "123 Main St", "city": "New York", "state": "NY", "country": "US", "zipCode": "10001" }, "identifyingInformation": [ { "type": "ssn", "issuingCountry": "US", "number": "123-45-6789" } ], "endorsements": ["basic", "cryptoCustody", "usd"] }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/customers \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: cst-first-customer-001" \ -d '{ "type": "individual", "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "phone": "+12125551234", "birthDate": "1990-05-15", "nationality": "US", "residentialAddress": { "line1": "123 Main St", "city": "New York", "state": "NY", "country": "US", "zipCode": "10001" }, "identifyingInformation": [ { "type": "ssn", "issuingCountry": "US", "number": "123-45-6789" } ], "endorsements": ["basic", "cryptoCustody", "usd"] }' ``` ```json theme={null} { "id": "cst_01H9Xa...", "object": "customer", "type": "individual", "status": "active", "endorsements": [ { "name": "basic", "status": "ACTIVE" }, { "name": "cryptoCustody", "status": "ACTIVE" }, { "name": "usd", "status": "PENDING" } ], "wallets": [], "createdAt": "2026-01-15T10:00:00Z" } ``` Store the `cst_` ID; you pass it to every wallet, quote, and transaction. Each endorsement tracks its own `status` in SCREAMING\_CASE and must reach `ACTIVE` before its capability unlocks. PII fields (`birthDate`, `residentialAddress`, `ipAddress`, `identifyingInformation`) are write-only: OMS accepts them but never returns them. The API accepts a customer with only `type`, but a customer created without the identifying fields below cannot be provisioned to move fiat. The record is created, yet calls that need a provisioned fiat account (cash-in or a fiat transaction) fail. For USD and cash flows, always provide: * A structured `residentialAddress` (the object above, not a free-text string) * `phone` in E.164 format * `birthDate` * A government ID in `identifyingInformation` (for US customers, an `ssn` or `itin`) Supply these at creation, or add them later with `PATCH /customers/{customerId}`. Products that never touch fiat rails may not need them. Only `individual` is supported for `type` today. In sandbox, endorsements are auto-approved so you can test without a live KYC integration. Provisioning still reads the identifying fields above, so include them in sandbox too. Create a custodial wallet for the customer. The path carries the customer ID; the body names the `asset` and `chain` to hold. OMS derives the on-chain address and manages the keys, with no wallet SDK or user signing. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/customers/cst_01H9Xa.../wallets \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: wlt-first-wallet-001" \ -d '{ "asset": "usdc", "chain": "polygon" }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/customers/cst_01H9Xa.../wallets \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: wlt-first-wallet-001" \ -d '{ "asset": "usdc", "chain": "polygon" }' ``` ```json theme={null} { "id": "wlt_01H9Xb...", "object": "wallet", "customerId": "cst_01H9Xa...", "type": "internal", "status": "active", "asset": "usdc", "chain": "polygon", "address": "0xBEEF4a2c891D56e72b67a3f21d0cf94F1D7c5911", "blockchainAsset": { "protocol": "evm", "chainId": "137", "tokenId": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" }, "createdAt": "2026-01-15T10:01:00Z" } ``` The `address` is the on-chain address. The `wlt_` ID is what you pass as the source or destination in quotes and transactions. Read the current balance with `GET /wallets/{walletId}/balance`. The cash-in flow is the quickest path for your first run: it needs no external account. The bank transfer flow shows the standard quote-to-transaction pattern for fiat payouts. Let a customer deposit physical cash at a retail location and receive USDC in their wallet. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/cash-ins \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ci-first-001" \ -d '{ "customerId": "cst_01H9Xa...", "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNDpsYXQ9..." }, "source": { "asset": "usd", "indicatedAmount": "100.00" }, "destination": { "asset": "usdc", "network": "polygon", "wallet": { "id": "wlt_01H9Xb..." } } }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/cash-ins \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ci-first-001" \ -d '{ "customerId": "cst_01H9Xa...", "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNDpsYXQ9..." }, "source": { "asset": "usd", "indicatedAmount": "100.00" }, "destination": { "asset": "usdc", "network": "polygon", "wallet": { "id": "wlt_01H9Xb..." } } }' ``` OMS returns a `depositInstructions.code` valid for one hour. The customer presents the code at the retail location, hands over cash, and USDC lands in their wallet automatically. See the [Cash-in guide](/api-reference/guide-cash-in) for the full flow. Pay out from a wallet to a bank account. Register the destination bank account with `POST /external-accounts`, then quote and execute against the returned `ext_bankUs_` ID. A quote's source is always an OMS wallet or a card. Bank payouts require the customer's `usd` endorsement to be `ACTIVE`. **Register the destination bank account:** The body carries an `owner`, a `type`, and exactly one per-type object matching the type (here `bankUs`). ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/external-accounts \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ext-bank-first-001" \ -d '{ "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "bankUs": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" } }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/external-accounts \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ext-bank-first-001" \ -d '{ "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "bankUs": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" } }' ``` The response returns the `ext_bankUs_` ID. The account starts `pending` and flips to `active` once provisioning completes. The full account number is write-only; reads expose only `bankUs.accountNumberLast4`. **Pay out from a wallet to a bank account (USDC to fiat):** ```bash Sandbox theme={null} # Step 1: create a quote against the registered bank account curl -X POST https://sandbox-api.polygon.technology/v0.10/quotes \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: qt-bank-out-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "100.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9X...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } }' # Step 2: execute curl -X POST https://sandbox-api.polygon.technology/v0.10/transactions \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: txn-bank-out-001" \ -d '{ "quoteId": "qt_01H9Xq..." }' ``` ```bash Production theme={null} # Step 1: create a quote against the registered bank account curl -X POST https://api.polygon.technology/v0.10/quotes \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: qt-bank-out-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "100.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9X...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } }' # Step 2: execute curl -X POST https://api.polygon.technology/v0.10/transactions \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: txn-bank-out-001" \ -d '{ "quoteId": "qt_01H9Xq..." }' ``` See the [Bank transfers guide](/api-reference/guide-bank-transfers) for both directions and ACH-specific flows. OMS fires webhooks at every meaningful state change. You can poll `GET /transactions/{transactionId}` instead, but webhooks are strongly recommended for production. Register an endpoint with `POST /webhooks` (or in the OMS Dashboard under **Webhooks**). OMS returns a signing secret with the `whsec_` prefix once in the create response, so store it immediately. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/webhooks \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: whk-first-001" \ -d '{ "url": "https://api.yourapp.com/webhooks/oms", "events": [] }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/webhooks \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: whk-first-001" \ -d '{ "url": "https://api.yourapp.com/webhooks/oms", "events": [] }' ``` Pass an empty `events` array (or `["*"]` / `["ALL"]`) to subscribe to every event, or list specific event names to filter. Manage endpoints with `GET /webhooks`, `PATCH /webhooks/{id}` (set `enabled: false` to pause), and `DELETE /webhooks/{id}`. Every event carries the full resource object under `payload`, so you rarely need to poll for additional data. Some events you will see early on: | Event | Fires when | | ------------------------------------ | ------------------------------------------------------------------- | | `transaction.fiatToCrypto.completed` | A fiat-funded transaction delivered crypto to the destination | | `transaction.cryptoToFiat.completed` | A payout from a wallet delivered fiat to the destination | | `cashIn.completed` | A cash deposit was received and converted | | `externalAccount.verified` | A registered bank account passed validation and is usable on quotes | See [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog. Verify every webhook with the `Webhook-Signature` header before acting on it. The signature is an HMAC-SHA256 keyed with your signing secret; compare it in constant time and reject events with a stale timestamp. *** ## What's next Full walkthrough of the cash deposit flow, including deposit code generation and retail location selection. Move money between bank accounts and wallets in both directions using ACH and card rails. How OMS handles payments, stablecoin settlement, and compliant fiat access end to end. Complete endpoint reference for all OMS resources. # Send from a wallet Source: https://docs.polygon.technology/payments/guides/crypto-to-fiat Use the quote and transaction flow to move USDC out of a custodial wallet to a crypto address or a bank account. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. This guide shows you how to move USDC out of an OMS custodial wallet using the two-step quote and transaction flow. The crypto-to-crypto path is described first; paying out to a bank account follows the same pattern and is covered at the end.
Send from a wallet flow
1 App OMS POST /quotes (USDC/polygon → USDC/polygon)
2 OMS App Quote with locked pricing
3 App OMS POST /transactions \{quoteId}
4 OMS Polygon Pull USDC and send onchain
5 OMS App Webhook: transaction.cryptoToCrypto.processing
6 OMS App Webhook: transaction.cryptoToCrypto.completed
## Prerequisites Before sending USDC from a wallet, you need: 1. **A customer** with a `cst_` ID. 2. **A funded OMS wallet** with a `wlt_` ID containing enough USDC to cover the source amount plus gas (if not sponsoring gas). 3. **A destination**: another OMS wallet (its `wlt_` ID) or an external onchain address (a `0x...` address on the same network). 4. **A webhook endpoint** registered with `POST /webhooks` (or in the OMS Dashboard) to receive transaction events. ## Step 1: Create a quote Create a quote to lock in the pricing. OMS infers the direction as `cryptoToCrypto` from the source and destination instruments (an OMS wallet to a crypto address). Each side of a quote is a typed instrument: a `type` plus a `details` object. The quote `source` must be an OMS wallet (`walletOms`) or a card. The `destination` describes where the funds land. To send to an external onchain address, use a `walletExternal` instrument with `details.blockchainAddress`. To send to another OMS wallet, use `walletOms` with `details.id`. Set `amount` on exactly one side; OMS calculates the other. ### Request ``` POST /v0.10/quotes Authorization: Bearer {accessToken} Idempotency-Key: qt-alice-usdc-send-001 Content-Type: application/json ``` ```json theme={null} { "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "2000.00" }, "destination": { "type": "walletExternal", "details": { "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456", "asset": "usdc", "network": "polygon" } }, "sponsorGas": true } ``` **Required fields:** * `customerId`: The customer who owns this transaction. * `source`: A typed instrument. Set `type` to `walletOms` and put the OMS wallet `id`, `asset` (`usdc` or `usdt`), and `network` (`polygon`, `ethereum`, `base`) in `details`. * `destination`: A typed instrument naming where the funds land. For a crypto address, use `type: "walletExternal"` with either `details.blockchainAddress` (a raw address) or `details.id` (a registered external wallet, `ext_wlt_` prefix), plus `asset` and `network`. To send to another OMS wallet, use `type: "walletOms"` with `details.id`. * `amount` on exactly one side: Here we set it on the source (send 2,000 USDC). OMS calculates the destination amount after fees. Set it on the destination instead to target an exact received amount. **Optional fields:** * `sponsorGas`: When `true`, OMS absorbs gas fees. The customer sees source-side fees as zero and the actual gas cost appears in `pricing.sponsorGasCost`. At launch gas is always sponsored. * `metadata`: Up to 20 arbitrary key-value pairs. ### Response, `201 Created` ```json theme={null} { "id": "qt_01H9Xt...", "object": "quote", "status": "open", "sourceToDestination": "cryptoToCrypto", "customerId": "cst_01H9Xa...", "source": { "party": { "relationship": "customer", "customerId": "cst_01H9Xa...", "entityType": "individual" }, "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "blockchainAddress": "0x7B3a9F2c4D1eA8bF6390cE5d2B7fA104C8e3D9b1", "custodyType": "custodial", "txHash": null } }, "destination": { "party": { "relationship": "externalUnregistered", "name": null, "address": null }, "type": "walletExternal", "category": "crypto", "details": { "id": null, "asset": "usdc", "network": "polygon", "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456" } }, "pricing": { "source": { "asset": "usdc", "amountGross": "2000.00", "amountNet": "2000.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "destination": { "asset": "usdc", "amountGross": "2000.00", "amountNet": "2000.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "pair": "usdc/usdc", "exchangeRate": "1.0000", "effectiveRate": "1.0000", "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0.00" }, "metadata": null, "expiresAt": "2026-01-15T10:05:00Z", "createdAt": "2026-01-15T10:00:00Z" } ``` **Key fields in the response:** * `id`: The quote ID (prefix `qt_`). You pass it to create the transaction. * `status: "open"`: Pricing is locked, awaiting acceptance. * `sourceToDestination: "cryptoToCrypto"`: The direction OMS inferred from the instruments. * `source.party` and `destination.party`: Identify who is on each side. Here the source is your customer (`relationship: "customer"`) and the destination is an unregistered external address (`relationship: "externalUnregistered"`). * `pricing`: All money lives here. `pricing.destination.amountNet` is what the destination receives. At launch OMS absorbs fees and gas, so the `feesDeducted` components are `"0.00"` and `pricing.sponsorGas` is `true`. * `pricing.sponsorGasCost`: The estimated gas cost OMS absorbs. This is an out-of-band cost, not deducted from the destination amount. * `expiresAt`: Pricing expires after a short window. Create a new quote if it lapses. If the customer accepts the pricing, proceed to Step 2. If the quote expires, create a new one. ## Step 2: Create the transaction Accept the quote by creating a transaction that references the quote ID. This is the point of no return: USDC is pulled from the wallet and the onchain send begins. ### Request ``` POST /v0.10/transactions Authorization: Bearer {accessToken} Idempotency-Key: txn-alice-usdc-send-001 Content-Type: application/json ``` ```json theme={null} { "quoteId": "qt_01H9Xt..." } ``` The request body is intentionally thin: the quote is the contract. All source, destination, and pricing details were locked in Step 1. The transaction ID prefix is `txn_`. ### Response, `201 Created` ```json theme={null} { "id": "txn_01H9Xd...", "object": "transaction", "sourceToDestination": "cryptoToCrypto", "status": "processing", "subStatus": "processing.fundsPulled", "customerId": "cst_01H9Xa...", "source": { "party": { "relationship": "customer", "customerId": "cst_01H9Xa...", "entityType": "individual" }, "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "blockchainAddress": "0x7B3a9F2c4D1eA8bF6390cE5d2B7fA104C8e3D9b1", "custodyType": "custodial", "txHash": "0x7f2a9b...c3d4e5" } }, "destination": { "party": { "relationship": "externalUnregistered", "name": null, "address": null }, "type": "walletExternal", "category": "crypto", "details": { "id": null, "asset": "usdc", "network": "polygon", "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456" } }, "pricing": { "source": { "asset": "usdc", "amountGross": "2000.00", "amountNet": "2000.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "destination": { "asset": "usdc", "amountGross": "2000.00", "amountNet": "2000.00", "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" } }, "pair": "usdc/usdc", "exchangeRate": "1.0000", "effectiveRate": "1.0000", "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0.42" }, "estimatedArrival": null, "error": null, "hold": null, "metadata": null, "createdAt": "2026-01-15T10:01:00Z", "updatedAt": "2026-01-15T10:01:00Z", "expiresAt": null } ``` **What to notice:** * `status: "processing"`: USDC has been pulled from the wallet and the onchain send is underway. * `subStatus: "processing.fundsPulled"`: Optional operational detail in dot notation, indicating the USDC has been pulled. The next sub-status reflects send progress. Branch on `status` alone. * `source.details.txHash`: The onchain transaction hash for the send. * `pricing.sponsorGasCost: "0.42"`: Final gas cost (may differ slightly from the quote estimate). * `hold: null`: No hold is in effect. If a transaction is blocked on the developer, an upstream provider, or compliance, `status` becomes `awaitingAction` and `hold` carries the detail. ### Webhook: `transaction.cryptoToCrypto.processing` OMS fires this event when the transaction enters processing: ```json theme={null} { "eventId": "evt_01H9Xw...", "eventName": "transaction.cryptoToCrypto.processing", "resourceType": "transaction_crypto_to_crypto", "resourceId": "txn_01H9Xd...", "occurredAt": "2026-01-15T10:01:00Z", "payload": { "id": "txn_01H9Xd...", "object": "transaction", "sourceToDestination": "cryptoToCrypto", "status": "processing", "subStatus": "processing.fundsPulled", "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "txHash": "0x7f2a9b...c3d4e5" } }, "destination": { "type": "walletExternal", "category": "crypto", "details": { "asset": "usdc", "network": "polygon", "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456" } }, "pricing": { "...": "full pricing object" }, "error": null, "hold": null, "metadata": null, "createdAt": "2026-01-15T10:01:00Z", "updatedAt": "2026-01-15T10:01:00Z" } } ``` The `payload` field carries the full transaction object in its new state; the event name tells you the state it entered. The envelope has more fields than shown here (`schemaVersion`, `orgId`, `sequence`, and others); see [Webhook events](/api-reference/webhook-events) for the full envelope and event catalog. ## Step 3: Track the transaction OMS pulls the USDC, broadcasts the onchain send, and the funds arrive at the destination address once the transaction confirms. OMS fires `transaction.cryptoToCrypto.completed` when the transaction reaches `completed`: ```json theme={null} { "eventId": "evt_01H9Xw2...", "eventName": "transaction.cryptoToCrypto.completed", "resourceType": "transaction_crypto_to_crypto", "resourceId": "txn_01H9Xd...", "occurredAt": "2026-01-15T10:02:30Z", "payload": { "id": "txn_01H9Xd...", "object": "transaction", "sourceToDestination": "cryptoToCrypto", "status": "completed", "subStatus": null, "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "txHash": "0x7f2a9b...c3d4e5" } }, "destination": { "type": "walletExternal", "category": "crypto", "details": { "asset": "usdc", "network": "polygon", "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456" } }, "pricing": { "...": "full pricing object" }, "error": null, "hold": null, "metadata": null, "createdAt": "2026-01-15T10:01:00Z", "updatedAt": "2026-01-15T10:02:30Z" } } ``` **What changed from the processing event:** * The event name is `transaction.cryptoToCrypto.completed`. * `payload.status` is now `completed` and `payload.subStatus` is `null`. * `updatedAt` reflects when the onchain send confirmed. At this point the flow is done. 2,000 USDC was pulled from the custodial wallet and delivered to the destination address. OMS absorbed the fees and the gas cost reported in `pricing.sponsorGasCost`. ### Polling alternative If you prefer polling over webhooks, retrieve the transaction directly: ``` GET /v0.10/transactions/txn_01H9Xd... Authorization: Bearer {accessToken} ``` Poll until `status` is `completed` or `failed`. Webhooks are preferred for production: they avoid unnecessary requests and notify you the moment status changes. ## Failure handling If the transaction fails after USDC has been pulled (for example, an onchain error or a compliance block), `status` moves to `failed` and OMS fires a `transaction.cryptoToCrypto.failed` event. The `error` object in the payload describes what went wrong: ```json theme={null} { "eventId": "evt_01H9Xw3...", "eventName": "transaction.cryptoToCrypto.failed", "resourceType": "transaction_crypto_to_crypto", "resourceId": "txn_01H9Xd...", "occurredAt": "2026-01-15T10:02:00Z", "payload": { "id": "txn_01H9Xd...", "object": "transaction", "sourceToDestination": "cryptoToCrypto", "status": "failed", "subStatus": null, "error": { "code": "sendRejected", "message": "Onchain send could not be completed" } } } ``` `failed` is terminal. Inspect `error.code` to decide how to retry or surface the failure to the customer. ### Returns and refunds A bank payout that fails or is returned after the funds left the wallet (an invalid account, a bank rejection, a return code) does not strand the money. The transaction's `error` object reports what happened and how the funds come back: * `error.recoverable` tells you whether the funds are being returned. * `error.refund` tracks the automatic return: the `amount`, `asset`, `network`, the `destinationWallet` receiving the re-credit, a `status` (`pending`, `completed`, `failed`), and the on-chain `txHash` once delivered. Because fiat payouts are funded from the wallet's stablecoin balance, a failed payout re-credits the stablecoin side automatically; there is nothing to request. * `error.recovery` appears instead of `refund` in the rare case where funds sit unattributed upstream and an operator-driven recovery is required; it carries the reference identifiers and instructions for that process. The refund fires its own events: `transaction.cryptoToFiat.refundCompleted` when the re-credit is delivered, and `transaction.cryptoToFiat.refundFailed` if the re-credit itself fails and needs operator follow-up (the crypto-to-crypto equivalents are `transaction.cryptoToCrypto.refundCompleted` and `.refundFailed`). Watch for the refund on the same transaction object rather than creating a compensating transfer yourself: crediting your user when `status` turns `failed` and again when `refund.status` turns `completed` is the classic double-credit bug. These rails have no card-style chargeback exposure: OMS does not enable ACH pull, so an inbound deposit cannot be clawed back the way a card payment can. ## Holds and compliance review If a transaction is blocked on the developer, an upstream provider, or compliance, it moves to `awaitingAction` rather than failing. The [event catalog](/api-reference/webhook-events) has no dedicated hold event, so read the transaction to observe the state; the `subStatus` carries the operational detail, and the `hold` object is populated when a specific, typed hold applies: ``` GET /v0.10/transactions/txn_01H9Xd... Authorization: Bearer {accessToken} ``` ```json theme={null} { "id": "txn_01H9Xd...", "object": "transaction", "sourceToDestination": "cryptoToFiatAccount", "status": "awaitingAction", "subStatus": "processing.underReview", "hold": null } ``` When a typed hold applies, `hold.type` is one of `senderAttribution`, `depositAddressFrozen`, or `depositAddressInactive`, and the object carries the fields for that reason. `awaitingAction` is non-terminal. Once the hold clears, the transaction returns to `processing` and continues to `completed` or `failed`, firing the corresponding transaction event. Branch on `status`: a transaction in `awaitingAction` is not done. ## Webhook events Transaction events are namespaced by transfer type: a crypto send fires `transaction.cryptoToCrypto.*` events, and a bank payout fires `transaction.cryptoToFiat.*` events. These are the events for this guide's flows; see [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog. | Event | When | | -------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `transaction.cryptoToCrypto.processing` | A wallet-to-wallet or wallet-to-address send starts executing | | `transaction.cryptoToCrypto.completed` | The crypto was delivered to the destination | | `transaction.cryptoToCrypto.failed` | The send failed; see `payload.error` | | `transaction.cryptoToCrypto.refundCompleted` / `.refundFailed` | The refund for a failed send was delivered, or itself failed | | `transaction.cryptoToFiat.processing` | A bank payout starts executing | | `transaction.cryptoToFiat.completed` | The fiat was delivered to the bank account | | `transaction.cryptoToFiat.failed` | The payout failed; see `payload.error` | | `transaction.cryptoToFiat.refundCompleted` / `.refundFailed` | The stablecoin re-credit for a failed payout was delivered, or itself failed | ## Key points * **The quote is the contract.** The transaction request body is just `{ "quoteId": "..." }`: no overrides. * **Sides are typed instruments.** A quote `source` is `walletOms` or `card`; the `destination` here is `walletExternal`. Send to another OMS wallet with `walletOms` and `details.id`, or to a raw address with `walletExternal` and `details.blockchainAddress`. * **Money lives in `pricing`.** The `source` and `destination` sides carry identity and instrument only. Read amounts, rates, and fees from the top-level `pricing` object. * **OMS absorbs fees and gas at launch.** `feesDeducted` components are `"0.00"` and `pricing.sponsorGas` is `true`. The gas cost OMS covers appears in `pricing.sponsorGasCost`, which may differ slightly between quote and transaction. * **`details.txHash` is populated** on the source because OMS executes an onchain transaction to send the USDC. * **Idempotency keys are per request.** Use a distinct `Idempotency-Key` for the quote and the transaction so retries are safe and don't create duplicates. ## Pay out to a bank account A fiat payout follows the same two-step quote and transaction flow. The quote `source` is still an OMS wallet holding USDC. The difference is the destination: instead of a crypto instrument, you target an external bank account. OMS infers the direction as `cryptoToFiatAccount`. Register the bank account first with `POST /external-accounts`: pass an `owner` (`{ "kind": "customer", "customerId": "..." }`), `type: "bankUs"`, and a `bankUs` object with the account and routing numbers. OMS returns the `ext_bankUs_` ID; the account starts `pending` and flips to `active` when provisioning completes. Reference that ID in the quote's destination, with `asset: "usd"`, a fiat `network` (`ach`, `achSameDay`, `wire`, or `rtp`), and `accountHolder: "customer"` (the only accepted value). The payout requires the customer's `usd` endorsement to be `ACTIVE`. ```json theme={null} { "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "2000.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9X...", "asset": "usd", "network": "ach", "accountHolder": "customer" } }, "sponsorGas": true } ``` Once the quote is open, create the transaction the same way, with `{ "quoteId": "..." }`. Track it with the `transaction.cryptoToFiat.processing`, `transaction.cryptoToFiat.completed`, and `transaction.cryptoToFiat.failed` events, or by polling `GET /v0.10/transactions/{transactionId}`. ACH transfers typically settle in 1 to 3 business days; `wire` and `rtp` are faster. The same operational notes apply: the quote is the contract, money lives in `pricing`, and you should use a distinct idempotency key per request. See [Customer onboarding](/payments/guides/customer-onboarding) for the full external-account registration walkthrough. For card rails, set `settlementType` on the quote to choose where the crypto lands: `internal` keeps it in OMS custody, `external` delivers it to an on-chain wallet. It defaults to `external` for a card buy and `internal` for a card sell, and is ignored for non-card rails. # Customer onboarding Source: https://docs.polygon.technology/payments/guides/customer-onboarding How to register a customer, collect identity verification, and provision their first wallet. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. This guide walks through registering a new end user in OMS, requesting the endorsements that unlock money movement, and provisioning a custodial wallet. Every transaction, deposit address, and virtual account in OMS belongs to a customer record, so this is the starting point for any integration. ## Architecture
Customer onboarding flow
1 App OMS POST /customers
2 OMS App \{id: "cst\_...", endorsements: \[...]}
3 App User Collect identity documents
4 User App KYC data
5 App OMS Submit verification (endorsement review)
6 OMS App Endorsements active
7 App OMS POST /customers/\{id}/wallets
8 OMS Polygon Derive wallet address
9 OMS App \{id: "wlt\_...", address: "0x..."}
10 App OMS POST /external-accounts
11 OMS App \{id: "ext\_bankUs\_...", status: "pending"}
*** ## Prerequisites * An OMS API key. * Webhook endpoints registered with `POST /webhooks` (or in the OMS Dashboard) if you plan to react to status changes as endorsements advance and transactions settle. *** ## Step 1: Create a customer Register the end user with a `POST /customers` call. OMS creates the record and returns an ID. To onboard a customer who can move USD or use cash services, send the full set of identifying fields, not just a name, and request the `endorsements` the customer needs. ### Request ```text theme={null} POST /v0.10/customers Authorization: Bearer {accessToken} Idempotency-Key: cst-alice-001 Content-Type: application/json ``` ```json theme={null} { "type": "individual", "firstName": "Alice", "lastName": "Martin", "email": "alice@example.com", "phone": "+12125551234", "birthDate": "1990-05-15", "nationality": "US", "residentialAddress": { "line1": "123 Main St", "city": "New York", "state": "NY", "country": "US", "zipCode": "10001" }, "identifyingInformation": [ { "type": "ssn", "issuingCountry": "US", "number": "123-45-6789" } ], "externalId": "usr_7890", "signedAgreement": true, "endorsements": ["basic", "cryptoCustody", "usd"] } ``` **Required field:** `type` must be `individual`. Only the individual customer type is supported today. **`endorsements`** declares which capabilities the customer needs: `basic` (identity and geo checks), `cryptoCustody` (custodial wallets that hold stablecoins), and `usd` (USD movement over fiat rails, including bank payouts, virtual accounts, and cash services). Requesting `cryptoCustody` or `usd` auto-includes `basic`. If you omit `endorsements`, OMS defaults to `cryptoCustody` and `usd`. Request endorsements through the Customers API. **`externalId`** is the recommended way to link the OMS customer record to your internal user ID. Pass it at creation to avoid a separate update call later. ### Response, `201 Created` ```json theme={null} { "id": "cst_01H9Xa...", "object": "customer", "type": "individual", "firstName": "Alice", "lastName": "Martin", "email": "alice@example.com", "externalId": "usr_7890", "status": "active", "endorsements": [ { "name": "basic", "status": "ACTIVE" }, { "name": "cryptoCustody", "status": "ACTIVE" }, { "name": "usd", "status": "PENDING" } ], "wallets": [], "createdAt": "2026-01-15T10:00:00Z" } ``` The `status` field is `active` or `inactive`; all the granularity lives in the per-endorsement `status`, which uses SCREAMING\_CASE (`INACTIVE`, `PENDING`, `ISSUES`, `ACTIVE`, `REJECTED`, `REVOKED_ISSUES`, `OFFBOARDED`). An endorsement must reach `ACTIVE` before its capability unlocks. PII fields (`birthDate`, `residentialAddress`, `ipAddress`, `identifyingInformation`) are write-only: OMS accepts them but never returns them. A customer created without the identifying fields cannot be provisioned to move fiat. *** ## Step 2: Collect identity and unlock endorsements OMS uses an endorsement model to gate access to financial operations. An endorsement tracks the KYC and compliance status behind a capability. Request the endorsements a customer needs at creation, or add them later with `PATCH /customers/{customerId}`. | Endorsement | Unlocks | | --------------- | ----------------------------------------------------------------------------------- | | `basic` | Identity and geo-based compliance checks. Auto-included with any other endorsement. | | `cryptoCustody` | Custodial wallets that hold stablecoins. | | `usd` | USD movement over fiat rails: US bank payouts, virtual accounts, and cash services. | Your KYC flow submits identity documents to OMS for review, which advances each requested endorsement toward `ACTIVE`. Provisioning reads the identifying fields above, so include them even in sandbox, where endorsements are auto-approved. A customer created with only `type` cannot be provisioned to move fiat. For USD and cash flows, always provide a structured `residentialAddress`, a `phone` in E.164 format, a `birthDate`, and a government ID in `identifyingInformation` (for US customers, an `ssn` or `itin`). Endorsement names on the wire are `basic`, `cryptoCustody`, and `usd`. An upcoming release adds a finer-grained set of endorsements for specific rails (for example, separate IBAN, Canadian bank, and card endorsements). Those are early access; contact us to enable them. *** ## Step 3: Provision a wallet Once the customer's `cryptoCustody` endorsement is `ACTIVE`, create a custodial wallet with `POST /customers/{customerId}/wallets`. The path carries the customer ID; the body names the single `asset` and `chain` the wallet will hold. OMS derives the onchain address and manages the keys, with no wallet SDK or user signing. ### Request ```text theme={null} POST /v0.10/customers/cst_01H9Xa.../wallets Authorization: Bearer {accessToken} Idempotency-Key: wlt-alice-polygon-001 Content-Type: application/json ``` ```json theme={null} { "asset": "usdc", "chain": "polygon" } ``` Each wallet holds a single `asset` (`usdc` or `usdt`) on one `chain` (`polygon`, `ethereum`, `base`, or `solana`). To provision the same customer on another chain or asset, call the endpoint again with a different `asset`/`chain` pair. ### Response, `201 Created` ```json theme={null} { "id": "wlt_01H9Xb...", "object": "wallet", "customerId": "cst_01H9Xa...", "type": "internal", "status": "active", "asset": "usdc", "chain": "polygon", "address": "0xBEEF4a2c891D56e72b67a3f21d0cf94F1D7c5911", "blockchainAsset": { "protocol": "evm", "chainId": "137", "tokenId": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" }, "createdAt": "2026-01-15T10:01:00Z" } ``` The `address` is the onchain address where deposits for this customer arrive, and `blockchainAsset` resolves the on-chain identity (protocol, chain ID, and token contract). `type` is `internal` for OMS-managed wallets; `status` is `active`, `suspended`, or `closed`. Store the `wlt_` ID, you will use it as the source or destination in quotes and transactions. Read the current balance with `GET /wallets/{walletId}/balance`, and list a customer's wallets with `GET /customers/{customerId}/wallets`. *** ## Step 4: Register an external bank account If the customer will receive fiat payouts, they need a registered external bank account. External accounts are referenced everywhere in OMS by their ID, with a per-type prefix: `ext_bankUs_` for US bank accounts, `ext_bankIban_` for IBAN accounts, `ext_bankCa_` for Canadian accounts, `ext_card_` for debit cards, and `ext_wlt_` for externally-custodied wallets. Register the account with `POST /external-accounts`. The body carries an `owner`, a `type`, and exactly one per-type object named after the type (here `bankUs`). Supplying a per-type object that does not match `type` is rejected with `422`. ### Request ```text theme={null} POST /v0.10/external-accounts Authorization: Bearer {accessToken} Idempotency-Key: ext-alice-bank-001 Content-Type: application/json ``` ```json theme={null} { "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "bankUs": { "accountNumber": "123456789012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "label": "Alice checking" } ``` For `bankUs`, `accountNumber` and `routingNumber` are required; `accountType` (`checking` or `savings`) and `bankName` are optional. IBAN accounts use a `bankIban` object (`iban` and `BIC` required) and Canadian accounts use a `bankCanada` object (`institutionNumber`, `transitNumber`, and `accountNumber` required). ### Response, `201 Created` ```json theme={null} { "id": "ext_bankUs_01H9X...", "object": "externalAccount", "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." }, "type": "bankUs", "category": "fiatAccount", "status": "pending", "bankUs": { "accountNumberLast4": "9012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "label": "Alice checking", "createdAt": "2026-01-15T10:02:00Z", "updatedAt": "2026-01-15T10:02:00Z" } ``` The account starts `pending` and flips to `active` once provisioning completes, or `failed` if the provider rejects it. `accountNumber` is write-only: reads expose only `bankUs.accountNumberLast4`, never the full number. List a customer's accounts with `GET /external-accounts?customerId=...` (the `customerId` query parameter is required). Once the account is `active`, reference its ID in the `destination` of a quote to pay out to that account: ```json theme={null} { "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "100.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9X...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } } ``` Bank payouts require the customer's `usd` endorsement to be `ACTIVE`. See [Send from a wallet](/payments/guides/crypto-to-fiat) for the full payout flow. To pay a third-party recipient rather than the customer, create a counterparty with `POST /counterparties` (`customerId` and `name` required), then register the recipient's bank account with `owner: { "kind": "counterparty", "counterpartyId": "..." }`. Debit cards (`type: "card"`) and external wallets (`type: "walletExternal"`) register through the same `POST /external-accounts` endpoint; there is no separate cards endpoint. *** ## What's next With a customer, wallet, and registered external account in place, you can: * Fund the wallet with a [cash-in](/api-reference/guide-cash-in) at a retail location. * Set up a [virtual account](/payments/guides/virtual-accounts) to accept recurring ACH deposits. * Create a [deposit address](/payments/guides/deposit-addresses) to receive crypto from external wallets. * Send funds out of the wallet with the [send guide](/payments/guides/crypto-to-fiat): crypto addresses or bank payouts. # Deposit addresses Source: https://docs.polygon.technology/payments/guides/deposit-addresses How to accept crypto at a persistent on-chain address that auto-converts incoming funds and pays out to a bank account. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {token}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. Deposit addresses must be enabled for your project before `POST /deposit-addresses` succeeds, and the customer must be provisioned for them. Contact us to enable deposit addresses for your project. Share your on-ramp use case and we'll enable deposit addresses for your project. A deposit address is a reusable on-chain address assigned to a customer. When crypto arrives at the address, OMS automatically creates and executes a `cryptoToFiatAccount` transaction that converts it and pays out to the customer's registered bank account. There is no quote step and no amount specified upfront: the amount is whatever the sender deposits.
Deposit address flow
1 App OMS Create the deposit address with POST /deposit-addresses
2 OMS App Provisioning completes and populates depositInstructions
3 App Sender Share the on-chain inlet address
4 Sender OMS Send USDC or USDT to the address
5 OMS Detect deposit, auto-create and execute the cryptoToFiatAccount transaction
6 OMS App Webhook: transaction.cryptoToFiat.completed
## Prerequisites Before you can create a deposit address, you need: 1. **A customer** with a `cst_` ID and the `cryptoCustody` endorsement active. 2. **A registered bank external account** owned by that customer to receive the payout. Register one with `POST /external-accounts` (`type` of `bankUs`, `bankIban`, or `bankCanada`); the account's `ext_` ID goes in the deposit address's `destination`. 3. **Deposit addresses enabled** for your project and the customer provisioned for them (contact us). 4. **A webhook subscription** covering the `transaction.cryptoToFiat.*` events (and, optionally, the `depositAddress.*` lifecycle events) so you learn when the auto-created transaction is delivered. Register one with `POST /webhooks` (body `{ url, events }`) or in the OMS Dashboard. See the [transaction lifecycle](/payments/core-concepts/transaction-lifecycle) for the delivery model. ## Create a deposit address Create the address with `POST /deposit-addresses`. You name the customer, the inbound asset and network the address watches, and the registered bank external account that receives the converted funds. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/deposit-addresses \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: da-alice-usdc-001" \ -d '{ "customerId": "cst_01H9Xa...", "expectedSourceAsset": "usdc", "expectedSourceNetwork": "base", "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9Xf...", "asset": "usd", "network": "ach", "accountHolder": "customer" } }, "label": "Alice USDC inbound" }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/deposit-addresses \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: da-alice-usdc-001" \ -d '{ "customerId": "cst_01H9Xa...", "expectedSourceAsset": "usdc", "expectedSourceNetwork": "base", "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9Xf...", "asset": "usd", "network": "ach", "accountHolder": "customer" } }, "label": "Alice USDC inbound" }' ``` * `customerId`: the customer who owns the address. Required. * `expectedSourceAsset`: the inbound stablecoin the address expects, `usdc` or `usdt` (lowercase). Required. * `expectedSourceNetwork`: the network the address watches for deposits, for example `base`, `ethereum`, or `solana`. Required. * `destination`: the registered bank external account that receives the payout. Required. `type` is `bankUs`, `bankIban`, or `bankCanada`, and `details` carries the account's `id` (for example `ext_bankUs_...`), `asset`, `network`, and `accountHolder`. `accountHolder` must be `customer`. OMS validates the `details` against the resolved external account. * `sponsorGas`: when `true`, OMS absorbs the on-chain gas cost of the destination delivery. Optional, defaults to `true`; only `true` is currently supported. * `label` and `metadata`: an optional display label and an optional string-to-string map for your own references. The `201` response returns the deposit address with a `da_` ID: ```json theme={null} { "id": "da_01H9Xy...", "object": "depositAddress", "customerId": "cst_01H9Xa...", "status": "pending", "statusReason": null, "failureReason": null, "expectedSourceAsset": "usdc", "expectedSourceNetwork": "base", "destination": { "type": "bankUs", "category": "fiatAccount", "details": { "id": "ext_bankUs_01H9Xf...", "asset": "usd", "network": "ach", "accountNumberLast4": "4321", "routingNumber": "021000021", "accountType": "checking", "bankName": "Example Bank" } }, "depositInstructions": null, "transactionType": "cryptoToFiat", "label": "Alice USDC inbound", "metadata": {}, "createdAt": "2026-01-15T14:30:00Z", "updatedAt": "2026-01-15T14:30:00Z" } ``` `depositInstructions` is `null` in the `201` response: OMS provisions the on-chain inlet address asynchronously. `transactionType` is always `cryptoToFiat`; the transactions the address produces report `sourceToDestination: cryptoToFiatAccount`. ## Wait for provisioning, then share the address Poll `GET /deposit-addresses/{depositAddressId}` (or re-fetch the address before you display it) until `status` is `active` and `depositInstructions` is populated: ``` GET /v0.10/deposit-addresses/da_01H9Xy... Authorization: Bearer {token} ``` Once the address is `active`, `depositInstructions` carries the inlet address to display to your customer: ```json theme={null} { "address": "0xABC123...", "asset": "usdc", "network": "base", "expiresAt": null } ``` | Field | Meaning | | ----------- | ------------------------------------------------------------------------------------------ | | `address` | The OMS-owned on-chain inlet address for this deposit address. Give this to your customer. | | `asset` | The stablecoin the address accepts. Same value as `expectedSourceAsset`. | | `network` | The chain the address accepts funds on. Same value as `expectedSourceNetwork`. | | `expiresAt` | Reserved for a future provider-imposed inlet expiry. Null today. | Crypto sent to `address` is converted and paid out to the configured bank external account. Deposit-address webhooks are live. The address itself fires lifecycle events (`depositAddress.created`, `depositAddress.active`, `depositAddress.paused`, `depositAddress.resumed`, `depositAddress.frozen`, `depositAddress.closed`, `depositAddress.deleted`, `depositAddress.failed`); each inbound deposit fires `deposit_address.crypto_deposit.*` events; and the payout leg fires `deposit_address.ach_payout.*`, `deposit_address.wire_payout.*`, or `deposit_address.intl_wire_payout.*` events. Subscribe to `depositAddress.active` to learn when the inlet address is ready instead of polling. See [Webhook events](/api-reference/webhook-events) for the full catalog. ## Test in sandbox In sandbox, simulate an inbound transfer to exercise the auto-created transaction path without moving real funds. Call `POST /deposit-addresses/{depositAddressId}/simulate` with the amount to simulate. The currency and network are resolved server-side from the deposit address. This endpoint returns `404` in production. ```bash theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/deposit-addresses/da_01H9Xy.../simulate \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "amount": { "value": "25000" } }' ``` The `value` is the stablecoin amount in cents (greater than 0 and at most 100000). The response echoes the simulated deposit and returns a synthetic `transactionHash` you can correlate against the webhook: ```json theme={null} { "depositAddressId": "da_01H9Xy...", "amount": { "value": "25000", "currency": "USDC" }, "network": { "...": "..." }, "transactionHash": "0x...", "status": "submitted", "submittedAt": "2026-01-15T10:32:00Z" } ``` The simulated inbound funds create the same auto-created transaction that a real deposit would, so this is the way to build and verify your webhook and reconciliation handling before going live. ## What OMS creates when funds arrive When OMS detects the deposit (real or simulated), it creates a transaction in `processing` status and fires the `transaction.cryptoToFiat.processing` event, with the full transaction under `payload`. The transaction skips the quote step, so its `precursor` is typed `depositAddress` and carries the `depositAddressId`. Pricing is calculated at the moment funds arrive and lives in the top-level `pricing` object. The transaction's `sourceToDestination` is `cryptoToFiatAccount`: the source is the sender's on-chain transfer and the destination is the deposit address's configured bank external account. ```json theme={null} { "eventId": "evt_01H9Xw...", "eventName": "transaction.cryptoToFiat.processing", "resourceType": "transaction_crypto_to_fiat", "resourceId": "txn_01H9Xd...", "occurredAt": "2026-01-15T14:30:00Z", "payload": { "id": "txn_01H9Xd...", "object": "transaction", "status": "processing", "subStatus": "processing.fundsPulled", "customerId": "cst_01H9Xa...", "sourceToDestination": "cryptoToFiatAccount", "precursor": { "type": "depositAddress", "details": { "depositAddressId": "da_01H9Xy...", "depositInstructions": { "address": "0xABC123...", "asset": "usdc", "network": "base" } } }, "source": { "type": "walletExternal", "category": "crypto", "details": { "blockchainAddress": "0xSENDER...", "asset": "usdc", "network": "base", "txHash": "0x8a3b7c...d4e5f6" } }, "destination": { "type": "bankUs", "category": "fiatAccount", "details": { "id": "ext_bankUs_01H9Xf...", "asset": "usd", "network": "ach", "accountNumberLast4": "4321", "routingNumber": "021000021", "accountType": "checking", "bankName": "Example Bank" } }, "pricing": { "source": { "asset": "usdc", "amountGross": "250.00", "amountNet": "246.25", "feesDeducted": { "total": "3.75", "developer": "3.75", "oms": "0.00", "gas": "0.00" } }, "destination": { "asset": "usd", "amountGross": "246.25", "amountNet": "246.25" }, "pair": "usdc/usd", "exchangeRate": "1.0", "effectiveRate": "0.985", "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0" }, "estimatedArrival": null, "error": null, "createdAt": "2026-01-15T14:30:00Z", "updatedAt": "2026-01-15T14:30:00Z" } } ``` **What to notice:** * `precursor.type` is `depositAddress`, and `precursor.details.depositAddressId` links the transaction back to the originating deposit address. * `source.details.txHash` is the on-chain hash of the incoming deposit. * `destination` is the bank external account you configured on the deposit address. * `pricing.source.amountGross` is the amount actually deposited, now known. * `pricing.source.feesDeducted.developer` is your fee, computed as a percentage of the deposit. * `fixedAmountSide` is `source`: the deposited amount is fixed and the destination payout is calculated from it. ## Track the transaction Branch on the transaction `status`. `processing` means the deposit was detected and execution is underway; `completed` means funds were delivered; `failed` is a terminal failure with an `error` object. See the [transaction lifecycle](/payments/core-concepts/transaction-lifecycle) for the full status model and sub-statuses. Prefer webhooks over polling: OMS fires `transaction.cryptoToFiat.processing`, `transaction.cryptoToFiat.completed`, and `transaction.cryptoToFiat.failed` for the auto-created transaction, and each delivery carries the full transaction object under `payload`, so your handler branches on the event name or `payload.status`. If you do poll, read the transaction directly: ``` GET /v0.10/transactions/txn_01H9Xd... Authorization: Bearer {token} ``` Or scope a listing to the customer: ``` GET /v0.10/transactions?customerId=cst_01H9Xa... Authorization: Bearer {token} ``` ## Reuse A deposit address is persistent. While it is `active` it keeps monitoring its inlet address, so every subsequent deposit triggers the same flow: a new transaction with a new `txn_` ID, the same `depositAddressId`, and pricing computed from the same configuration. There is no limit on the number of transactions a single deposit address can produce. ## Status lifecycle | Status | Meaning | | ------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `pending` | Created; OMS is provisioning the inlet address. `depositInstructions` is still null. | | `active` | The inlet address is live. Deposits convert and pay out to the destination. | | `frozen` | Deposits are suspended. `statusReason` explains why. | | `closed` | The address is permanently closed and no longer monitors its inlet address. | | `failed` | Provisioning failed; `failureReason` identifies the category. Create a new deposit address. | | `inactiveActionRequired` | The destination external account is no longer usable. Re-point `destination` with `PATCH` to recover to `active`. | ## Held deposits A deposit can arrive in a state the transaction cannot settle from. Instead of failing, the auto-created transaction moves to `awaitingAction` with a typed `hold` that explains what is blocking it and, when a deadline applies, how long you have to resolve it. A deposit held for sender attribution also fires the `deposit_address.crypto_deposit.needs_attribution` event: | `hold.type` | Cause | Resolution | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `senderAttribution` | The deposit came from an on-chain address OMS cannot attribute to a known sender. The hold carries the `txHash` and `matchableExternalAccountCriteria` (the address and network family to match). | Register a `walletExternal` external account matching the criteria. The registration's create response lists the released transactions in `resolvedTransactions`, and each moves back to `processing` once the provider confirms settlement. | | `depositAddressFrozen` | The deposit arrived while the deposit address was `frozen`. | Clears when the freeze lifts; no developer action. | | `depositAddressInactive` | The destination external account became unusable (see `hold.cause` for the account, its status, and the reason). | Re-point the deposit address `destination` to a healthy external account with `PATCH`. | Unresolved holds fail at their deadline with a matching terminal sub-status (`failed.attributionTimeout`, `failed.depositAddressFrozenTimeout`, `failed.depositAddressInactiveTimeout`). Branch on `status`; use `subStatus` and `hold` for operational detail. ## Manage deposit addresses ### List `GET /deposit-addresses` spans every customer in your organization. Filter with `customerId` and `status`, both optional. Paginate with `limit`, `startingAfter`, and `endingBefore`: ``` GET /v0.10/deposit-addresses?customerId=cst_01H9Xa...&status=active&limit=20 Authorization: Bearer {token} ``` The response is a list envelope: `{ object, data, hasMore, nextCursor, previousCursor }`. Pass `nextCursor` as `startingAfter` to fetch the next page, or `previousCursor` as `endingBefore` to page backward; `hasMore` signals whether more rows exist in the direction of travel. ### Update `PATCH /deposit-addresses/{depositAddressId}` accepts `destination`, `label`, and `metadata`; `sponsorGas` is also accepted, but only `true` is supported. Any other key in the body is rejected with `400`. ```bash theme={null} curl -X PATCH https://sandbox-api.polygon.technology/v0.10/deposit-addresses/da_01H9Xy... \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: da-repoint-001" \ -d '{ "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9Xg...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } }' ``` Re-pointing `destination` to a healthy bank external account recovers a deposit address from `inactiveActionRequired` back to `active`. A re-point on an already `active` address updates the target without a status change. The new destination is validated exactly like create. ### No delete There is no `DELETE` endpoint for deposit addresses. If you no longer want deposits on an address, stop sharing its inlet address. ## Related * [Deposit addresses overview](/payments/deposit-addresses): concept summary and comparison with virtual accounts * [Virtual accounts guide](/payments/guides/virtual-accounts): the fiat equivalent, a bank account number that auto-converts to crypto * [Transaction lifecycle](/payments/core-concepts/transaction-lifecycle): statuses, sub-statuses, and webhook events for the auto-created transaction # Fiat to crypto Source: https://docs.polygon.technology/payments/guides/fiat-to-crypto How to bring fiat into a customer's custodial wallet. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. Fiat-in does not use the quote and transaction pattern. A quote's `source` is always an OMS wallet or a card, so there is no way to fund a quote with incoming fiat. Instead, fiat enters through a fiat-in product that auto-creates a transaction once the money arrives: * **Cash-in**: a customer deposits physical cash at a retail location, and OMS delivers USDC to the destination wallet. The auto-created transaction has direction `cashToCrypto`. * **Virtual accounts**: each customer gets a dedicated bank account number; an ACH, wire, or SWIFT deposit auto-converts to USDC. The auto-created transaction has direction `fiatAccountToCrypto`. Both paths deliver to a crypto destination (an OMS wallet) and let OMS create the transaction for you. You never call `POST /quotes` for fiat-in. The exception is **debit card funding**: a registered card is a valid quote `source`, so pull-from-card funding uses the standard quote and transaction flow rather than a fiat-in product. See [Debit cards](/payments/debit-cards) for the card source shape and `settlementType`. ## Cash-in Cash-in is a fiat-to-crypto path in the OMS API. You create a cash-in record that names the customer, the destination wallet, and the cash location where the customer will deposit. OMS returns estimated pricing upfront and finalizes it once the customer deposits cash at the counter. ### Create a cash-in ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/cash-ins \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ci-alice-usdc-001" \ -d '{ "customerId": "cst_01H9Xa...", "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNDpsYXQ9MzguNDk1MTMzLGxuZz0tMTIxLjUwNTMzNg==" }, "source": { "asset": "usd", "indicatedAmount": "100.00" }, "destination": { "asset": "usdc", "network": "polygon", "wallet": { "id": "wlt_01H9Xb..." } }, "sponsorGas": true }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/cash-ins \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ci-alice-usdc-001" \ -d '{ "customerId": "cst_01H9Xa...", "cash": { "locationId": "loc_01H9Xd...", "locationReference": "R1JFRU5ET1QtMjQzNDpsYXQ9MzguNDk1MTMzLGxuZz0tMTIxLjUwNTMzNg==" }, "source": { "asset": "usd", "indicatedAmount": "100.00" }, "destination": { "asset": "usdc", "network": "polygon", "wallet": { "id": "wlt_01H9Xb..." } }, "sponsorGas": true }' ``` * `customerId`: the customer making the deposit. Needs the `usd` endorsement to be `ACTIVE`. * `cash.locationId` and `cash.locationReference`: the deposit location, taken from the `locId` and `cashLocationReference` fields of the `GET /cash-locations` response. * `source.asset`: always `usd`. * `source.indicatedAmount`: the expected deposit amount, used for upfront pricing estimates. Optional; the actual amount is whatever the customer deposits at the counter. * `destination`: the crypto destination. Set `asset` and `network`, and identify the OMS wallet with `wallet.id`. * `sponsorGas`: when `true`, OMS absorbs the gas cost of delivering crypto. At launch gas is always sponsored. OMS returns the cash-in with estimated pricing in the top-level `pricing` object. When the customer deposits cash, OMS recalculates pricing on the actual amount, creates the `cashToCrypto` transaction, and delivers USDC to the destination wallet, firing `cashIn.completed` and the `transaction.fiatToCrypto.*` events along the way. For the full lifecycle, pricing fields, location lookup, and webhook events, see the [Cash-in guide](/api-reference/guide-cash-in). ## Virtual accounts Virtual accounts give each customer a dedicated bank account number. When the customer sends an ACH, wire, or SWIFT transfer to that number, OMS auto-converts the deposit to USDC and creates the `fiatAccountToCrypto` transaction. This suits recurring funding, where a customer tops up the same account repeatedly rather than deposits cash. Account provisioning requires the customer's `usd` endorsement to be `ACTIVE`, and virtual accounts must be enabled for your project (contact us to enable them). ### Create a virtual account Create the account with `POST /virtual-accounts`, naming the customer, the fiat source, and the wallet that receives the converted crypto: ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/virtual-accounts \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: va-alice-usd-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "network": "usBank" }, "destination": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" } }, "accountHolder": "customer", "type": "bankUs", "bankMemo": "Alice funding" }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/virtual-accounts \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: va-alice-usd-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "network": "usBank" }, "destination": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" } }, "accountHolder": "customer", "type": "bankUs", "bankMemo": "Alice funding" }' ``` * `source`: the fiat side; `asset` must be `usd` and `network` must be `usBank` today. * `destination`: either `walletOms` with the customer's wallet `id`, `asset`, and `network`, or `walletExternal` with the `id` of a registered external wallet account (`ext_wlt_...`) plus `asset` and `network`. Raw blockchain addresses are not accepted. * `accountHolder`: must be `customer`. `type`: must be `bankUs`. * `bankMemo`: an optional memo the customer can include on the transfer. The `201` response carries the `va_` ID with `bankDetails` set to `null`: OMS provisions the underlying deposit account asynchronously. Subscribe to the `virtualAccount.provisioned` event, or poll `GET /virtual-accounts/{virtualAccountId}` until `status` is `active` and `bankDetails` carries the domestic and SWIFT deposit instructions to share with your customer. When a transfer arrives, OMS credits the destination wallet and creates the transaction automatically, with no additional call from you; track it through the `virtualAccount.deposit.*` and `transaction.fiatToCrypto.*` events. ### Test in sandbox In sandbox you can rehearse the full flow without a real bank transfer. Simulate an inbound deposit with `POST /virtual-accounts/{virtualAccountId}/simulate`, setting `rail` to `ach_in`, `wire_in`, or `swift_in`: ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/virtual-accounts/va_01H9Xe.../simulate \ -H "Authorization: Bearer {accessToken}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: va-sim-alice-001" \ -d '{ "rail": "ach_in", "amount": { "value": "10000", "currency": "USD" } }' ``` The simulate endpoint is sandbox-only; it returns `404` in production. It drives the same auto-created transaction and webhook flow (`transaction.fiatToCrypto.*`) a real deposit would, so you can test reconciliation before going live. For the full lifecycle, statuses, updates, and deletion, see the [Virtual accounts guide](/payments/guides/virtual-accounts). ## Key points * **Fiat-in never uses a quote.** A quote's source is always an OMS wallet or a card, so incoming fiat cannot fund a quote. Use cash-in or virtual accounts, which auto-create the transaction for you. * **The destination is a crypto wallet.** Cash-in identifies it with `destination.wallet.id`; a virtual account uses the side shape, with `destination.type` set to `walletOms` or `walletExternal` and the wallet ID in `destination.details.id`. * **Direction is inferred.** Cash-in produces a `cashToCrypto` transaction; a virtual account produces a `fiatAccountToCrypto` transaction. * **The `usd` endorsement gates fiat-in.** Both cash-in and virtual accounts require the customer's `usd` endorsement to be `ACTIVE`. # Pay a third party Source: https://docs.polygon.technology/payments/guides/third-party-payouts Register a counterparty and their bank account, then pay them from a customer's wallet with the quote and transaction flow. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every `POST` and `PATCH` also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. This guide shows you how to pay someone who is not your customer: a vendor, a payee, or any other third party. In OMS, that recipient is a **counterparty**, an entry in the customer's address book that can own registered external accounts. Beneficiaries never need an OMS account of their own. The flow has four steps: create the counterparty, register their bank account, create a quote, and execute the transaction. ## Prerequisites 1. **A customer** with a `cst_` ID and the `usd` endorsement: the payout is funded from this customer's wallet. 2. **A funded OMS wallet** with a `wlt_` ID holding enough USDC to cover the payout. 3. **The recipient's details**: their legal name, and their bank account information. ## Step 1: Create the counterparty `POST /counterparties` requires only the owning `customerId` and the recipient's `name`. The identity fields are optional but improve compliance screening: `entityType` (`individual` or `business`), `email`, `phone`, `dateOfBirth`, `taxId`, `nationality`, and an `address`. ### Request ``` POST /counterparties Authorization: Bearer {accessToken} Idempotency-Key: ctp-acme-001 Content-Type: application/json ``` ```json theme={null} { "customerId": "cst_01H9Xa...", "name": "Acme Supplies LLC", "entityType": "business", "email": "billing@acme.example", "taxId": "98-7654321", "address": { "streetAddress": "500 Market St", "city": "San Francisco", "countryArea": "CA", "postalCode": "94105", "country": "US" } } ``` The `address` shape is `streetAddress`, `city`, `postalCode`, and `country` (required when an address is supplied), plus an optional `countryArea` for the state or region. ### Response, `201 Created` ```json theme={null} { "id": "ctp_01H9Xc...", "object": "counterparty", "customerId": "cst_01H9Xa...", "name": "Acme Supplies LLC", "entityType": "business", "email": "billing@acme.example", "taxId": "98-7654321", "address": { "streetAddress": "500 Market St", "city": "San Francisco", "countryArea": "CA", "postalCode": "94105", "country": "US" }, "status": "active", "rejectionReason": null, "createdAt": "2026-07-07T10:00:00Z", "updatedAt": "2026-07-07T10:00:00Z" } ``` Store the `ctp_` ID. A counterparty that fails compliance screening comes back `rejected` with a `rejectionReason`; only `active` counterparties can own external accounts. ## Step 2: Register the counterparty's bank account Register the recipient's bank with `POST /external-accounts`, setting the `owner` to the counterparty. The per-type detail object must match the `type`: `bankUs` for US accounts, `bankIban` for international IBAN accounts, or `bankCanada` for Canadian accounts. ### Request ``` POST /external-accounts Authorization: Bearer {accessToken} Idempotency-Key: ext-acme-bank-001 Content-Type: application/json ``` ```json theme={null} { "owner": { "kind": "counterparty", "counterpartyId": "ctp_01H9Xc..." }, "type": "bankUs", "bankUs": { "accountNumber": "987654321012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "label": "Acme payouts" } ``` ### Response, `201 Created` ```json theme={null} { "id": "ext_bankUs_01H9Xm...", "object": "externalAccount", "owner": { "kind": "counterparty", "counterpartyId": "ctp_01H9Xc..." }, "type": "bankUs", "category": "fiatAccount", "status": "pending", "bankUs": { "accountNumberLast4": "1012", "routingNumber": "021000021", "accountType": "checking", "bankName": "Chase" }, "label": "Acme payouts", "createdAt": "2026-07-07T10:01:00Z", "updatedAt": "2026-07-07T10:01:00Z" } ``` The full account number is write-only; reads return `accountNumberLast4`. The account starts `pending` and flips to `active` (usable on quotes) or `failed`. Wait for `active` before quoting against it. ## Step 3: Create the quote The quote is identical to any other bank payout: the `source` is the customer's OMS wallet, and the `destination` is the counterparty's registered bank account referenced by its `ext_bankUs_` ID. ``` POST /quotes Authorization: Bearer {accessToken} Idempotency-Key: qt-acme-payout-001 Content-Type: application/json ``` ```json theme={null} { "customerId": "cst_01H9Xa...", "source": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" }, "amount": "2500.00" }, "destination": { "type": "bankUs", "details": { "id": "ext_bankUs_01H9Xm...", "asset": "usd", "network": "ach", "accountHolder": "customer" } } } ``` On the response, OMS resolves the recipient's identity onto the destination's `party` block, so you can render who is being paid without extra reads: ```json theme={null} "destination": { "party": { "relationship": "externalRegistered", "counterpartyId": "ctp_01H9Xc...", "entityType": "business", "name": "Acme Supplies LLC" }, "type": "bankUs", "category": "fiatAccount", "details": { "id": "ext_bankUs_01H9Xm...", "asset": "usd", "network": "ach" } } ``` All amounts, fees, and rates live in the top-level `pricing` object. The direction is inferred as `cryptoToFiatAccount`. ## Step 4: Execute and track Accept the quote with `POST /transactions` and the `quoteId`: ```json theme={null} { "quoteId": "qt_01H9Xq..." } ``` The transaction starts at `processing` and moves to `completed` when the bank payout settles, or `failed` with an `error` object. The payout leg surfaces granular progress in `subStatus`: `processing.awaitingFiatOut` while the payout is queued, then `processing.fiatOut` once it is in flight. Poll `GET /transactions/{transactionId}` or subscribe to `transaction.cryptoToFiat.completed` (and `transaction.cryptoToFiat.failed`); see the [webhook events catalog](/api-reference/webhook-events) for the full list. To reconcile a payout run, filter `GET /transactions?customerId=...`: each transaction's destination `party` carries the `counterpartyId` it paid. Payouts are sent from named accounts: the recipient's bank statement shows the paying customer's own legal-entity name as the sender, not Polygon and not your platform. ## Managing counterparties * `GET /counterparties?customerId=...` lists a customer's counterparties (`customerId` is required). Paginate with `limit`, `startingAfter`, and `endingBefore`. * `GET /counterparties/{counterpartyId}` fetches one by ID. * `PATCH /counterparties/{counterpartyId}` partially updates identity fields; omitted fields are unchanged. * `DELETE /counterparties/{counterpartyId}` soft-deletes. It returns `409` while the counterparty still owns active external accounts: delete those accounts first, then delete the counterparty. ## Related * [B2B payouts](/api-reference/guide-b2b-payouts): running payouts to many recipients * [Bank transfers](/api-reference/guide-bank-transfers): rails, per-type bank fields, and registration details * [External accounts](/payments/external-accounts): the full registration and lifecycle model * [Crypto to fiat](/payments/guides/crypto-to-fiat): the underlying quote and transaction flow # Virtual accounts Source: https://docs.polygon.technology/payments/guides/virtual-accounts How to give a customer a dedicated bank account number that auto-converts incoming fiat deposits to crypto. **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {token}` on every request. Every mutating request (`POST` and `PATCH`) also requires an `Idempotency-Key` header. See [Get started](/payments/get-started) for the full flow. Virtual accounts must be enabled for your project before `POST /virtual-accounts` succeeds. Contact us to enable virtual accounts for your project. Share your on-ramp use case and we'll enable virtual accounts for your project. A virtual account gives a customer a dedicated bank account number. When fiat arrives via a supported rail, OMS automatically creates and executes a `fiatAccountToCrypto` transaction and delivers crypto to the configured destination wallet. There is no quote step and no amount specified upfront: the amount is whatever the sender deposits.
Virtual account flow
1 App OMS Create the virtual account with POST /virtual-accounts
2 OMS App Provisioning completes and populates bankDetails
3 App Customer Share bank deposit instructions
4 Customer Bank Initiate ACH or wire transfer
5 OMS Detect deposit, auto-create fiatAccountToCrypto transaction
6 OMS App Webhook: transaction.fiatToCrypto.completed
## Prerequisites Before you can create a virtual account, you need: 1. **A customer** with a `cst_` ID and the `usd` endorsement active. 2. **A destination for the converted crypto**: the customer's OMS wallet (a `wlt_` ID, created with `POST /customers/{customerId}/wallets`) or a registered external wallet (an `ext_wlt_` ID, registered with `POST /external-accounts`). Raw blockchain addresses are not accepted. 3. **Virtual accounts enabled** for your project (contact us). 4. **A webhook subscription** covering the `transaction.fiatToCrypto.*` events (and, optionally, the `virtualAccount.*` events) so you learn when the auto-created transaction is delivered. Register one with `POST /webhooks` (body `{ url, events }`) or in the OMS Dashboard. See the [transaction lifecycle](/payments/core-concepts/transaction-lifecycle) for the delivery model. ## Create a virtual account Create the account with `POST /virtual-accounts`. You name the customer, the fiat source, and the wallet that receives the converted crypto. ```bash Sandbox theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/virtual-accounts \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: va-alice-usd-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "network": "usBank" }, "destination": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" } }, "accountHolder": "customer", "type": "bankUs", "bankMemo": "Alice funding", "label": "Alice USD funding account" }' ``` ```bash Production theme={null} curl -X POST https://api.polygon.technology/v0.10/virtual-accounts \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: va-alice-usd-001" \ -d '{ "customerId": "cst_01H9Xa...", "source": { "asset": "usd", "network": "usBank" }, "destination": { "type": "walletOms", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" } }, "accountHolder": "customer", "type": "bankUs", "bankMemo": "Alice funding", "label": "Alice USD funding account" }' ``` * `customerId`: the customer who owns the account. Required. * `source`: the fiat side the account accepts. Required. `asset` must be `usd` and `network` must be `usBank` today. * `destination`: where the converted crypto is delivered. Required. Either `walletOms` (`details` with the customer's OMS wallet `id`, `asset`, and `network`) or `walletExternal` (`details` with a registered external wallet account `id` such as `ext_wlt_...`, plus `asset` and `network`). Raw blockchain addresses are not accepted; register the wallet as an external account first. OMS validates `asset` and `network` against the resolved account. * `accountHolder`: must be `customer`. Required. * `type`: must be `bankUs`. Required. * `bankMemo`: an optional memo the customer can include on the wire or ACH transfer. * `sponsorGas`: when `true`, OMS absorbs the on-chain gas cost of the destination delivery. Optional, defaults to `true`; only `true` is currently supported. * `label` and `metadata`: an optional display label and an optional string-to-string map for your own references. The `201` response returns the virtual account with a `va_` ID: ```json theme={null} { "id": "va_01H9Xv...", "object": "virtualAccount", "customerId": "cst_01H9Xa...", "status": "pending", "statusReason": null, "failureReason": null, "source": { "asset": "usd", "network": "usBank" }, "destination": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" } }, "bankDetails": null, "bankMemo": "Alice funding", "label": "Alice USD funding account", "metadata": {}, "deletionRequestedAt": null, "deletionRequestedBy": null, "finalBalance": null, "createdAt": "2026-01-15T14:30:00Z", "updatedAt": "2026-01-15T14:30:00Z" } ``` `bankDetails` is `null` in the `201` response: OMS provisions the underlying deposit account asynchronously. Subscribe to the `virtualAccount.provisioned` event (fired when `bankDetails` is populated) and `virtualAccount.active` (fired when the account is accepting deposits), or poll `GET /virtual-accounts/{virtualAccountId}` until `status` is `active` and `bankDetails` is populated. ## Deposit instructions Once the account is `active`, display its bank deposit instructions to the customer. `bankDetails` carries dual-rail instructions: a `domestic` block for funding from a US bank and a `swift` block for funding from an international bank. ```json theme={null} { "domestic": { "bankName": "OMS bank partner", "bankAddress": "...", "accountNumber": "8675309123", "routingNumber": "021000021", "accountType": "checking", "network": ["ACH", "WIRE"], "beneficiary": { "name": "Customer name", "address": { "...": "..." } } }, "swift": { "bankName": "Intermediary bank", "bankAddress": "...", "accountNumber": "...", "bic": "...", "memo": "FFC ", "beneficiary": { "name": "OMS bank partner", "address": { "...": "..." } } } } ``` On the domestic route, funds go directly to the OMS bank partner with the customer as the beneficiary. On the SWIFT route, funds route through a correspondent bank with the OMS bank partner as the beneficiary; the `memo` references the customer's provisioned account so the receiving bank credits the correct account. ## Test in sandbox In sandbox, simulate an inbound fiat transfer to exercise the auto-created transaction path without moving real funds. Call `POST /virtual-accounts/{virtualAccountId}/simulate` with a rail-discriminated body. The `rail` field selects the deposit type: `ach_in`, `wire_in`, or `swift_in`. Amounts are in minor units (cents). This endpoint returns `404` in production. ```bash theme={null} curl -X POST https://sandbox-api.polygon.technology/v0.10/virtual-accounts/va_01H9Xv.../simulate \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "rail": "ach_in", "amount": { "currency": "USD", "value": "50000" } }' ``` The SWIFT rail (`swift_in`) also takes the originator's account number and BIC, which the upstream provider requires. The response echoes the simulated deposit with a `submitted` status: ```json theme={null} { "rail": "ach_in", "virtualAccountId": "va_01H9Xv...", "amount": { "currency": "USD", "value": "50000" }, "status": "submitted", "submittedAt": "2026-01-15T10:33:00Z" } ``` The simulated inbound funds create the same auto-created transaction that a real deposit would, so this is the way to build and verify your webhook and reconciliation handling before going live. ## What OMS creates when funds arrive When OMS detects the deposit (real or simulated), it creates a transaction in `processing` status and fires the `transaction.fiatToCrypto.processing` event, with the full transaction under the envelope's `payload`. The inbound bank leg also emits `virtualAccount.deposit.*` events (`pending`, `settled`, `failed`, `returned`) as it progresses. The transaction skips the quote step, so its `precursor` is typed `virtualAccount` and carries the `virtualAccountId`. Pricing is calculated at the moment funds arrive and lives in the top-level `pricing` object. The direction is `fiatAccountToCrypto`. ```json theme={null} { "id": "txn_01H9Xd...", "object": "transaction", "status": "processing", "subStatus": "processing.fundsPulled", "customerId": "cst_01H9Xa...", "sourceToDestination": "fiatAccountToCrypto", "precursor": { "type": "virtualAccount", "details": { "virtualAccountId": "va_01H9Xv...", "depositInstructions": { "...": "..." } } }, "source": { "type": "bankUs", "category": "fiatAccount", "details": { "asset": "usd", "network": "ach" } }, "destination": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" } }, "pricing": { "source": { "asset": "usd", "amountGross": "500.00", "amountNet": "494.00", "feesDeducted": { "total": "6.00", "developer": "5.00", "oms": "1.00", "gas": "0.00" } }, "destination": { "asset": "usdc", "amountGross": "494.00", "amountNet": "494.00" }, "pair": "usd/usdc", "exchangeRate": "1.0", "effectiveRate": "0.988", "fixedAmountSide": "source", "sponsorGas": true, "sponsorGasCost": "0" }, "estimatedArrival": null, "error": null, "createdAt": "2026-01-15T14:30:00Z", "updatedAt": "2026-01-15T14:30:00Z" } ``` **What to notice:** * `precursor.type` is `virtualAccount`, and `precursor.details.virtualAccountId` links the transaction back to the originating virtual account. * `pricing.source.amountGross` is the fiat amount actually deposited, now known. * `pricing.source.feesDeducted` breaks out your developer fee and the OMS fee, both deducted from the source. * `fixedAmountSide` is `source`: the deposited amount is fixed and the crypto delivered is calculated from it. ## Track the transaction Branch on the transaction `status`. `processing` means the deposit was detected and conversion is underway; `completed` means crypto was delivered to the wallet; `failed` is a terminal failure with an `error` object (for example an ACH return). See the [transaction lifecycle](/payments/core-concepts/transaction-lifecycle) for the full status model and sub-statuses. Prefer webhooks over polling: OMS fires `transaction.fiatToCrypto.processing`, `transaction.fiatToCrypto.completed`, and `transaction.fiatToCrypto.failed` for the auto-created transaction, and each delivery carries the full transaction object under `payload`, so your handler branches on the event name or `payload.status`. See [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog. If you do poll, read the transaction directly: ``` GET /v0.10/transactions/txn_01H9Xd... Authorization: Bearer {token} ``` Or scope a listing to the customer: ``` GET /v0.10/transactions?customerId=cst_01H9Xa... Authorization: Bearer {token} ``` ## Reuse A virtual account is persistent. While it is `active` it keeps monitoring its bank account number, so every subsequent transfer triggers the same flow: a new transaction with a new `txn_` ID, the same `virtualAccountId`, and pricing computed from the same configuration. There is no limit on the number of transactions a single virtual account can produce. ## Status lifecycle | Status | Meaning | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | Created; OMS is provisioning the underlying deposit account. `bankDetails` is still null. | | `active` | The account number is live. Deposits convert and deliver to the destination. | | `frozen` | Deposits are suspended. `statusReason` explains why. | | `closed` | The account is permanently closed. | | `deleted` | The asynchronous delete finished and the underlying deposit account is closed. `finalBalance` snapshots the balance at the moment of deletion. | | `failed` | Provisioning failed; `failureReason` identifies the category. Create a new virtual account. | | `inactiveActionRequired` | The destination external account is no longer usable. Re-point `destination` with `PATCH` to recover to `active`. | ## Manage virtual accounts ### List `GET /virtual-accounts` spans every customer in your organization. Filter with `customerId` and `status`, both optional. Paginate with `limit`, `startingAfter`, and `endingBefore`: ``` GET /v0.10/virtual-accounts?customerId=cst_01H9Xa...&status=active&limit=20 Authorization: Bearer {token} ``` The response is a list envelope: `{ object, data, hasMore, nextCursor, previousCursor }`. Pass `nextCursor` as `startingAfter` to fetch the next page, or `previousCursor` as `endingBefore` to page backward; `hasMore` signals whether more rows exist in the direction of travel. ### Update `PATCH /virtual-accounts/{virtualAccountId}` accepts `destination`, `sponsorGas`, `label`, and `metadata`. Any other key in the body is rejected with `400`. ```bash theme={null} curl -X PATCH https://sandbox-api.polygon.technology/v0.10/virtual-accounts/va_01H9Xv... \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: va-repoint-001" \ -d '{ "destination": { "type": "walletExternal", "details": { "id": "ext_wlt_01H9Xh...", "asset": "usdc", "network": "polygon" } } }' ``` Re-pointing `destination` to a healthy external account recovers a virtual account from `inactiveActionRequired` back to `active`. A re-point on an already `active` account updates the target without a status change. ### Delete `DELETE /virtual-accounts/{virtualAccountId}` returns `202`: deletion is asynchronous. OMS initiates the close of the underlying deposit account, sets `deletionRequestedAt` (and records the caller in `deletionRequestedBy`), and leaves `status` as-is during the delete-pending window. Once the underlying account closes, `status` finalizes to `deleted` and `finalBalance` records the balance at that moment. ``` DELETE /v0.10/virtual-accounts/va_01H9Xv... Authorization: Bearer {token} ``` The `202` response is the virtual account with `deletionRequestedAt` set. Poll `GET /virtual-accounts/{virtualAccountId}` to observe the transition to `deleted`. ## Deposit address vs. virtual account Both are persistent auto-route configurations. The difference is which side is fiat: | | Deposit address | Virtual account | | --------------- | -------------------------------- | ---------------------------------------- | | Incoming funds | Crypto (on-chain) | Fiat (bank rail) | | Deposit details | On-chain inlet address | Bank account number and routing | | Direction | `cryptoToFiatAccount` | `fiatAccountToCrypto` | | Destination | Registered bank external account | OMS wallet or registered external wallet | ## Related * [Virtual accounts overview](/payments/virtual-accounts): concept summary and comparison with deposit addresses * [Deposit addresses guide](/payments/guides/deposit-addresses): the crypto equivalent for on-chain deposits * [Transaction lifecycle](/payments/core-concepts/transaction-lifecycle): statuses, sub-statuses, and webhook events for the auto-created transaction # On-ramps & Off-ramps Source: https://docs.polygon.technology/payments/onramps-offramps Convert between fiat and stablecoins via OMS or third-party providers on Polygon. The Open Money Stack provides regulated, enterprise-grade on-ramp and off-ramp infrastructure with KYC, AML, and compliance built in. Third-party providers are also available for teams not using OMS directly. ## OMS on-ramps and off-ramps OMS handles both directions. The quote-and-execute pattern applies to crypto sends and payouts; cash-in and virtual accounts auto-create their transactions when funds arrive. Fund a wallet from a bank account or cash at a retail location and deliver USDC to a wallet on Polygon. Pay out USDC from a custodial wallet to a bank account, card, or cash pickup, or to any wallet or address. Let a customer deposit physical cash at a retail location and receive USDC in their wallet. Give a customer a dedicated bank account number that auto-converts incoming fiat to USDC. Supported bank rails include ACH, same-day ACH, wire, and RTP, plus IBAN for international and Canadian bank accounts. Card funding and payouts are available, as is cash: deposits at 50,000+ retail locations across 48 US states, and cash pickup for payouts (capped at \$400 per transaction). Request access to the OMS on-ramp and off-ramp API. *** ## Third-party providers For teams building directly on Polygon without OMS, the following providers support Polygon on-ramps and off-ramps. Polygon does not endorse or provide support for third-party providers. Do your own due diligence before integrating any service. ### On-ramp providers Convert fiat to crypto on Polygon via card, bank transfer, or local payment methods: | Provider | Coverage | Payment methods | | ----------------------------------------------------------------------- | -------------- | -------------------------------- | | [MoonPay](https://dev.moonpay.com/docs/on-ramp-overview) | Global | Card, bank transfer | | [Transak](https://docs.transak.com/) | 160+ countries | Card, bank, local methods | | [Ramp](https://docs.rampnetwork.com/) | Global | Card, bank, Apple/Google Pay | | [Bridge](https://apidocs.bridge.xyz/) | US + global | Stablecoin-focused, programmable | | [Revolut](https://www.revolut.com/en-US/) | Revolut users | Revolut account | | [Crossmint](https://crossmint.io/) | Global | Credit card, NFT/token checkout | | [BlindPay](https://blindpay.com/docs/getting-started/quick-start-payin) | Global | Privacy-focused | | [Payy](https://payy.link/) | Global | Payment link, checkout | | [Avenia](https://avenia.io/) | Latin America | Local payment methods | | [Mercuryo](https://oor-redirect.redoc.ly/#section/About) | Global | Card, bank transfer | | [Electrocoin](https://electrocoin.eu) | Europe | Local methods | See the [Stripe on-ramp tutorial](/payment-services/stablecoins/onramps-stripe) for a step-by-step integration with Stripe's Crypto On-ramp API. ### Off-ramp providers Convert crypto on Polygon to fiat via bank transfer, card payout, or cash: | Provider | Coverage | Payout methods | | -------------------------------------------------------- | ------------- | -------------------------- | | [MoonPay](https://dev.moonpay.com/docs/on-ramp-overview) | Global | Bank transfer, card payout | | [Transak](https://docs.transak.com/) | 60+ countries | Bank transfer | | [Ramp](https://docs.rampnetwork.com/) | Global | Bank transfer | | [Bridge](https://apidocs.bridge.xyz/) | US + global | Stablecoin-to-fiat | | [Mercuryo](https://oor-redirect.redoc.ly/#section/About) | Global | Card, bank | | [Revolut](https://www.revolut.com/en-US/) | Revolut users | Revolut account | # Open Money Stack Payments API Source: https://docs.polygon.technology/payments/overview Open Money Stack Payments API: fiat-to-crypto and crypto-to-fiat on-ramps, custodial wallets, compliance, and stablecoin orchestration in a single integration. ACH, wire, SWIFT, cash, and card rails. The Open Money Stack (OMS) Payments API moves money between fiat and stablecoins. It provides the full infrastructure stack: identity, custodial wallets, compliance, and fiat rail access, all integrated so they hand off cleanly to each other. One integration covers crypto-to-crypto, fiat-to-crypto, and crypto-to-fiat money movement across ACH, wire, SWIFT, cash, and card rails. OMS infers the direction (`sourceToDestination`) from the instruments on each side of a transaction. Onboard a customer, provision a wallet, and make your first transaction. Full endpoint reference: transactions, quotes, wallets, customers, webhooks. *** ## Core concepts The full resource model: customers, wallets, quotes, transactions, cash-ins, virtual accounts, deposit addresses, counterparties, and external accounts, and how they relate. How OMS locks pricing, structures fees, and calculates exchange rates before you commit to a transaction. Custodial wallets, virtual bank accounts, deposit addresses, and external accounts. Statuses, sub-statuses, webhook events, and auto-created transactions from deposit flows. Supported assets, networks, and fiat rails: ACH, SEPA, PIX, UPI, SPEI, cash networks, and stablecoins. *** ## Use cases Common products built on the Open Money Stack. Each card links to a step-by-step walkthrough. Give users a real USD account number that receives ACH transfers and holds a stablecoin balance. Pay contractors, suppliers, and recipients from a single treasury wallet, via bank rails or cash pickup. Fund a wallet with cash or bank rails, hold a USDC balance, and withdraw back to a bank account. Drop USDC rewards and cashback straight into user wallets. No card networks, no breakage, no expiry. Fiat in one country, fiat delivered in another, settled via Polygon in seconds. *** ## OMS primitives The OMS API is managed through a core set of resources. Every transaction, deposit, and disbursement is built from these. An identity record whose `endorsements` (`basic`, `cryptoCustody`, `usd`) gate access to financial operations. Every wallet belongs to a customer. Custodial or non-custodial stablecoin balances on Polygon. Created under a customer with `POST /customers/{customerId}/wallets`. Source or destination for any transaction. A rate lock with full fee breakdown. Created before every transaction. Expires if not executed within the validity window. Execute a quoted money movement. OMS infers the direction (`sourceToDestination`) from the instruments. Track status via webhooks through processing to completed. A code-based deposit flow for in-person cash funding at retail locations. Auto-creates a transaction on confirmation. Subscribe to events as they happen. Full CRUD: create, list, update, and delete subscriptions with `POST/GET/PATCH/DELETE /webhooks`, or manage them in the OMS Dashboard. ### Deposit and payout resources These resources extend the core model with reusable deposit configurations and off-platform funding and payout references. You create and manage them directly through the API. Transactions reference these resources by ID; when funds arrive at a deposit address or virtual account, OMS auto-creates the transaction. Deposit addresses must be enabled for your project: [contact us](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=note\&utm_campaign=oms_access) to enable them. A dedicated bank account number assigned to a customer. Incoming fiat auto-converts to a stablecoin at the configured destination. Create and manage with `POST/GET/PATCH/DELETE /virtual-accounts`. A reusable onchain address for a customer. Incoming crypto auto-triggers a transaction to a registered bank account. Create and manage with `POST/GET/PATCH /deposit-addresses`. Off-platform banks, external wallets, and cards. Register them with `POST /external-accounts` and reference them by `ext_` ID as a quote source or destination. A third party that is not your customer but owns external accounts you pay, for example a vendor. Full CRUD via `/counterparties`. *** ## Why Polygon for settlement * **Sub-2-second finality** with 99.9%+ network uptime * **\$0.002 average transaction cost** on Polygon Chain * **\$54B+ in stablecoin transfer volume** processed onchain * **Native USDC**: no wrapping, no bridging, no surprise deductions * **Compliance included**: KYC, KYB, AML screening, and transaction monitoring across 48 US states and international corridors # Explore Money Flows Source: https://docs.polygon.technology/payments/payment-flows Interactive visualization of OMS payment flows and transaction routing. # Payment Providers Source: https://docs.polygon.technology/payments/payment-providers Directory of payment infrastructure projects supporting Polygon. The following payment providers have integrations with Polygon Chain. Coverage spans on-ramps, off-ramps, card processing, checkout, stablecoin settlement, and embedded fiat-to-crypto flows. OMS is the regulated full-stack option. The providers below are public integrations with their own onboarding and documentation paths. ## Enterprise & custodial | Provider | Category | Details | | ---------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Open Money Stack | On-ramp, off-ramp, custody, compliance | Full-stack regulated infrastructure: on-ramp, off-ramp, custodial and non-custodial wallets, KYC/AML, cash kiosks. [Request OMS access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=table\&utm_campaign=oms_access) | ## On-ramps & off-ramps | Provider | Category | Details | | ----------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------- | | [Stripe](https://docs.stripe.com/crypto/stablecoin-payments) | On-ramp | Card and bank transfer to USDC on Polygon. [Tutorial](/payment-services/stablecoins/onramps-stripe) | | [MoonPay](https://dev.moonpay.com/docs/on-ramp-overview) | On-ramp, off-ramp | Card and bank, global coverage | | [Transak](https://docs.transak.com/) | On-ramp, off-ramp | 160+ countries, local payment methods | | [Ramp](https://docs.rampnetwork.com/) | On-ramp, off-ramp | Card, bank, Apple/Google Pay | | [Bridge](https://apidocs.bridge.xyz/) | Stablecoin rails | Programmable stablecoin-to-fiat and fiat-to-stablecoin | | [Revolut](https://www.revolut.com/en-US/) | On-ramp, off-ramp | Revolut account holders | | [Mercuryo](https://oor-redirect.redoc.ly/#section/About) | On-ramp, off-ramp | Card and bank, global | | [BlindPay](https://blindpay.com/docs/getting-started/quick-start-payin) | On-ramp | Privacy-focused | | [Payy](https://payy.link/) | Payment links | Checkout and payment link flows | | [Avenia](https://avenia.io/) | On-ramp | Latin America focus | | [Electrocoin](https://electrocoin.eu) | On-ramp, off-ramp | European markets | ## eCommerce & checkout | Provider | Category | Details | | -------------------------------------------- | --------- | ----------------------------------------- | | [Shopify](https://depay.com/plugins/shopify) | eCommerce | Crypto checkout via DePay plugin | | [Crossmint](https://crossmint.io/) | Checkout | Credit card purchases for tokens and NFTs | ## Stablecoin issuers | Stablecoin | Issuer | Details | | ---------- | ---------------------------------------- | ------------------------------------------- | | USDC | [Circle](https://developers.circle.com/) | Native on Polygon; CCTP cross-chain support | | USDT | [Tether](https://tether.to/) | Native on Polygon | | DAI | [MakerDAO](https://docs.makerdao.com/) | Decentralized stablecoin | | AUSD | [Agora](https://www.agora.finance/) | Native on Polygon | ## Agentic payments For AI agent and autonomous payment flows (x402, LLM wallets, per-use API billing), see the [Agentic tab](/payment-services/index) for full documentation. # Stablecoins Source: https://docs.polygon.technology/payments/stablecoins Overview of stablecoins available on Polygon Chain, with integration guides for USDC and cross-chain transfers. Polygon Chain supports 50+ stablecoins covering USD, EUR, GBP, and local currencies. All are available as native ERC-20 tokens. USDC additionally supports cross-chain transfers via Circle's Cross-Chain Transfer Protocol (CCTP). ## Stablecoins available on Polygon Search by market, stablecoin ticker, issuer, or contract address. Click a truncated address to view on Polygonscan, or copy the full address with the copy button. This list is not exhaustive. ## USDC integration USDC is the most commonly used stablecoin for payment applications on Polygon. There are two integration paths depending on whether your use case is single-chain or cross-chain. **Native USDC** is a standard ERC-20 token on Polygon. Use it for payments, balances, and transfers within Polygon. **Gateway USDC (CCTP)** uses Circle's Cross-Chain Transfer Protocol to move USDC between chains by burning on the source chain and minting on the destination. Use it for cross-chain settlement. Read balances and transfer native USDC on Polygon using viem. Deposit, attest, and mint USDC cross-chain via Circle's CCTP gateway. ## On-ramps to stablecoins To help users acquire stablecoins from fiat, see the [On-ramps & off-ramps](/payments/onramps-offramps) page for the full list of supported providers, including Stripe, MoonPay, and Transak. # Supported Jurisdictions Source: https://docs.polygon.technology/payments/supported-jurisdictions-networks Jurisdiction eligibility for Polygon OMS, including prohibited countries and how restrictions are enforced. Polygon OMS supports customers and transactions in most jurisdictions. The jurisdictions listed below are prohibited: associated transactions and customers are blocked. ## Residency requirements Using US bank rails does not require US residency or a US entity. Foreign entities onboard directly through KYB and receive US bank rails in their own legal name. The jurisdictional floor is the restricted lists below; there are no end-user nationality restrictions for beneficiaries beyond them. The cash and card retail ramps serve US persons, which is separate from the bank-rail corridor. ## Sanctioned and blacklisted countries Applies to all OMS services: Dashboard, API, non-custodial wallet SDK, and Trails SDK. Cuba, Iran, Myanmar, North Korea, Ukraine (Crimea, Donetsk, Luhansk, Kherson, and Zaporizhzhia regions only) China is also blocked at the IP level. ## FATF gray list, blocked for bank ramps In addition to the sanctioned countries above, the following jurisdictions are blocked for bank ramp transactions: Algeria, Angola, Bolivia, Bulgaria, Cameroon, Côte d'Ivoire, Democratic Republic of Congo, Haiti, Kenya, Kuwait, Laos, Lebanon, Monaco, Namibia, Nepal, Papua New Guinea, South Sudan, Syria, Venezuela, Vietnam, Virgin Islands (UK), Yemen The Russian Federation is blocked for SDK access (non-custodial wallet SDK and Trails SDK). ## How restrictions are enforced Prohibition is enforced against the following per transaction or customer: * Business applicant's incorporation address or physical address * Associated person's physical address * SWIFT counterparty address country * SWIFT counterparty bank BIC (inbound or outbound) If a counterparty's citizenship, nationality, or passport country falls in a prohibited jurisdiction, proof of address and proof of source of funds are required before the transaction can proceed. # Transactions Source: https://docs.polygon.technology/payments/transactions The core money movement operation in OMS: quote, then execute. A transaction moves money through OMS. You specify a source instrument and a destination instrument on the quote; OMS infers the direction from those two sides and reports it as `sourceToDestination`. There is no settable transaction "type". Standard sends and payouts follow a two-step flow: create a quote to lock pricing, then create a transaction to execute it. Cash-in and auto-created transactions (virtual accounts, deposit addresses) skip the quote step. ## Direction (`sourceToDestination`) The `sourceToDestination` field is a composite of the source and destination instrument categories, inferred from each side: | Value | Source | Destination | | --------------------- | ----------------------------------- | --------------------------------------- | | `cryptoToCrypto` | OMS wallet | OMS wallet or external on-chain address | | `cryptoToFiatAccount` | OMS wallet | Registered bank external account | | `cryptoToCash` | OMS wallet | Cash pickup (early access) | | `fiatAccountToCrypto` | Bank account, via a virtual account | OMS wallet | | `cashToCrypto` | Cash, via a cash-in | OMS wallet | ## Two-step flow ``` POST /quotes → quote in "open" status (rate locked) │ POST /transactions → transaction in "processing" │ completed | failed ``` The quote locks the exchange rate and fee breakdown for a short validity window. To execute it, call `POST /transactions` with a body of `{ "quoteId": "qt_..." }`. If the quote expires before you create the transaction, create a new quote. ## Quote object ```json theme={null} { "id": "qt_...", "object": "quote", "status": "open", "customerId": "cst_...", "sourceToDestination": "cryptoToCrypto", "source": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_...", "asset": "usdc", "network": "polygon" } }, "destination": { "type": "walletExternal", "category": "crypto", "details": { "blockchainAddress": "0x...", "asset": "usdc", "network": "polygon" } }, "pricing": { "source": { "asset": "usdc", "amountGross": "100.00", "amountNet": "99.42", "feesDeducted": { "total": "0.58", "developer": "0.20", "oms": "0.35", "gas": "0.03" } }, "destination": { "asset": "usdc", "amountGross": "99.42", "amountNet": "99.42" }, "pair": "usdc/usdc", "exchangeRate": "1.0", "effectiveRate": "0.9942", "fixedAmountSide": "source" }, "expiresAt": "2025-01-15T10:35:00Z" } ``` Set `pricing.fixedAmountSide` to `"source"` when the user specifies how much to send, or `"destination"` when the user specifies how much to receive. The `source` and `destination` are typed instruments discriminated by `type`: an OMS wallet (`walletOms`), an externally-custodied wallet (`walletExternal`), a bank account (`bankUs`, `bankIban`, `bankCanada`), a debit card (`card`), or cash pickup (`cash`). ## Transaction statuses | Status | Meaning | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `processing` | Executing. Funds are being pulled or converted. | | `awaitingAction` | Non-terminal. Blocked on developer, upstream, or compliance action; see the `hold` object. Returns to `processing` once cleared. | | `completed` | Funds delivered to the destination. | | `failed` | Terminal failure. The `error` object describes the cause. | OMS fires a webhook on every status change. Each delivery carries the full transaction object, so your handler branches on the `status` field rather than parsing an event-name string. Subscribe with the Webhooks endpoints (`POST /webhooks` with a body of `{ url, events }`; omit `events` or pass `["*"]` for all events) or in the OMS Dashboard. See the [transaction lifecycle](/payments/core-concepts/transaction-lifecycle) for the delivery model. ## Developer fees Developer fees are configurable per integration and are never shown to the end user. Set them on your OMS account or pass them in the quote request. They appear in the `feesDeducted.developer` field on both the source and destination sides of the `pricing` object. ## Key operations | Operation | Endpoint | | -------------------- | ----------------------------------- | | Create a quote | `POST /quotes` | | Get a quote | `GET /quotes/{quoteId}` | | Create a transaction | `POST /transactions` | | Get a transaction | `GET /transactions/{transactionId}` | | List transactions | `GET /transactions` | `GET /transactions` returns results newest-first and filters by `status`, `sourceToDestination`, `customerId`, the originating instrument (`walletId`, `virtualAccountId`, `depositAddressId`, `cashInId`), and inclusive `createdAfter`/`createdBefore` date bounds. Pagination uses `limit`, `startingAfter`, and `endingBefore`. All `POST` endpoints accept an `Idempotency-Key` header. Use a stable key tied to your internal order ID to safely retry on network failure without risk of double-execution. ## Related * [Fiat to crypto guide](/payments/guides/fiat-to-crypto): cash-in and virtual-account funding * [Send from a wallet guide](/payments/guides/crypto-to-fiat): crypto sends and bank payouts * [Bank transfers guide](/api-reference/guide-bank-transfers): ACH and wire transfer details # Consumer on-ramp and off-ramp Source: https://docs.polygon.technology/payments/use-cases/consumer-on-offramp Fund a crypto wallet via cash-in or bank rails, and withdraw back to a bank. A user funds their wallet by paying in cash at any of 50,000+ retail locations, which OMS converts to USDC. They hold the USDC on Polygon. When they want to move money out, they can send it onchain to another wallet or external blockchain address, or withdraw it back to a bank account. Funding via bank rails uses a dedicated virtual account for inbound ACH. This is the core loop for any consumer crypto product: move money in, hold it onchain, move it out. OMS handles the fiat rails, conversion, and compliance on both sides. **Who this is for:** * Consumer apps that want to offer crypto-backed savings or spending accounts * Crypto exchanges and brokers handling retail on/off-ramp volume * DeFi frontends that need fiat on-ramps for users without existing crypto holdings * Apps onboarding cash-preferring or unbanked users who fund without a bank account or card Cash-in funding, crypto-to-crypto sends, bank funding, and bank payouts all run on the OMS API today. Create a virtual account for inbound ACH with `POST /virtual-accounts`, and register the customer's bank account for payouts with `POST /external-accounts`. In sandbox you can exercise inbound ACH with the virtual-account simulate endpoint. The OMS API is in early access: [request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=note\&utm_campaign=oms_access) for an API key. *** ## How it works
On-ramp flow
1 App OMS POST /cash-ins (cash funding), or fund via a Virtual Account for inbound ACH
2 OMS App Quote with rate, fees, and expiry window
3 App OMS POST /transactions (quoteId)
4 OMS Receives fiat cash, or ACH to a Virtual Account, converts to USDC, credits wallet
5 OMS App Webhook: transaction.fiatToCrypto.completed
Off-ramp flow
1 App OMS POST /quotes (wallet source, bank destination)
2 OMS App Quote with rate, fees, and expiry window
3 App OMS POST /transactions (quoteId)
4 OMS Pulls USDC from wallet, initiates ACH or wire transfer to the external account
5 OMS App Webhook: transaction.cryptoToFiat.completed
Both directions use the same quote-then-execute flow. The source is always an OMS wallet, and the customer agrees to a rate and fee breakdown before any money moves. Rates lock at quote time for a short window, so the customer sees exactly what they'll receive. To move money out as a crypto-to-crypto send, run the same `POST /quotes` then `POST /transactions` flow: the source is the customer's wallet and the destination is another wallet or an external blockchain address. Bank payouts follow the flow above, with the destination referencing an external account, registered with `POST /external-accounts`, by its `ext_` identifier. *** ## OMS resources | Resource | Role | | --------------------------------- | ---------------------------------------------------------------------------------------------- | | Customer | Identity record with KYC and endorsements | | Wallet | Custodial USDC balance | | Virtual account | Dedicated account number for inbound ACH funding, created with `POST /virtual-accounts` | | External account | Customer's bank account, registered with `POST /external-accounts` and referenced by `ext_` ID | | Quote | Rate lock with fee breakdown before execution | | `fiatAccountToCrypto` transaction | Converts inbound fiat to USDC and credits the wallet | | `cashToCrypto` transaction | Converts cash-in funding to USDC and credits the wallet | | `cryptoToCrypto` transaction | Sends USDC from the wallet to another wallet or external blockchain address | | `cryptoToFiatAccount` transaction | Pulls USDC from the wallet, delivers fiat via ACH or wire | *** ## Implementation Create customer records, collect KYC, and provision wallets. Quote and execute cash-in funding and ACH on-ramps via virtual accounts. Quote and execute ACH or wire off-ramps. Full walkthrough of both funding and withdrawal flows. # Stablecoin cross-border remittance Source: https://docs.polygon.technology/payments/use-cases/cross-border-remittance Build cross-border remittance products on stablecoin rails: fiat in, fiat out, settled on Polygon in minutes instead of days. Skip the correspondent banking chain and the 5-7% fee. A sender deposits cash or pays by card in their home country. The recipient receives local currency in their bank account, mobile wallet, or cash pickup location in another country. Polygon handles settlement in the middle. Total time from send to delivery: under two minutes. Traditional correspondent banking takes 2-5 days and charges 5-7% in fees. Using Polygon as the settlement layer eliminates the correspondent chain entirely. **Who this is for:** * Remittance services targeting high-volume corridors (US-MX, US-PH, EU-NG) * Neobanks serving diaspora communities who send money home regularly * Mobile money operators looking to add international send capability *** ## How it works
Sender
fiat deposit
card/ACH/cash
OMS
fiat in
USDC
Polygon
\~2s finality
USDC
OMS
fiat out
local fiat
Recipient
bank/cash
The rate is locked at quote time, so both sender and recipient know exactly what will be delivered before any money moves. The sender is an OMS customer. The recipient is a counterparty, created with `POST /counterparties`, whose bank account is registered with `POST /external-accounts` and referenced on the payout quote by its `ext_` identifier. OMS handles KYC and compliance checks for the corridor. *** ## Planned capabilities * Card, ACH, and cash-in on the sender side * ACH, SWIFT, wire, and cash pickup on the recipient side * Multi-currency corridors with locked rates at quote time * Developer fee configuration per corridor * Compliance and KYC handled by OMS at every step *** Global remittance is under active development and available through early access. Contact us to enable it for your project. Share your use case and we'll reach out when global remittance is available. # USD stablecoin accounts for global users Source: https://docs.polygon.technology/payments/use-cases/dollar-accounts Give customers in any country a real USD account number for ACH transfers, backed by USDC on Polygon. Dollar banking and stablecoin-backed accounts without a US bank or correspondent banking chain. A user anywhere in the world gets a real USD account number: routing number, account number, the works. They receive ACH transfers from US employers, marketplaces, or family members. The balance sits in USDC on Polygon. They withdraw to their local bank whenever they want. No US bank account required. No correspondent banking delays. The account behaves like a standard US bank account from the sender's perspective. **Who this is for:** * Neobanks and fintech apps serving emerging-market users who need USD-denominated accounts * Freelancer platforms whose contractors need a US bank account to receive client payments * Marketplaces that want to offer USD wallet balances to international sellers Create each customer's virtual account with `POST /virtual-accounts`; OMS assigns the routing and account number once the account is provisioned. Virtual accounts must be enabled for your project, and the OMS API is in early access: [request access](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=note\&utm_campaign=oms_access) for an API key. In sandbox you can exercise incoming deposits with the virtual-account simulate endpoint. See the [Virtual accounts](/payments/guides/virtual-accounts) guide. *** ## How it works
Dollar account flow
1 App OMS Onboard customer, provision wallet, create virtual account
2 OMS App Unique routing + account number assigned to this customer
3 Payer Account Sends ACH from any US bank
4 OMS Converts USD deposit to USDC, credits customer wallet
5 OMS App Webhook: transaction.fiatToCrypto.completed
6 Customer Withdraws USDC to local bank via ACH, wire, or SWIFT
Each customer gets their own dedicated account number. When a payer sends an ACH transfer to that number, OMS automatically converts the USD to USDC and credits the customer's custodial wallet. The customer can hold the balance or withdraw at any time. *** ## OMS resources | Resource | Role | | --------------------------------- | ------------------------------------------------------------------------------------------- | | Customer | Identity record with KYC and endorsements | | Wallet | Custodial USDC balance | | Virtual account | Dedicated US bank account number (routing + account), created with `POST /virtual-accounts` | | `fiatAccountToCrypto` transaction | Auto-created on each incoming deposit | | `cryptoToFiatAccount` transaction | Created when customer initiates a withdrawal | *** ## Implementation Create customer records, collect KYC, and provision wallets. Assign a persistent USD account number and handle incoming deposits. Quote and execute outbound ACH and wire withdrawals. Convert USDC to fiat for withdrawal via OMS. # Payroll and contractor disbursements Source: https://docs.polygon.technology/payments/use-cases/payroll-disbursements Pay workers and contractors in their local currency from a single USDC treasury wallet. A platform holds USDC in a treasury wallet and needs to pay out to dozens or hundreds of recipients across different countries. Each recipient gets their local currency via ACH, wire, or SWIFT. OMS handles conversion and delivery per recipient in parallel, from a single disbursement run. This replaces the operational overhead of managing multiple banking relationships and currency accounts. One wallet, one API, every corridor. Recipients without a bank account can be paid out to physical cash, covered in [Cash disbursements](#cash-disbursements) below. **Who this is for:** * Gig economy platforms paying drivers, couriers, or freelancers across markets * Global payroll providers managing contractor payments in multiple currencies * Marketplaces that settle earnings to international sellers on a weekly or monthly basis *** ## How it works
Per-recipient disbursement
1 App OMS POST /quotes per recipient (treasury wallet → recipient bank)
2 OMS App Rate + fee per recipient locked for confirmation window
3 App OMS POST /transactions per recipient (execute quoted disbursement)
4 OMS Pulls USDC from treasury, initiates ACH/wire/SWIFT per recipient
5 OMS App Webhook: transaction.cryptoToFiat.completed per transaction
Each payout is a separate quote-and-execute pair, with the recipient's external bank account as the destination. On a quote, an external account is referenced by its `ext_` identifier (for example `ext_bankUs_...`, `ext_bankIban_...`, `ext_bankCa_...`). All quotes for a disbursement run can be created in parallel. Idempotency keys on each transaction make retries safe if a run is interrupted. Register each recipient once before their first payout. Create a counterparty with `POST /counterparties` (the recipient's name plus optional contact and address details), then register their bank account with `POST /external-accounts`, setting the owner to that counterparty. The account starts `pending` and becomes `active` once provisioning succeeds; reference its `ext_` identifier on every subsequent quote. *** ## OMS resources | Resource | Role | | --------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Platform wallet | Central USDC treasury that funds all disbursements | | Counterparty (per recipient) | Named third-party recipient, created with `POST /counterparties` | | External account (per recipient) | Recipient's bank account for delivery, registered with `POST /external-accounts` and referenced by `ext_` ID | | Quote | Locked rate + fee for one recipient's payout | | `cryptoToFiatAccount` transaction | Executes one recipient's bank disbursement | | `cryptoToCash` transaction | Executes one recipient's cash disbursement | *** ## Cash disbursements Recipients without a bank account can be paid out to physical cash. OMS converts the wallet balance, issues a time-limited pickup code, and the recipient collects bills at a retail counter or ATM. This is the disbursement counterpart to [cash-in funding](/payments/use-cases/consumer-on-offramp): together they form a complete cash loop, accepting cash into digital wallets and paying digital balances back out as cash. A cash disbursement uses the same payout model as a bank transfer. Instead of an external bank account, the destination is a cash network, and the recipient redeems a pickup code rather than receiving an ACH or wire.
Cash disbursement flow
1 App OMS POST /quotes (cryptoToCash, amount on the cash destination)
2 OMS App Pulls USDC from wallet, issues time-limited pickup code + nearest location
3 Recipient Retail Present code at counter or ATM, collect cash
4 OMS Automatically refunds the USDC to the wallet if the code expires unused
Cash disbursements suit US fintech apps paying customers who prefer cash over ACH or card, remittance recipients in cash-dependent corridors, and relief or gig platforms paying recipients without bank accounts. The amount on a cash payout must be a multiple of $20.00, up to a maximum of $400.00 per transaction. *** Bank and cash payouts run on the quote-and-execute flow available today; see [Pay a third party](/payments/guides/third-party-payouts) for the per-recipient walkthrough. Bulk-payout tooling for large disbursement runs is on the roadmap; for now, create and execute each payout individually. To be notified when bulk tooling launches, register your interest. Share your use case and we'll reach out when bulk-payout tooling is available. # Stablecoin rewards and loyalty Source: https://docs.polygon.technology/payments/use-cases/rewards-loyalty Drop USDC rewards, cashback, and loyalty balances directly into user wallets on Polygon. Programmable stablecoin rewards with no card networks, no breakage, and no expiry. A platform wants to reward users with something they can actually spend. Instead of points that expire or card-network cashback that takes a billing cycle to post, OMS drops USDC straight into each user's wallet. Recipients hold a real balance they can spend, hold, or withdraw. No breakage, no float, no expiry. A reward drop is a `cryptoToCrypto` transfer: USDC moves from your treasury wallet to each recipient's wallet on Polygon, settling in seconds. Because the asset matches on both sides, there is no exchange-rate spread, and the quote returns the exact fees before you commit. **Who this is for:** * Consumer apps and loyalty programs issuing rewards as spendable balances * Fintech card issuers and debit programs paying stablecoin cashback * Gaming and promotion platforms running incentive drops * Creator platforms distributing bonuses or tips to many recipients at once *** ## How it works
Reward drop flow
1 App OMS POST /quotes (treasury wallet → recipient wallet, USDC → USDC)
2 OMS App Rate locked, full fee breakdown returned
3 App OMS POST /transactions (execute the drop)
4 OMS Pulls USDC from treasury, settles to recipient wallet on Polygon
5 OMS App Webhook: transaction.cryptoToCrypto.completed
Each drop is a single quote-and-execute pair. For a campaign, create the quotes in parallel and execute each as it confirms. Idempotency keys on every transaction make a reward run safe to retry without paying anyone twice. The destination can be a recipient's OMS wallet or any external on-chain address. *** ## OMS resources | Resource | Role | | ---------------------------- | -------------------------------------------------------- | | Treasury wallet | Central USDC wallet the rewards are funded from | | Customer (per recipient) | Identity record that owns each recipient wallet | | Recipient wallet | Destination USDC wallet, or an external on-chain address | | Quote | Locked fee breakdown for one drop | | `cryptoToCrypto` transaction | Executes one reward drop | *** ## Beyond reward drops **Cashback** is a reward drop on a trigger. Fire a drop for each qualifying purchase or event, and the recipient's balance updates as they spend. The flow is identical: quote, then execute a `cryptoToCrypto` transfer. **Streaming pay** and **tokenized loyalty points** go beyond the Payments API. Per-second streaming uses onchain payment channels, and programmable points are token contracts you deploy and redeem across merchants. Both run on [Polygon Chain](/pos/get-started/building-on-polygon) rather than through OMS quotes and transactions. Reward drops run on the same quote-and-execute flow available today. Bulk drop tooling for large campaigns is on the roadmap alongside [bulk payouts](/payments/use-cases/payroll-disbursements). *** ## Build it Quote and execute a USDC transfer between wallets. Create the customer and wallet each recipient needs. The quote-then-execute reference for cryptoToCrypto. Provision and fund the treasury and recipient wallets. # Virtual accounts Source: https://docs.polygon.technology/payments/virtual-accounts Dedicated bank account numbers that auto-convert incoming fiat to crypto. A virtual account gives a customer a dedicated bank account number. When fiat arrives via a supported rail, OMS automatically creates a transaction (`fiatAccountToCrypto`) and delivers crypto to the customer's configured wallet. No developer action is required after the account is provisioned. Virtual accounts are persistent. OMS keeps them active until they are frozen, closed, or deleted. Virtual accounts must be enabled for your project: OMS provisions the underlying deposit accounts with a virtual account provider configured during onboarding. To get set up, share your use case below. Tell us about your on-ramp flow and we'll enable virtual accounts for your project. ## How it works Once a virtual account is provisioned, you display its bank deposit instructions to the customer. When fiat arrives at that account, OMS: 1. Detects the inbound deposit on the bank rail. 2. Creates a transaction with a typed `precursor` of `virtualAccount`, carrying the `virtualAccountId` and its bank routing block. 3. Moves the transaction directly to `processing` (there is no quote step, because pricing cannot be locked before the funds arrive). 4. Converts the deposited fiat to crypto and delivers it to the configured wallet. The resulting transaction carries `sourceToDestination: "fiatAccountToCrypto"` and follows the standard [transaction lifecycle](/payments/core-concepts/transaction-lifecycle), including webhook events. ## Creating a virtual account Create a virtual account with `POST /virtual-accounts`: ```json theme={null} { "customerId": "cst_...", "source": { "asset": "usd", "network": "usBank" }, "destination": { "type": "walletOms", "details": { "id": "wlt_...", "asset": "usdc", "network": "polygon" } }, "accountHolder": "customer", "type": "bankUs", "label": "USD funding account" } ``` | Field | Required | Meaning | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `customerId` | Yes | The customer the account belongs to. | | `source` | Yes | The inbound fiat rail. `{ "asset": "usd", "network": "usBank" }` are the only supported values today. | | `destination` | Yes | Where converted crypto is delivered: `walletOms` (an OMS wallet) or `walletExternal` (a registered [external account](/payments/external-accounts), referenced by its `ext_wlt_` ID only; raw on-chain addresses are not accepted). OMS validates the `details` (asset and network) against the resolved account. | | `accountHolder` | Yes | Must be `customer`. | | `type` | Yes | Must be `bankUs`. | | `bankMemo` | No | A wire/ACH memo the customer can include with deposits. | | `sponsorGas` | No | When `true` (the default), OMS absorbs the on-chain gas cost for the destination delivery. Only `true` is currently supported. | | `label`, `metadata` | No | A display label and free-form string metadata. | The `201` response returns the virtual account with `bankDetails: null`. OMS provisions the underlying deposit account asynchronously, and the account moves from `pending` to `active` once the bank details are ready. ## Deposit instructions The `bankDetails` field carries dual-rail bank deposit instructions to display in your UI. It is null until the underlying deposit account is provisioned. Both a domestic block and a SWIFT block are populated so a customer can fund the account from a US bank or from an international bank: ```json theme={null} { "domestic": { "bankName": "OMS bank partner", "bankAddress": "...", "accountNumber": "123456789", "routingNumber": "021000021", "accountType": "checking", "network": ["ACH", "WIRE"], "beneficiary": { "name": "Customer name", "address": { "...": "..." } } }, "swift": { "bankName": "Intermediary bank", "bankAddress": "...", "accountNumber": "...", "bic": "...", "memo": "FFC ", "beneficiary": { "name": "OMS bank partner", "address": { "...": "..." } } } } ``` On the domestic route, funds go directly to the OMS bank partner with the customer as the beneficiary. On the SWIFT route, funds route through a correspondent bank with the OMS bank partner as the beneficiary; the `memo` references the customer's provisioned account so the receiving bank credits the correct account. ## Third-party deposits A virtual account is a named account: it carries the onboarded customer's own legal name, so inbound transfers are addressed to your customer, not to Polygon or your platform. Senders do not need to be registered or identity-matched to the account holder. Any third party can push funds to the account over the supported rails, and each inbound deposit auto-creates its own transaction and fires its own events, so two deposits from two different senders arrive as two records. Every inbound payment is screened (AML and sanctions) before it converts. Each deposit carries the originator details from the payment message: name, account and routing as sent, rail, amount, and reference. Wire and SWIFT payments carry the originator's name consistently; many ACH payments do not include it, so build reconciliation on your own reference and amount rather than on sender name alone. ## Listing and retrieving `GET /virtual-accounts` lists virtual accounts across every customer in your organization. Both filters are optional: `customerId` scopes the list to one customer, and `status` to one lifecycle state. Paginate with `limit`, `startingAfter`, and `endingBefore`; each page returns `nextCursor`, `previousCursor`, and `hasMore`. `GET /virtual-accounts/{virtualAccountId}` fetches a single virtual account by ID. ## Updating `PATCH /virtual-accounts/{virtualAccountId}` accepts `destination` (re-point to a different `walletOms` or `walletExternal` target), `sponsorGas`, `label`, and `metadata`; any other key in the body is rejected with `400`. Re-pointing `destination` to a healthy external account recovers a virtual account from `inactiveActionRequired` back to `active`. A re-point on an already `active` account updates the target without a status transition. ## Deleting `DELETE /virtual-accounts/{virtualAccountId}` returns `202` and starts an asynchronous close of the underlying deposit account. The response echoes the virtual account with `deletionRequestedAt` set (`deletionRequestedBy` identifies the caller); the status stays as-is during the delete-pending window and finalizes to `deleted` once the underlying account closes. `finalBalance` snapshots the deposit account balance at the moment the account flips to `deleted`. ## Statuses | Status | Meaning | | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `pending` | Created; OMS is provisioning the underlying deposit account. `bankDetails` is null. | | `active` | Provisioned and accepting deposits. | | `frozen` | Temporarily suspended; inbound deposits are not processed. | | `closed` | Permanently closed. | | `deleted` | Deletion finalized; the underlying deposit account is closed. | | `failed` | Provisioning failed; `failureReason` identifies the cause. | | `inactiveActionRequired` | The destination is no longer usable. Re-point `destination` to a healthy account to recover to `active`. | ## Simulating inbound deposits In sandbox, you can simulate an inbound bank deposit to test your webhook and reconciliation flows without moving real funds. This endpoint is available in non-production environments only. `POST /virtual-accounts/{virtualAccountId}/simulate` with a rail-discriminated body. The `rail` field selects the deposit type: `ach_in`, `wire_in`, or `swift_in`. Amounts are in minor units (cents): ```json theme={null} { "rail": "ach_in", "amount": { "currency": "USD", "value": "5000" } } ``` The SWIFT rail (`swift_in`) also takes the originator's account number and BIC, which the upstream provider requires. The response echoes the simulated deposit with a `submitted` status: ```json theme={null} { "rail": "ach_in", "virtualAccountId": "va_...", "amount": { "currency": "USD", "value": "5000" }, "status": "submitted", "submittedAt": "2026-01-15T10:33:00Z" } ``` Use the simulate endpoint to exercise the full auto-created transaction path in sandbox: the simulated inbound funds create a `fiatAccountToCrypto` transaction just as a real deposit would. ## Deposit address vs. virtual account Both are persistent auto-route configurations. The difference is which side is fiat: | | Deposit address | Virtual account | | --------------- | ---------------------- | ------------------------------- | | Incoming funds | Crypto (on-chain) | Fiat (bank rail) | | Deposit details | On-chain inlet address | Bank account number and routing | | Direction | `cryptoToFiatAccount` | `fiatAccountToCrypto` | | Destination | Fiat account | Crypto wallet | ## Related * [Virtual accounts guide](/payments/guides/virtual-accounts): walkthrough of the inbound flow and webhook handling * [Deposit addresses](/payments/deposit-addresses): the crypto equivalent for on-chain deposits * [External accounts](/payments/external-accounts): registering an external wallet as a virtual account destination * [Transaction lifecycle](/payments/core-concepts/transaction-lifecycle): statuses, sub-statuses, and webhook events for the auto-created transaction # Introduction Source: https://docs.polygon.technology/pos/architecture/bor/introduction How Bor, Polygon Chain's block production layer, works: producer selection, spans, sprints, stall detection, and state synchronization with Ethereum. Bor is the block production layer of Polygon Chain. Its block production mechanism is adapted from the Clique proof-of-authority protocol, described in [EIP-225](https://eips.ethereum.org/EIPS/eip-225), modified for stake-weighted producers selected by Heimdall. Producers sign their blocks using `secp256k1`. Since the Rio hardfork, each span has exactly one selected producer that seals every block in that span (the "VEBloP" model). If that producer stalls, a new span under a different producer is required to resume block production. ## Producer selection Post-Rio, each span is produced by a single block producer drawn from a small set of eligible producers (the producer pool, currently up to four). The pool is determined on-chain from validator producer votes, and one producer from it is assigned to each span. The producer pool is computed as follows: 1. Validators cast ranked producer votes on-chain. Each vote is weighted by the voting validator's stake and by the position of the candidate in that validator's ranked list, so higher-ranked picks carry more weight. 2. Candidates are ranked by their total weighted score, with ties broken deterministically by validator ID. 3. Walking down the ranking, a candidate is admitted only if its score clears a per-position threshold of roughly two-thirds of the maximum possible weighted vote at that position. The first candidate that fails the threshold stops the walk, so the pool can be shorter than the limit, or empty. 4. If no candidate set qualifies, the network falls back to a default set of producers. For each span, one producer from this pool is assigned, and the assignment rotates across spans. If the current producer fails to make progress, Heimdall rotates to a different producer in the pool (see [Stall detection and span rotation](#stall-detection-and-span-rotation)). ## Spans A span is a defined set of blocks produced under a single assigned producer. The producer for each span is drawn from the producer pool (see [Producer selection](#producer-selection)), and the assignment rotates across spans. ## Sprints Within a span, a sprint is a smaller, fixed-size subset of blocks (currently 16 blocks). Pre-Rio, the sprint was the unit at which the producer was committed and at which backup producers rotated. Post-Rio, with a single producer per span, the sprint no longer drives producer selection or delay. The remaining live use of the sprint boundary is state synchronization: Bor commits pending state-sync records at the start of each sprint (see [State synchronization with Ethereum](#state-synchronization-with-ethereum)). ## Block authorization Block producers sign the hash of the block header (excluding the signature itself) using `secp256k1`. The signature is appended to the `extraData` field of the block header. Post-Rio, the span's single producer seals every block with difficulty `1`. There are no backup producers and no out-of-turn signing. ### Stall detection and span rotation If a producer stalls, block production does not fall back to a backup producer. Instead, during header verification Bor detects a stall when a new header has the same author as its parent and no milestone covers it. Bor then blocks production and waits for Heimdall to publish a new span with a different producer (or until a timeout). Recovery therefore depends on Heimdall rotating the span to a new producer. ## Span commitment Heimdall mints spans ahead of time. Bor polls its Heimdall node to fetch new span data and commits it on-chain by calling `commitSpan` on the `BorValidatorSet` genesis contract. Bor trusts the span data as given; it points at a single Heimdall endpoint and does not cryptographically verify Heimdall's data. ## State synchronization with Ethereum Bor synchronizes specific events from Ethereum through a state sync mechanism: 1. Contracts on Ethereum emit the `StateSynced` event via `StateSender.sol`. 2. Heimdall validators monitor these events and record the state-sync data in Heimdall state. 3. At the start of every sprint, Bor fetches pending records from Heimdall and commits them via the `StateReceiver.sol` contract on Bor, keeping Bor state consistent with Ethereum. This mechanism ensures that deposits from Ethereum are reflected in Bor's state. For the full state sync flow, see [State sync](/pos/architecture/bor/state-sync/). # Network configuration Source: https://docs.polygon.technology/pos/architecture/bor/network-config Default ports used by Bor and Heimdall nodes for peer connections, RPC, WebSocket, monitoring, and discovery. The tables below list the default ports used by Bor and Heimdall nodes. These ports govern peer connections, RPC access, monitoring endpoints, and peer discovery. ## Bor node | Name | Port | Tags | Description | | ---------------------- | ----- | ------------------------- | --------------------------------------------------------------------------------------------------- | | Network Listening Port | 30303 | Public | Port used by Bor for peer connections and synchronization. | | RPC Server | 8545 | Can be Public, Internal | RPC port for sending transactions and fetching data. Heimdall uses this port to obtain Bor headers. | | WebSocket Server | 8546 | Can be Public, Internal | WebSocket port for real-time updates. | | GraphQL Server | 8547 | Internal | GraphQL port for querying data. | | Prometheus Server | 9091 | Can be Public, Monitoring | Prometheus APIs for Grafana data source. Can be mapped to ports 80/443 via an Nginx reverse proxy. | | Grafana Server | 3001 | Can be Public, Monitoring | Grafana web server. Can be mapped to ports 80/443 via an Nginx reverse proxy. | | Pprof Server | 7071 | Internal, Monitoring | Pprof server for collecting Bor metrics. | | UDP Discovery | 30301 | Can be Public, Internal | Default port for Bootnode peer discovery. | ## Heimdall node Heimdall runs on CometBFT and the Cosmos SDK, so its ports differ from Bor's. | Name | Port | Tags | Description | | ----------------- | ----- | ------------------------- | --------------------------------------------------------------------------------------------- | | P2P Port | 26656 | Public | CometBFT peer-to-peer port for connecting to other Heimdall nodes and synchronization. | | RPC Server | 26657 | Can be Public, Internal | CometBFT RPC endpoint for queries and submitting transactions. Bound to localhost by default. | | REST API | 1317 | Can be Public, Internal | Cosmos SDK REST (LCD) API for querying Heimdall state over HTTP. | | gRPC Server | 9090 | Internal | Cosmos SDK gRPC endpoint for querying Heimdall state. Bound to localhost by default. | | Prometheus Server | 26660 | Can be Public, Monitoring | CometBFT Prometheus metrics endpoint for monitoring. | | Pprof Server | 6060 | Internal, Monitoring | Pprof server for collecting Heimdall runtime metrics. | # State sync Source: https://docs.polygon.technology/pos/architecture/bor/state-sync How state sync works in Polygon Chain, allowing Ethereum contract state to be reflected in Bor through the StateSender and StateReceiver contracts. State sync is the mechanism by which contract state on Ethereum is propagated to the Bor chain. It allows dApps on Polygon Chain to read and react to events that originate on Ethereum. ## How state sync works State transfer from Ethereum to Bor happens through a system call: 1. A contract on Ethereum calls `syncState` on the `StateSender.sol` contract, emitting a `StateSynced` event. 2. Heimdall validators listen for `StateSynced` events. One validator sends a state-sync transaction to Heimdall. 3. Once the transaction is included in a Heimdall block, it is added to the pending state-sync list. 4. After every sprint on Bor (currently 16 blocks), the Bor node fetches pending state-sync records from Heimdall via API. 5. Bor commits these records at the start of each sprint by calling `onStateReceive` on the target contract. This is a system call from address `2^160-2`. The receiver contract on Bor must implement `IStateReceiver`. The `onStateReceive` function can only be called by `StateReceiver.sol` at address `0x0000000000000000000000000000000000001001`. ## State sender contract Source: [StateSender.sol](https://github.com/0xPolygon/pos-contracts/blob/develop/contracts/root/stateSyncer/StateSender.sol) The `StateSender` contract on Ethereum exposes a `syncState` function: ```jsx theme={null} contract StateSender { /** * Emits `stateSynced` events to start sync process on Ethereum chain * @param receiver Target contract on Bor chain * @param data Data to send */ function syncState ( address receiver, bytes calldata data ) external; } ``` Calling `syncState` emits the following event: ```jsx theme={null} /** * Emits `stateSynced` events to start sync process on Ethereum chain * @param id State id * @param contractAddress Target contract address on Bor * @param data Data to send to Bor chain for Target contract address */ event StateSynced ( uint256 indexed id, address indexed contractAddress, bytes data ); ``` Once the `StateSynced` event is emitted, Heimdall listens for it. After 2/3+ validators agree, the state-sync record is added to Heimdall state. Bor fetches and applies it at the start of the next sprint. ## State receiver interface on Bor The target contract on Bor must implement the following interface: ```jsx theme={null} // IStateReceiver represents interface to receive state interface IStateReceiver { function onStateReceive(uint256 stateId, bytes calldata data) external; } ``` Only `StateReceiver.sol` at address `0x0000000000000000000000000000000000001001` is permitted to call `onStateReceive` on target contracts. ## System calls The state sync commit on Bor uses a system call. Only the system address `2^160-2` can make a system call. Bor calls it internally with the system address as `msg.sender`, which changes contract state and updates the state root for the block, without requiring a user-initiated transaction. This is inspired by [EIP-210](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-210.md). ## State-sync logs and Bor block receipts Events emitted by system calls are handled differently from normal logs. Bor produces a synthetic transaction and receipt for each state-sync event, so clients can query these events through standard Ethereum JSON-RPC methods. The transaction hash is derived from: ```jsx theme={null} keccak256("matic-bor-receipt-" + block number + block hash) ``` This synthetic transaction does not affect consensus logic. `eth_getBlockByNumber`, `eth_getTransactionReceipt`, and `eth_getLogs` include state-sync logs from the derived transaction. Note that the block bloom filter does not include state-sync log entries, and the derived transaction is not included in `transactionRoot` or `receiptRoot`. # Authentication Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/authentication How the Heimdall auth module handles account types, transaction validation, gas fees, AnteHandlers, and keepers. ## Abstract This document specifies the auth module of the Polygon's Cosmos SDK fork. The auth module is responsible for specifying the base transaction and account types for an application, since the SDK itself is agnostic to these particulars. It contains the middlewares, where all basic transaction validity checks (signatures, nonces, auxiliary fields) are performed, and exposes the account keeper, which allows other modules to read, write, and modify accounts. This module is used in the Cosmos Hub. ## Concepts **Note:** The auth module is different from the [authz](https://docs.cosmos.network/main/build/modules/authz) module. The differences are: * `auth` - authentication of accounts and transactions for Cosmos SDK applications and is responsible for specifying the base transaction and account types. * `authz` - authorization for accounts to perform actions on behalf of other accounts and enables a granter to grant authorizations to a grantee that allows the grantee to execute messages on behalf of the granter. ### Gas & Fees Fees serve two purposes for an operator of the network. Fees limit the growth of the state stored by every full node and allow for general purpose censorship of transactions of little economic value. Fees are best suited as an anti-spam mechanism where validators are disinterested in the use of the network and identities of users. Fees are determined by the gas limits and gas prices transactions provide, where `fees = ceil(gasLimit * gasPrices)`. Txs incur gas costs for all state reads/writes, signature verification, as well as costs proportional to the tx size. Operators should set minimum gas prices when starting their nodes. They must set the unit costs of gas in each token denomination they wish to support: `heimdalld start ... --minimum-gas-prices=0.00001stake;0.05photinos` When adding transactions to mempool or gossipping transactions, validators check if the transaction's gas prices, which are determined by the provided fees, meet any of the validator's minimum gas prices. In other words, a transaction must provide a fee of at least one denomination that matches a validator's minimum gas price. CometBFT does not currently provide fee based mempool prioritization, and fee based mempool filtering is local to node and not part of consensus. But with minimum gas prices set, such a mechanism could be implemented by node operators. Because the market value for tokens will fluctuate, validators are expected to dynamically adjust their minimum gas prices to a level that would encourage the use of the network. In Heimdall, a default fee of 10^15 pol (`DefaultFeeInPol`) is deducted from the tx sender for every tx. ## State ### Accounts Accounts contain authentication information for a uniquely identified external user of an SDK blockchain, including public key, address, and account number / sequence number for replay protection. For efficiency, since account balances must also be fetched to pay fees, account structs also store the balance of a user as `sdk.Coins`. Accounts are exposed externally as an interface, and stored internally as either a base account or vesting account. Module clients wishing to add more account types may do so. * `0x01 | Address -> ProtocolBuffer(account)` #### Account Interface The account interface exposes methods to read and write standard account information. Note that all of these methods operate on an account struct conforming to the interface - in order to write the account to the store, the account keeper will need to be used. ```go theme={null} // AccountI is an interface used to store coins at a given address within state. // It presumes a notion of sequence numbers for replay protection, // a notion of account numbers for replay protection for previously pruned accounts, // and a pubkey for authentication purposes. // // Many complex conditions can be used in the concrete struct which implements AccountI. type AccountI interface { proto.Message GetAddress() sdk.AccAddress SetAddress(sdk.AccAddress) error // errors if already set. GetPubKey() crypto.PubKey // can return nil. SetPubKey(crypto.PubKey) error GetAccountNumber() uint64 SetAccountNumber(uint64) error GetSequence() uint64 SetSequence(uint64) error // Ensure that account implements stringer String() string Validate() } ``` ##### Base Account A base account is the simplest and most common account type, which just stores all requisite fields directly in a struct. ```protobuf theme={null} // BaseAccount defines a base account type. It contains all the necessary fields // for basic account functionality. Any custom account type should extend this // type for additional functionality (e.g. vesting). message BaseAccount { string address = 1; google.protobuf.Any pub_key = 2; uint64 account_number = 3; uint64 sequence = 4; } ``` ### Vesting Account See [Vesting](https://docs.cosmos.network/main/modules/auth/vesting/).\ Heimdall does not currently support vesting accounts, so they will be treated as base accounts. ## AnteHandlers The `x/auth` module presently has no transaction handlers of its own, but does expose the special `AnteHandler`, used for performing basic validity checks on a transaction, such that it could be thrown out of the mempool. The `AnteHandler` can be seen as a set of decorators that check transactions within the current context, per [ADR 010](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-010-modular-antehandler.md). Note that the `AnteHandler` is called on both `CheckTx` and `DeliverTx`, as CometBFT proposers presently have the ability to include in their proposed block transactions which fail `CheckTx`. ### Decorators The auth module provides `AnteDecorator`s that are recursively chained together into a single `AnteHandler` in the following order: * `SetUpContextDecorator`: Sets the `GasMeter` in the `Context` and wraps the next `AnteHandler` with a defer clause to recover from any downstream `OutOfGas` panics in the `AnteHandler` chain to return an error with information on gas provided and gas used. * `RejectExtensionOptionsDecorator`: Rejects all extension options which can optionally be included in protobuf transactions. * `MempoolFeeDecorator`: Checks if the `tx` fee is above local mempool `minFee` parameter during `CheckTx`. * `ValidateBasicDecorator`: Calls `tx.ValidateBasic` and returns any non-nil error. * `TxTimeoutHeightDecorator`: Check for a `tx` height timeout. * `ValidateMemoDecorator`: Validates `tx` memo with application parameters and returns any non-nil error. * `ConsumeGasTxSizeDecorator`: Consumes gas proportional to the `tx` size based on application parameters. * `DeductFeeDecorator`: Deducts the `FeeAmount` from first signer of the `tx`. If the `x/feegrant` module is enabled and a fee granter is set, it deducts fees from the fee granter account. * `SetPubKeyDecorator`: Sets the pubkey from a `tx`'s signers that does not already have its corresponding pubkey saved in the state machine and in the current context. * `ValidateSigCountDecorator`: Validates the number of signatures in `tx` based on app-parameters. * `SigGasConsumeDecorator`: Consumes parameter-defined amount of gas for each signature. This requires pubkeys to be set in context for all signers as part of `SetPubKeyDecorator`. * `SigVerificationDecorator`: Verifies all signatures are valid. This requires pubkeys to be set in context for all signers as part of `SetPubKeyDecorator`. * `IncrementSequenceDecorator`: Increments the account sequence for each signer to prevent replay attacks. Some of these decorators may be disabled due to Heimdall's specific requirements. ## Keepers The auth module only exposes one keeper, the account keeper, which can be used to read and write accounts. ### Account Keeper Presently only one fully-permissioned account keeper is exposed, which has the ability to both read and write all fields of all accounts, and to iterate over all stored accounts. ```go theme={null} // AccountKeeperI is the interface contract that x/auth's keeper implements. type AccountKeeperI interface { // Return a new account with the next account number and the specified address. Does not save the new account to the store. NewAccountWithAddress(sdk.Context, sdk.AccAddress) types.AccountI // Return a new account with the next account number. Does not save the new account to the store. NewAccount(sdk.Context, types.AccountI) types.AccountI // Check if an account exists in the store. HasAccount(sdk.Context, sdk.AccAddress) bool // Retrieve an account from the store. GetAccount(sdk.Context, sdk.AccAddress) types.AccountI // Set an account in the store. SetAccount(sdk.Context, types.AccountI) // Remove an account from the store. RemoveAccount(sdk.Context, types.AccountI) // Iterate over all accounts, calling the provided function. Stop iteration when it returns true. IterateAccounts(sdk.Context, func(types.AccountI) bool) // Fetch the public key of an account at a specified address GetPubKey(sdk.Context, sdk.AccAddress) (crypto.PubKey, error) // Fetch the sequence of an account at a specified address. GetSequence(sdk.Context, sdk.AccAddress) (uint64, error) // Fetch the next account number, and increment the internal counter. NextAccountNumber(sdk.Context) uint64 } ``` ## Parameters The auth module contains the following parameters: | Key | Type | Example | | ---------------------- | ------ | ------- | | MaxMemoCharacters | uint64 | 256 | | TxSigLimit | uint64 | 7 | | TxSizeCostPerByte | uint64 | 10 | | SigVerifyCostED25519 | uint64 | 590 | | SigVerifyCostSecp256k1 | uint64 | 1000 | | MaxTxGas | uint64 | 1000 | | TxFees | string | "1000" | ## Client ### CLI A user can query and interact with the `auth` module using the CLI. ### Query The `query` commands allow users to query `auth` state. ```bash theme={null} heimdalld query auth --help ``` #### account The `account` command allow users to query for an account by it's address. ```bash theme={null} heimdalld query auth account [address] [flags] ``` Example: ```bash theme={null} heimdalld query auth account cosmos1... ``` Example Output: ```bash theme={null} '@type': /cosmos.auth.v1beta1.BaseAccount account_number: "0" address: 0x... pub_key: '@type': /cosmos.crypto.secp256k1.PubKey key: ApDrE38zZdd7wLmFS9YmqO684y5DG6fjZ4rVeihF/AQD sequence: "1" ``` #### accounts The `accounts` command allow users to query all the available accounts. ```bash theme={null} heimdalld query auth accounts [flags] ``` Example: ```bash theme={null} heimdalld query auth accounts ``` Example Output: ```bash theme={null} accounts: - '@type': /cosmos.auth.v1beta1.BaseAccount account_number: "0" address: 0x... pub_key: '@type': /cosmos.crypto.secp256k1.PubKey key: ApDrE38zZdd7wLmFS9YmqO684y5DG6fjZ4rVeihF/AQD sequence: "1" - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "8" address: 0x... pub_key: null sequence: "0" name: transfer permissions: - minter - burner - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "4" address: 0x... pub_key: null sequence: "0" name: bonded_tokens_pool permissions: - burner - staking - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "5" address: 0x... pub_key: null sequence: "0" name: not_bonded_tokens_pool permissions: - burner - staking - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "6" address: 0x... pub_key: null sequence: "0" name: gov permissions: - burner - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "3" address: 0x... pub_key: null sequence: "0" name: distribution permissions: [] - '@type': /cosmos.auth.v1beta1.BaseAccount account_number: "1" address: 0x... pub_key: null sequence: "0" - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "7" address: 0x... pub_key: null sequence: "0" name: mint permissions: - minter - '@type': /cosmos.auth.v1beta1.ModuleAccount base_account: account_number: "2" address: 0x... pub_key: null sequence: "0" name: fee_collector permissions: [] pagination: next_key: null total: "0" ``` #### params The `params` command allow users to query the current auth parameters. ```bash theme={null} heimdalld query auth params [flags] ``` Example: ```bash theme={null} heimdalld query auth params ``` Example Output: ```bash theme={null} max_memo_characters: "256" sig_verify_cost_ed25519: "590" sig_verify_cost_secp256k1: "1000" tx_sig_limit: "7" tx_size_cost_per_byte: "10" max_tx_gas: "1000"; tx_fees: "1000"; ``` ### Transactions The `auth` module supports transactions commands to help you with signing and more. Compared to other modules you can access directly the `auth` module transactions commands using the only `tx` command. Use directly the `--help` flag to get more information about the `tx` command. ```bash theme={null} heimdalld tx --help ``` #### `sign` The `sign` command allows users to sign transactions that was generated offline. ```bash theme={null} heimdalld tx sign tx.json --from $ALICE > tx.signed.json ``` The result is a signed transaction that can be broadcasted to the network thanks to the broadcast command. More information about the `sign` command can be found running `heimdalld tx sign --help`. #### `sign-batch` The `sign-batch` command allows users to sign multiples offline generated transactions. The transactions can be in one file, with one tx per line, or in multiple files. ```bash theme={null} heimdalld tx sign txs.json --from $ALICE > tx.signed.json ``` or ```bash theme={null} heimdalld tx sign tx1.json tx2.json tx3.json --from $ALICE > tx.signed.json ``` The result is multiples signed transactions. For combining the signed transactions into one transactions, use the `--append` flag. More information about the `sign-batch` command can be found running `heimdalld tx sign-batch --help`. #### `multi-sign` The `multi-sign` command allows users to sign transactions that was generated offline by a multisig account.\ The multi signature functionality is not supported by Heimdall. ```bash theme={null} heimdalld tx multisign transaction.json k1k2k3 k1sig.json k2sig.json k3sig.json ``` Where `k1k2k3` is the multisig account address, `k1sig.json` is the signature of the first signer, `k2sig.json` is the signature of the second signer, and `k3sig.json` is the signature of the third signer. ##### Nested multisig transactions To allow transactions to be signed by nested multisigs, meaning that a participant of a multisig account can be another multisig account, the `--skip-signature-verification` flag must be used. ```bash theme={null} # First aggregate signatures of the multisig participant heimdalld tx multi-sign transaction.json ms1 ms1p1sig.json ms1p2sig.json --signature-only --skip-signature-verification > ms1sig.json # Then use the aggregated signatures and the other signatures to sign the final transaction heimdalld tx multi-sign transaction.json k1ms1 k1sig.json ms1sig.json --skip-signature-verification ``` Where `ms1` is the nested multisig account address, `ms1p1sig.json` is the signature of the first participant of the nested multisig account, `ms1p2sig.json` is the signature of the second participant of the nested multisig account, and `ms1sig.json` is the aggregated signature of the nested multisig account. `k1ms1` is a multisig account comprised of an individual signer and another nested multisig account (`ms1`). `k1sig.json` is the signature of the first signer of the individual member. More information about the `multi-sign` command can be found running `heimdalld tx multi-sign --help`. #### `multisign-batch` The `multisign-batch` works the same way as `sign-batch`, but for multisig accounts. With the difference that the `multisign-batch` command requires all transactions to be in one file, and the `--append` flag does not exist.\ The multi signature functionality is not supported by Heimdall. More information about the `multisign-batch` command can be found running `heimdalld tx multisign-batch --help`. #### `validate-signatures` The `validate-signatures` command allows users to validate the signatures of a signed transaction. ```bash theme={null} $ heimdalld tx validate-signatures tx.signed.json Signers: 0: 0x... Signatures: 0: 0x... [OK] ``` More information about the `validate-signatures` command can be found running `heimdalld tx validate-signatures --help`. #### `broadcast` The `broadcast` command allows users to broadcast a signed transaction to the network. ```bash theme={null} heimdalld tx broadcast tx.signed.json ``` More information about the `broadcast` command can be found running `heimdalld tx broadcast --help`. ### gRPC A user can query the `auth` module using gRPC endpoints. #### Account The `account` endpoint allow users to query for an account by it's address. ```bash theme={null} cosmos.auth.v1beta1.Query/Account ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"address":"cosmos1.."}' \ localhost:9090 \ cosmos.auth.v1beta1.Query/Account ``` Example Output: ```bash theme={null} { "account":{ "@type":"/cosmos.auth.v1beta1.BaseAccount", "address":"0x...", "pubKey":{ "@type":"/cosmos.crypto.secp256k1.PubKey", "key":"ApDrE38zZdd7wLmFS9YmqO684y5DG6fjZ4rVeihF/AQD" }, "sequence":"1" } } ``` #### Accounts The `accounts` endpoint allow users to query all the available accounts. ```bash theme={null} cosmos.auth.v1beta1.Query/Accounts ``` Example: ```bash theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.auth.v1beta1.Query/Accounts ``` Example Output: ```bash theme={null} { "accounts":[ { "@type":"/cosmos.auth.v1beta1.BaseAccount", "address":"0x...", "pubKey":{ "@type":"/cosmos.crypto.secp256k1.PubKey", "key":"ApDrE38zZdd7wLmFS9YmqO684y5DG6fjZ4rVeihF/AQD" }, "sequence":"1" }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"8" }, "name":"transfer", "permissions":[ "minter", "burner" ] }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"4" }, "name":"bonded_tokens_pool", "permissions":[ "burner", "staking" ] }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"5" }, "name":"not_bonded_tokens_pool", "permissions":[ "burner", "staking" ] }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"6" }, "name":"gov", "permissions":[ "burner" ] }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"3" }, "name":"distribution" }, { "@type":"/cosmos.auth.v1beta1.BaseAccount", "accountNumber":"1", "address":"0x..." }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"7" }, "name":"mint", "permissions":[ "minter" ] }, { "@type":"/cosmos.auth.v1beta1.ModuleAccount", "baseAccount":{ "address":"0x...", "accountNumber":"2" }, "name":"fee_collector" } ], "pagination":{ "total":"9" } } ``` #### Params The `params` endpoint allow users to query the current auth parameters. ```bash theme={null} cosmos.auth.v1beta1.Query/Params ``` Example: ```bash theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.auth.v1beta1.Query/Params ``` Example Output: ```bash theme={null} { "params": { "maxMemoCharacters": "256", "txSigLimit": "7", "txSizeCostPerByte": "10", "sigVerifyCostEd25519": "590", "sigVerifyCostSecp256k1": "1000" "maxTxGas": "1000", "txFees": "1000" } } ``` ### REST A user can query the `auth` module using REST endpoints. #### Account The `account` endpoint allow users to query for an account by it's address. ```bash theme={null} /cosmos/auth/v1beta1/account?address={address} ``` #### Accounts The `accounts` endpoint allow users to query all the available accounts. ```bash theme={null} /cosmos/auth/v1beta1/accounts ``` #### Params The `params` endpoint allow users to query the current auth parameters. ```bash theme={null} /cosmos/auth/v1beta1/params ``` ### Heimdall Notes Note that in the example provided here, `0x...` is used as a placeholder for an actual ethereum compatible address. # Balance transfers Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/balance-transfers How the Heimdall bank module handles multi-asset coin transfers, module accounts, supply tracking, send restrictions, and CLI/gRPC/REST query commands. Heimdall's `bank` module handles balance transfers between accounts. This module corresponds to the `bank` module from the Cosmos SDK. ## Abstract The bank module is responsible for handling multi-asset coin transfers between accounts and tracking special-case pseudo-transfers which must work differently with particular kinds of accounts (notably delegating/undelegating for vesting accounts). It exposes several interfaces with varying capabilities for secure interaction with other modules which must alter user balances. In addition, the bank module tracks and provides query support for the total supply of all assets used in the application. This module is used in the Cosmos Hub and [Heimdall](https://github.com/0xPolygon/heimdall-v2). **NOTE**: For heimdall, minimal changes have been done in the fork listed [here](#conceptschanges-specific-to-heimdall) ## Supply The `supply` functionality: * passively tracks the total supply of coins within a chain, * provides a pattern for modules to hold/interact with `Coins`, and * introduces the invariant check to verify a chain's total supply. ### Total Supply The total `Supply` of the network is equal to the sum of all coins from the account. The total supply is updated every time a `Coin` is minted (eg: as part of the inflation mechanism) or burned (eg: due to slashing or if a governance proposal is vetoed). ## Module Accounts The supply functionality introduces a new type of `auth.Account` which can be used by modules to allocate tokens and in special cases mint or burn tokens. At a base level these module accounts are capable of sending/receiving tokens to and from `auth.Account` and other module accounts. This design replaces previous alternative designs where, to hold tokens, modules would burn the incoming tokens from the sender account, and then track those tokens internally. Later, in order to send tokens, the module would need to effectively mint tokens within a destination account. The new design removes duplicate logic between modules to perform this accounting. The `ModuleAccount` interface is defined as follows: ```go theme={null} type ModuleAccount interface { auth.Account // same methods as the Account interface GetName() string // name of the module; used to obtain the address GetPermissions() []string // permissions of module account HasPermission(string) bool } ``` > **WARNING!** > Any module or message handler that allows either direct or indirect sending of funds must explicitly guarantee those funds cannot be sent to module accounts (unless allowed). The supply `Keeper` also introduces new wrapper functions for the auth `Keeper` and the bank `Keeper` that are related to `ModuleAccount` in order to be able to: * Get and set `ModuleAccount` by providing the `Name`. * Send coins from and to other `ModuleAccount` or standard `Account` (`BaseAccount` or `VestingAccount`) by passing only the `Name`. * `Mint` or `Burn` coins for a `ModuleAccount` (restricted to its permissions). ### Permissions Each `ModuleAccount` has a different set of permissions that provide different object capabilities to perform certain actions. Permissions need to be registered upon the creation of the supply `Keeper` so that every time a `ModuleAccount` calls the allowed functions, the `Keeper` can lookup the permissions to that specific account and perform or not perform the action. The available permissions are: * `Minter`: allows for a module to mint a specific amount of coins. * `Burner`: allows for a module to burn a specific amount of coins. * `Staking`: allows for a module to delegate and undelegate a specific amount of coins. ## State The `x/bank` module keeps state of the following primary objects: 1. Account balances 2. Denomination metadata 3. The total supply of all balances 4. Information on which denominations are allowed to be sent. In addition, the `x/bank` module keeps the following indexes to manage the aforementioned state: * Supply Index: `0x0 | byte(denom) -> byte(amount)` * Denom Metadata Index: `0x1 | byte(denom) -> ProtocolBuffer(Metadata)` * Balances Index: `0x2 | byte(address length) | []byte(address) | []byte(balance.Denom) -> ProtocolBuffer(balance)` * Reverse Denomination to Address Index: `0x03 | byte(denom) | 0x00 | []byte(address) -> 0` ## Params The bank module stores it's params in state with the prefix of `0x05`, it can be updated with governance or the address with authority. * Params: `0x05 | ProtocolBuffer(Params)` protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/bank.proto#L12-L23](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/bank.proto#L12-L23) ## Keepers The bank module provides these exported keeper interfaces that can be passed to other modules that read or update account balances. Modules should use the least-permissive interface that provides the functionality they require. Best practices dictate careful review of `bank` module code to ensure that permissions are limited in the way that you expect. ### Denied Addresses The `x/bank` module accepts a map of addresses that are considered blocklisted from directly and explicitly receiving funds through means such as `MsgSend` and `MsgMultiSend` and direct API calls like `SendCoinsFromModuleToAccount`. Typically, these addresses are module accounts. If these addresses receive funds outside the expected rules of the state machine, invariants are likely to be broken and could result in a halted network. By providing the `x/bank` module with a blocklisted set of addresses, an error occurs for the operation if a user or client attempts to directly or indirectly send funds to a blocklisted account, for example, by using [IBC](https://ibc.cosmos.network). ### Common Types #### Input An input of a multiparty transfer ```protobuf theme={null} // Input models transaction input. message Input { string address = 1; repeated cosmos.base.v1beta1.Coin coins = 2; } ``` #### Output An output of a multiparty transfer. ```protobuf theme={null} // Output models transaction outputs. message Output { string address = 1; repeated cosmos.base.v1beta1.Coin coins = 2; } ``` ### BaseKeeper The base keeper provides full-permission access: the ability to arbitrary modify any account's balance and mint or burn coins. Restricted permission to mint per module could be achieved by using baseKeeper with `WithMintCoinsRestriction` to give specific restrictions to mint (e.g. only minting certain denom). ```go theme={null} // Keeper defines a module interface that facilitates the transfer of coins // between accounts. type Keeper interface { SendKeeper WithMintCoinsRestriction(MintingRestrictionFn) BaseKeeper InitGenesis(context.Context, *types.GenesisState) ExportGenesis(context.Context) *types.GenesisState GetSupply(ctx context.Context, denom string) sdk.Coin HasSupply(ctx context.Context, denom string) bool GetPaginatedTotalSupply(ctx context.Context, pagination *query.PageRequest) (sdk.Coins, *query.PageResponse, error) IterateTotalSupply(ctx context.Context, cb func(sdk.Coin) bool) GetDenomMetaData(ctx context.Context, denom string) (types.Metadata, bool) HasDenomMetaData(ctx context.Context, denom string) bool SetDenomMetaData(ctx context.Context, denomMetaData types.Metadata) IterateAllDenomMetaData(ctx context.Context, cb func(types.Metadata) bool) SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error DelegateCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error UndelegateCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error DelegateCoins(ctx context.Context, delegatorAddr, moduleAccAddr sdk.AccAddress, amt sdk.Coins) error UndelegateCoins(ctx context.Context, moduleAccAddr, delegatorAddr sdk.AccAddress, amt sdk.Coins) error // GetAuthority gets the address capable of executing governance proposal messages. Usually the gov module account. GetAuthority() string types.QueryServer } ``` ### SendKeeper The send keeper provides access to account balances and the ability to transfer coins between accounts. The send keeper does not alter the total supply (mint or burn coins). ```go theme={null} // SendKeeper defines a module interface that facilitates the transfer of coins // between accounts without the possibility of creating coins. type SendKeeper interface { ViewKeeper AppendSendRestriction(restriction SendRestrictionFn) PrependSendRestriction(restriction SendRestrictionFn) ClearSendRestriction() InputOutputCoins(ctx context.Context, input types.Input, outputs []types.Output) error SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error GetParams(ctx context.Context) types.Params SetParams(ctx context.Context, params types.Params) error IsSendEnabledDenom(ctx context.Context, denom string) bool SetSendEnabled(ctx context.Context, denom string, value bool) SetAllSendEnabled(ctx context.Context, sendEnableds []*types.SendEnabled) DeleteSendEnabled(ctx context.Context, denom string) IterateSendEnabledEntries(ctx context.Context, cb func(denom string, sendEnabled bool) (stop bool)) GetAllSendEnabledEntries(ctx context.Context) []types.SendEnabled IsSendEnabledCoin(ctx context.Context, coin sdk.Coin) bool IsSendEnabledCoins(ctx context.Context, coins ...sdk.Coin) error BlockedAddr(addr sdk.AccAddress) bool } ``` #### Send Restrictions The `SendKeeper` applies a `SendRestrictionFn` before each transfer of funds. ```go theme={null} // A SendRestrictionFn can restrict sends and/or provide a new receiver address. type SendRestrictionFn func(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) (newToAddr sdk.AccAddress, err error) ``` After the `SendKeeper` (or `BaseKeeper`) has been created, send restrictions can be added to it using the `AppendSendRestriction` or `PrependSendRestriction` functions. Both functions compose the provided restriction with any previously provided restrictions. `AppendSendRestriction` adds the provided restriction to be run after any previously provided send restrictions. `PrependSendRestriction` adds the restriction to be run before any previously provided send restrictions. The composition will short-circuit when an error is encountered. I.e. if the first one returns an error, the second is not run. During `SendCoins`, the send restriction is applied after coins are removed from the from address, but before adding them to the to address. During `InputOutputCoins`, the send restriction is applied after the input coins are removed and once for each output before the funds are added. A send restriction function should make use of a custom value in the context to allow bypassing that specific restriction. Send Restrictions are not placed on `ModuleToAccount` or `ModuleToModule` transfers. This is done due to modules needing to move funds to user accounts and other module accounts. This is a design decision to allow for more flexibility in the state machine. The state machine should be able to move funds between module accounts and user accounts without restrictions. Secondly this limitation would limit the usage of the state machine even for itself. users would not be able to receive rewards, not be able to move funds between module accounts. In the case that a user sends funds from a user account to the community pool and then a governance proposal is used to get those tokens into the users account this would fall under the discretion of the app chain developer to what they would like to do here. We can not make strong assumptions here. Thirdly, this issue could lead into a chain halt if a token is disabled and the token is moved in the begin/endblock. This is the last reason we see the current change and more damaging then beneficial for users. For example, in your module's keeper package, you'd define the send restriction function: ```go theme={null} var _ banktypes.SendRestrictionFn = Keeper{}.SendRestrictionFn func (k Keeper) SendRestrictionFn(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.AccAddress, error) { // Bypass if the context says to. if mymodule.HasBypass(ctx) { return toAddr, nil } // Your custom send restriction logic goes here. return nil, errors.New("not implemented") } ``` The bank keeper should be provided to your keeper's constructor so the send restriction can be added to it: ```go theme={null} func NewKeeper(cdc codec.BinaryCodec, storeKey storetypes.StoreKey, bankKeeper mymodule.BankKeeper) Keeper { rv := Keeper{/*...*/} bankKeeper.AppendSendRestriction(rv.SendRestrictionFn) return rv } ``` Then, in the `mymodule` package, define the context helpers: ```go theme={null} const bypassKey = "bypass-mymodule-restriction" // WithBypass returns a new context that will cause the mymodule bank send restriction to be skipped. func WithBypass(ctx context.Context) context.Context { return sdk.UnwrapSDKContext(ctx).WithValue(bypassKey, true) } // WithoutBypass returns a new context that will cause the mymodule bank send restriction to not be skipped. func WithoutBypass(ctx context.Context) context.Context { return sdk.UnwrapSDKContext(ctx).WithValue(bypassKey, false) } // HasBypass checks the context to see if the mymodule bank send restriction should be skipped. func HasBypass(ctx context.Context) bool { bypassValue := ctx.Value(bypassKey) if bypassValue == nil { return false } bypass, isBool := bypassValue.(bool) return isBool && bypass } ``` Now, anywhere where you want to use `SendCoins` or `InputOutputCoins`, but you don't want your send restriction applied: ```go theme={null} func (k Keeper) DoThing(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { return k.bankKeeper.SendCoins(mymodule.WithBypass(ctx), fromAddr, toAddr, amt) } ``` ### ViewKeeper The view keeper provides read-only access to account balances. The view keeper does not have balance alteration functionality. All balance lookups are `O(1)`. ```go theme={null} // ViewKeeper defines a module interface that facilitates read only access to // account balances. type ViewKeeper interface { ValidateBalance(ctx context.Context, addr sdk.AccAddress) error HasBalance(ctx context.Context, addr sdk.AccAddress, amt sdk.Coin) bool GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins GetAccountsBalances(ctx context.Context) []types.Balance GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin LockedCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins SpendableCoin(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin IterateAccountBalances(ctx context.Context, addr sdk.AccAddress, cb func(coin sdk.Coin) (stop bool)) IterateAllBalances(ctx context.Context, cb func(address sdk.AccAddress, coin sdk.Coin) (stop bool)) } ``` ## Messages ### MsgSend Send coins from one address to another. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L38-L53](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L38-L53) The message will fail under the following conditions: * The coins do not have sending enabled * The `to` address is restricted ### MsgMultiSend Send coins from one sender and to a series of different address. If any of the receiving addresses do not correspond to an existing account, a new account is created. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L58-L69](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L58-L69) The message will fail under the following conditions: * Any of the coins do not have sending enabled * Any of the `to` addresses are restricted * Any of the coins are locked * The inputs and outputs do not correctly correspond to one another ### MsgUpdateParams The `bank` module params can be updated through `MsgUpdateParams`, which can be done using governance proposal. The signer will always be the `gov` module account address. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L74-L88](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L74-L88) The message handling can fail if: * signer is not the gov module account address. ### MsgSetSendEnabled **NOTE**: Heimdall only supports denom (pol) and hence this msg type is not supported. Used with the x/gov module to set create/edit SendEnabled entries. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L96-L117](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/bank/v1beta1/tx.proto#L96-L117) The message will fail under the following conditions: * The authority is not a bech32 address. * The authority is not x/gov module's address. * There are multiple SendEnabled entries with the same Denom. * One or more SendEnabled entries has an invalid Denom. ## Events The bank module emits the following events: ### Message Events #### MsgSend | Type | Attribute Key | Attribute Value | | -------- | ------------- | -------------------- | | transfer | recipient | `{recipientAddress}` | | transfer | amount | `{amount}` | | message | module | bank | | message | action | send | | message | sender | `{senderAddress}` | #### MsgMultiSend | Type | Attribute Key | Attribute Value | | -------- | ------------- | -------------------- | | transfer | recipient | `{recipientAddress}` | | transfer | amount | `{amount}` | | message | module | bank | | message | action | multisend | | message | sender | `{senderAddress}` | ### Keeper Events In addition to message events, the bank keeper will produce events when the following methods are called (or any method which ends up calling them) #### MintCoins ```json theme={null} { "type": "coinbase", "attributes": [ { "key": "minter", "value": "{{sdk.AccAddress of the module minting coins}}", "index": true }, { "key": "amount", "value": "{{sdk.Coins being minted}}", "index": true } ] } ``` ```json theme={null} { "type": "coin_received", "attributes": [ { "key": "receiver", "value": "{{sdk.AccAddress of the module minting coins}}", "index": true }, { "key": "amount", "value": "{{sdk.Coins being received}}", "index": true } ] } ``` #### BurnCoins ```json theme={null} { "type": "burn", "attributes": [ { "key": "burner", "value": "{{sdk.AccAddress of the module burning coins}}", "index": true }, { "key": "amount", "value": "{{sdk.Coins being burned}}", "index": true } ] } ``` ```json theme={null} { "type": "coin_spent", "attributes": [ { "key": "spender", "value": "{{sdk.AccAddress of the module burning coins}}", "index": true }, { "key": "amount", "value": "{{sdk.Coins being burned}}", "index": true } ] } ``` #### addCoins ```json theme={null} { "type": "coin_received", "attributes": [ { "key": "receiver", "value": "{{sdk.AccAddress of the address beneficiary of the coins}}", "index": true }, { "key": "amount", "value": "{{sdk.Coins being received}}", "index": true } ] } ``` #### subUnlockedCoins/DelegateCoins ```json theme={null} { "type": "coin_spent", "attributes": [ { "key": "spender", "value": "{{sdk.AccAddress of the address which is spending coins}}", "index": true }, { "key": "amount", "value": "{{sdk.Coins being spent}}", "index": true } ] } ``` ## Parameters The bank module contains the following parameters ### SendEnabled The SendEnabled parameter is now deprecated and not to be use. It is replaced with state store records. ### DefaultSendEnabled The default send enabled value controls send transfer capability for all coin denominations unless specifically included in the array of `SendEnabled` parameters. ## Client ### CLI A user can query and interact with the `bank` module using the CLI. #### Query The `query` commands allow users to query `bank` state. ```shell theme={null} heimdalld query bank --help ``` ##### balances The `balances` command allows users to query account balances by address. ```shell theme={null} heimdalld query bank balances [address] [flags] ``` Example: ```shell theme={null} heimdalld query bank balances cosmos1.. ``` Example Output: ```yml theme={null} balances: - amount: "1000000000" denom: stake pagination: next_key: null total: "0" ``` ##### denom-metadata The `denom-metadata` command allows users to query metadata for coin denominations. A user can query metadata for a single denomination using the `--denom` flag or all denominations without it. ```shell theme={null} heimdalld query bank denom-metadata [flags] ``` Example: ```shell theme={null} heimdalld query bank denom-metadata --denom stake ``` Example Output: ```yml theme={null} metadata: base: stake denom_units: - aliases: - STAKE denom: stake description: native staking token of simulation app display: stake name: SimApp Token symbol: STK ``` ##### total The `total` command allows users to query the total supply of coins. A user can query the total supply for a single coin using the `--denom` flag or all coins without it. ```shell theme={null} heimdalld query bank total [flags] ``` Example: ```shell theme={null} heimdalld query bank total --denom stake ``` Example Output: ```yml theme={null} amount: "10000000000" denom: stake ``` ##### send-enabled The `send-enabled` command allows users to query for all or some SendEnabled entries. ```shell theme={null} heimdalld query bank send-enabled [denom1 ...] [flags] ``` Example: ```shell theme={null} heimdalld query bank send-enabled ``` Example output: ```yml theme={null} send_enabled: - denom: foocoin enabled: true - denom: barcoin pagination: next-key: null total: 2 ``` #### Transactions The `tx` commands allow users to interact with the `bank` module. ```shell theme={null} heimdalld tx bank --help ``` ##### send The `send` command allows users to send funds from one account to another. ```shell theme={null} heimdalld tx bank send [from_key_or_address] [to_address] [amount] [flags] ``` Example: ```shell theme={null} heimdalld tx bank send cosmos1.. cosmos1.. 100stake ``` ## gRPC A user can query the `bank` module using gRPC endpoints. ### Balance The `Balance` endpoint allows users to query account balance by address for a given denomination. ```shell theme={null} cosmos.bank.v1beta1.Query/Balance ``` Example: ```shell theme={null} grpcurl -plaintext \ -d '{"address":"cosmos1..","denom":"stake"}' \ localhost:9090 \ cosmos.bank.v1beta1.Query/Balance ``` Example Output: ```json theme={null} { "balance": { "denom": "stake", "amount": "1000000000" } } ``` ### AllBalances The `AllBalances` endpoint allows users to query account balance by address for all denominations. ```shell theme={null} cosmos.bank.v1beta1.Query/AllBalances ``` Example: ```shell theme={null} grpcurl -plaintext \ -d '{"address":"cosmos1.."}' \ localhost:9090 \ cosmos.bank.v1beta1.Query/AllBalances ``` Example Output: ```json theme={null} { "balances": [ { "denom": "stake", "amount": "1000000000" } ], "pagination": { "total": "1" } } ``` ### DenomMetadata The `DenomMetadata` endpoint allows users to query metadata for a single coin denomination. ```shell theme={null} cosmos.bank.v1beta1.Query/DenomMetadata ``` Example: ```shell theme={null} grpcurl -plaintext \ -d '{"denom":"stake"}' \ localhost:9090 \ cosmos.bank.v1beta1.Query/DenomMetadata ``` Example Output: ```json theme={null} { "metadata": { "description": "native staking token of simulation app", "denomUnits": [ { "denom": "stake", "aliases": [ "STAKE" ] } ], "base": "stake", "display": "stake", "name": "SimApp Token", "symbol": "STK" } } ``` ### DenomsMetadata The `DenomsMetadata` endpoint allows users to query metadata for all coin denominations. ```shell theme={null} cosmos.bank.v1beta1.Query/DenomsMetadata ``` Example: ```shell theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.bank.v1beta1.Query/DenomsMetadata ``` Example Output: ```json theme={null} { "metadatas": [ { "description": "native staking token of simulation app", "denomUnits": [ { "denom": "stake", "aliases": [ "STAKE" ] } ], "base": "stake", "display": "stake", "name": "SimApp Token", "symbol": "STK" } ], "pagination": { "total": "1" } } ``` ### DenomOwners The `DenomOwners` endpoint allows users to query metadata for a single coin denomination. ```shell theme={null} cosmos.bank.v1beta1.Query/DenomOwners ``` Example: ```shell theme={null} grpcurl -plaintext \ -d '{"denom":"stake"}' \ localhost:9090 \ cosmos.bank.v1beta1.Query/DenomOwners ``` Example Output: ```json theme={null} { "denomOwners": [ { "address": "cosmos1..", "balance": { "denom": "stake", "amount": "5000000000" } }, { "address": "cosmos1..", "balance": { "denom": "stake", "amount": "5000000000" } }, ], "pagination": { "total": "2" } } ``` ### TotalSupply The `TotalSupply` endpoint allows users to query the total supply of all coins. ```shell theme={null} cosmos.bank.v1beta1.Query/TotalSupply ``` Example: ```shell theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.bank.v1beta1.Query/TotalSupply ``` Example Output: ```json theme={null} { "supply": [ { "denom": "stake", "amount": "10000000000" } ], "pagination": { "total": "1" } } ``` ### SupplyOf The `SupplyOf` endpoint allows users to query the total supply of a single coin. ```shell theme={null} cosmos.bank.v1beta1.Query/SupplyOf ``` Example: ```shell theme={null} grpcurl -plaintext \ -d '{"denom":"stake"}' \ localhost:9090 \ cosmos.bank.v1beta1.Query/SupplyOf ``` Example Output: ```json theme={null} { "amount": { "denom": "stake", "amount": "10000000000" } } ``` ### Params The `Params` endpoint allows users to query the parameters of the `bank` module. ```shell theme={null} cosmos.bank.v1beta1.Query/Params ``` Example: ```shell theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.bank.v1beta1.Query/Params ``` Example Output: ```json theme={null} { "params": { "defaultSendEnabled": true } } ``` ### SendEnabled The `SendEnabled` enpoints allows users to query the SendEnabled entries of the `bank` module. Any denominations NOT returned, use the `Params.DefaultSendEnabled` value. ```shell theme={null} cosmos.bank.v1beta1.Query/SendEnabled ``` Example: ```shell theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.bank.v1beta1.Query/SendEnabled ``` Example Output: ```json theme={null} { "send_enabled": [ { "denom": "foocoin", "enabled": true }, { "denom": "barcoin" } ], "pagination": { "next-key": null, "total": 2 } } ``` ## concepts/changes specific to heimdall Since heimdall is a unique component of the PoS architecture and not a traditional Cosmos SDK app, some changes had to be made in order to ensure correct functionality, some of which are : * MsgSetSendEnabled is not supported since POL is the only denom used. * (Un)delegation is not supported since staking related logic is handled by PoS [core contracts](https://github.com/0xPolygon/core-contracts/tree/main/contracts) deployed on Ethereum. * A default fee of 10^15 POL (`DefaultFeeInPol`) is deducted from the tx sender. # Chain management Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/chain-management How the Heimdall chainmanager module stores and exposes PoS protocol parameters, including contract addresses, chain IDs, and confirmation block counts. The chainmanager module stores and exposes the PoS protocol parameters. These params include addresses of contracts deployed on mainchain (Ethereum) and bor chain (Bor), chain ids, mainchain and bor chain confirmation blocks. ```protobuf theme={null} message ChainParams { option (gogoproto.equal) = true; string bor_chain_id = 1 [ (amino.dont_omitempty) = true ]; string heimdall_chain_id = 2 [ (amino.dont_omitempty) = true ]; string pol_token_address = 3 [ (amino.dont_omitempty) = true ]; string staking_manager_address = 4 [ (amino.dont_omitempty) = true ]; string slash_manager_address = 5 [ (amino.dont_omitempty) = true ]; string root_chain_address = 6 [ (amino.dont_omitempty) = true ]; string staking_info_address = 7 [ (amino.dont_omitempty) = true ]; string state_sender_address = 8 [ (amino.dont_omitempty) = true ]; string state_receiver_address = 9 [ (amino.dont_omitempty) = true ]; string validator_set_address = 10 [ (amino.dont_omitempty) = true ]; } message Params { option (gogoproto.equal) = true; ChainParams chain_params = 1 [ (amino.dont_omitempty) = true, (gogoproto.nullable) = false ]; uint64 main_chain_tx_confirmations = 2 [ (amino.dont_omitempty) = true ]; uint64 bor_chain_tx_confirmations = 3 [ (amino.dont_omitempty) = true ]; } ``` ## Query commands One can run the following query commands from the chainmanager module : * `params` - Fetch the parameters associated with the chainmanager module. ### CLI commands ```bash theme={null} heimdalld query chainmanager params ``` ### GRPC Endpoints ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.chainmanager.Query/GetChainManagerParams ``` ### REST endpoints ```bash theme={null} curl localhost:1317/heimdallv2/chainmanager/params ``` # Changes from v1 Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/changes-from-v1 Key differences between Heimdall v1 and v2, including encoding changes, the keyring requirement for validator signing, and vote extensions. The main differences between Heimdall v1 and v2 are described below. ## Encoding change to Base64 Data encoding format changed from hex-encoded to Base64-encoded. Example: * Hex encoded: `BJSk2KCI4snP2Cw/ntDdgp8R25XJ2xg18KL67fyEAwgtPMpeq5APSUHrkv5wtgrFfpmcDivnP8HPGufyyXnByxo=` * Base64 encoded: `0x0494a4d8a088e2c9cfd82c3f9ed0dd829f11db95c9db1835f0a2faedfc8403082d3cca5eab900f4941eb92fe70b60ac57e999c0e2be73fc1cf1ae7f2c979c1cb1a` ## Validator signing key and keyring In Heimdall-v2, validator signing keys must be imported into the keyring before the node can sign transactions. This replaces the v1 approach of reading the key directly from a config file. From the Cosmos documentation: > The keyring holds the private/public keypairs used to interact with a node. For instance, a validator key needs to be set up before running the blockchain node, so that blocks can be correctly signed. The private key can be stored in different locations, called "backends", such as a file or the operating system's own key storage. For details on keyring configuration, see the [Cosmos SDK documentation](https://docs.cosmos.network/v0.46/run-node/keyring.html). To import your validator private key into the keyring: 1. Get your Base64-encoded private key: ```bash theme={null} cat /var/lib/heimdall/config/priv_validator_key.json ``` 2. Convert the Base64-encoded key to hex: ```bash theme={null} echo "" | base64 -d | xxd -p -c 256 ``` 3. Import the hex-encoded key into the keyring: ```bash theme={null} heimdalld keys import-hex --home ``` When you first import a key, you are prompted for a password. This password is required each time you sign a transaction. 4. When running a transaction command, specify `--from` with the key name: ```bash theme={null} heimdalld tx gov vote 1 yes --from ``` ## Vote extensions in each block In Heimdall-v2, the first transaction of each block contains encoded vote extensions. To decode these, use the additional decode command provided in `heimdalld`. # Checkpoints Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/checkpoints How Heimdall checkpoints work, from proposal through Ethereum submission and acknowledgment, including the ABCI++ processing flow and message types. Checkpoints are vital components of the Polygon network, representing snapshots of the Bor chain state. These checkpoints are attested by a majority of the validator set before being validated and submitted on Ethereum contracts. Heimdall, an integral part of this process, manages checkpoint functionalities using the `checkpoint` module. It coordinates with the Bor chain to verify checkpoint root hashes when a new checkpoint is proposed. ## Overview Heimdall selects the next proposer using CometBFT’s leader selection algorithm.\ The multi-stage checkpoint process is crucial due to potential failures when submitting checkpoints on the Ethereum chain caused by factors like gas limit, network traffic, or high gas fees. Each checkpoint has a validator as the proposer.\ The outcome of a checkpoint on the Ethereum chain (success or failure) triggers an ack (acknowledgment) or no-ack (no acknowledgment) transaction,\ altering the proposer for the next checkpoint on Heimdall. Checkpoint Flow.png ## Flow ### Checkpoint Proposal A checkpoint proposal is initiated by a proposer, a validator with POL tokens staked on the L1 Ethereum root chain.\ The checkpointing process is managed by the `bridge processor` which generates a `MsgCheckpoint` and broadcasts it as a transaction. * The proposer derives the root hash from the Bor chain contract. * Due to Bor’s finality time, the root hash may not always reflect the latest Bor tip. ### Checkpoint Processing in Heimdall Once the checkpoint message is included in a Heimdall block, it undergoes processing through the message handling system.\ Each validator node independently verifies the checkpoint by checking the Bor root hash provided in the message against its local Bor chain. ### ABCI++ Processing Flow for the checkpoint submission on Heimdall * `Prepare Proposal`: During the proposal phase, the checkpoint message `MsgCheckpoint` is included in the proposed block only if dry-running this tx does not return any errors. * `Process Proposal`: The proposal is validated to ensure correctness. * `Extend Vote`: As part of the voting process, validators execute a side transaction to verify the checkpoint against their local Bor data. If the checkpoint is valid, validators include a vote extension confirming their approval. * `Verify Vote Extension`: Injected vote extensions are verified.\ • `Next block - Finalize`: In the next block, the finalized votes are processed, and the checkpoint is considered approved if a sufficient majority supports it.\ The `preBlocker` triggers post-tx handlers performing the Heimdall state changes when the checkpoint is finally saved in the checkpoint buffer as the checkpoint that needs to be further bridged to the Ethereum L1 root chain. ### Submission to Ethereum (L1) Once approved, the checkpoint is added to a checkpoint buffer and an event is emitted. The bridge system, which listens for these events, submits the checkpoint data along with validator signatures to the Ethereum root chain. ### Acknowledgment from Ethereum (L1) After the checkpoint is successfully included on the Ethereum chain, an acknowledgment `MsgCpAck` is sent back to Heimdall from the bridge processor.\ This acknowledgment, once processed through the ABCI++ flow with side and post-tx handlers: updates the state, flushes processed checkpoints from the buffer, and increments the number of ACK counters to track confirmations of checkpoints.\ Additionally, the selection of the next checkpoint proposer is adjusted based on the updated state. ### Missing Checkpoint Acknowledgment from Ethereum (L1) The `MsgCpNoAck` message is broadcast by the bridge processor to indicate that a checkpoint was potentially transferred to the Ethereum chain but has not received an acknowledgment.\ A background routine periodically checks for time elapsed and publishes the No-ACK signal. No-ACK is sent if a sufficient amount of time has passed since: * the last checkpoint was created on the Heimdall-v2 chain and * the last No-ACK was issued.\ To conclude, the No-ACKs are triggered only when a checkpoint acknowledgment is overdue, ensuring they are not sent too frequently.\ This message is broadcasted only by the proposer. This entire flow ensures that checkpoints are securely proposed, verified, and finalized across the Heimdall and Ethereum chains in a decentralized manner. Checkpoint ABCI diagram.png ### Messages #### MsgCheckpoint `MsgCheckpoint` defines a message for creating a checkpoint on the Ethereum chain. ```protobuf theme={null} message MsgCheckpoint { option (cosmos.msg.v1.signer) = "proposer"; option (amino.name) = "heimdallv2/checkpoint/MsgCheckpoint"; option (gogoproto.equal) = true; option (gogoproto.goproto_getters) = true; string proposer = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 start_block = 2 [ (amino.dont_omitempty) = true ]; uint64 end_block = 3 [ (amino.dont_omitempty) = true ]; bytes root_hash = 4 [ (amino.dont_omitempty) = true ]; bytes account_root_hash = 5 [ (amino.dont_omitempty) = true ]; string bor_chain_id = 6 [ (amino.dont_omitempty) = true ]; } ``` #### MsgCpAck `MsgCpAck` defines a message for creating the ack tx of a submitted checkpoint. ```protobuf theme={null} message MsgCpAck { option (cosmos.msg.v1.signer) = "from"; option (amino.name) = "heimdallv2/checkpoint/MsgCpAck"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = true; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 number = 2 [ (amino.dont_omitempty) = true ]; string proposer = 3 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 start_block = 4 [ (amino.dont_omitempty) = true ]; uint64 end_block = 5 [ (amino.dont_omitempty) = true ]; bytes root_hash = 6 [ (amino.dont_omitempty) = true ]; bytes tx_hash = 7 [ (amino.dont_omitempty) = true ]; uint64 log_index = 8 [ (amino.dont_omitempty) = true ]; } ``` #### MsgCheckpointNoAck `MsgCpNoAck` defines a message for creating the no-ack tx of a checkpoint. ```protobuf theme={null} message MsgCpNoAck { option (cosmos.msg.v1.signer) = "from"; option (amino.name) = "heimdallv2/checkpoint/MsgCpNoAck"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = true; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; } ``` ## Interact with the Node ### Tx Commands #### Send checkpoint ```bash theme={null} heimdalld tx checkpoint send-checkpoint --proposer= --start-block= --end-block= --root-hash= --account-root= --bor-chain-id= --auto-configure=true/false ``` #### Send checkpoint ack ```bash theme={null} heimdalld tx checkpoint send-ack --tx-hash= --log-index= --header=
--proposer= --auto-configure=true/false ``` #### Send checkpoint no-ack ```bash theme={null} heimdalld tx checkpoint checkpoint-no-ack --from ``` ## CLI Query Commands One can run the following query commands from the checkpoint module: * `get-params` - Get checkpoint params * `get-overview` - Get checkpoint overview * `get-ack-count` - Get checkpoint ack count * `get-checkpoint` - Get checkpoint based on its number * `get-checkpoint-latest` - Get the latest checkpoint * `get-checkpoint-buffer` - Get the checkpoint buffer * `get-last-no-ack` - Get the last no ack * `get-next-checkpoint` - Get the next checkpoint * `get-current-proposer` - Get the current proposer * `get-proposers` - Get the proposers * `get-checkpoint-list` - Get the list of checkpoints ```bash theme={null} heimdalld query checkpoint get-params ``` ```bash theme={null} heimdalld query checkpoint get-overview ``` ```bash theme={null} heimdalld query checkpoint get-ack-count ``` ```bash theme={null} heimdalld query checkpoint get-checkpoint ``` ```bash theme={null} heimdalld query checkpoint get-checkpoint-latest ``` ```bash theme={null} heimdalld query checkpoint get-checkpoint-buffer ``` ```bash theme={null} heimdalld query checkpoint get-last-no-ack ``` ```bash theme={null} heimdalld query checkpoint get-next-checkpoint ``` ```bash theme={null} heimdalld query checkpoint get-current-proposer ``` ```bash theme={null} heimdalld query checkpoint get-proposers ``` ```bash theme={null} heimdalld query checkpoint get-checkpoint-list ``` ## GRPC Endpoints The endpoints and the params are defined in the [checkpoint/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/checkpoint/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetCheckpointParams ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetCheckpointOverview ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetAckCount ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetCheckpointLatest ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetCheckpointBuffer ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetLastNoAck ``` ```bash theme={null} grpcurl -plaintext -d '{"bor_chain_id": <>}' localhost:9090 heimdallv2.checkpoint.Query/GetNextCheckpoint ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetCurrentProposer ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetProposers ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.checkpoint.Query/GetCheckpointList ``` ```bash theme={null} grpcurl -plaintext -d '{"tx_hash": <>}' localhost:9090 heimdallv2.checkpoint.QueryGetCheckpointSignatures ``` ```bash theme={null} grpcurl -plaintext -d '{"number": <>}' localhost:9090 heimdallv2.checkpoint.Query/GetCheckpoint ``` ## REST Endpoints The endpoints and the params are defined in the [checkpoint/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/checkpoint/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} curl localhost:1317/checkpoints/params ``` ```bash theme={null} curl localhost:1317/checkpoints/overview ``` ```bash theme={null} curl localhost:1317/checkpoints/count ``` ```bash theme={null} curl localhost:1317/checkpoints/latest ``` ```bash theme={null} curl localhost:1317/checkpoints/buffer ``` ```bash theme={null} curl localhost:1317/checkpoints/last-no-ack ``` ```bash theme={null} curl localhost:1317/checkpoints/prepare-next ``` ```bash theme={null} curl localhost:1317/checkpoint/proposers/current ``` ```bash theme={null} curl localhost:1317/checkpoint/proposers/{times} ``` ```bash theme={null} curl localhost:1317/checkpoints/list ``` ```bash theme={null} curl localhost:1317/checkpoints/signatures/{tx_hash} ``` ```bash theme={null} curl localhost:1317/checkpoints/{number} ``` # Clerk Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/clerk How the Heimdall clerk module manages state-sync event records from Ethereum, including the EventRecord structure and the validation flow. ## Preliminary terminology * A `StateSender` is a contract deployed on L1 (Ethereum) responsible for emitting state-sync events. * A `StateReceiver` is a contract deployed on L2 (Bor) responsible for receiving state-sync events. * A `EventRecord` is a record of the state-sync event stored in the heimdall state. ## Overview Clerk module manages generic event records from the Ethereum blockchain related to state-sync events.\ These are specially designed events that are emitted by the StateSender contract on the L1 chain to notify the L2 nodes (Bor in case of PoS) about the state changes in the L1. Once the bridge processes the events, the clerk module listens to these events and stores them in the database for further processing. ## State-Sync Mechanism It's a mechanism for state-management between the Ethereum and Bor chain. The events generated are called state-sync events. This is a way to move data from the L1 chain to the L2 chain. State-Sync Flow ## How it works An `EventRecord` is defined by the data structure : ```protobuf theme={null} message EventRecord { option (gogoproto.goproto_getters) = false; option (gogoproto.equal) = false; uint64 id = 1 [ (amino.dont_omitempty) = true ]; string contract = 2 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; bytes data = 3 [ (amino.dont_omitempty) = true ]; string tx_hash = 4 [ (amino.dont_omitempty) = true ]; uint64 log_index = 5 [ (amino.dont_omitempty) = true ]; string bor_chain_id = 6 [ (amino.dont_omitempty) = true ]; google.protobuf.Timestamp record_time = 7 [ (gogoproto.stdtime) = true, (gogoproto.nullable) = false, (amino.dont_omitempty) = true ]; } ``` * `id` is the unique identifier for the event record, Generated by the `StateSender` contract. * `contract` is the address of the contract on the L2 chain on which the event will be processed. * `data` is the data of the event which will be processed by the contract. * `txHash` is the transaction hash of the event on the L1 chain. * `logIndex` is the log index of the event on the L1 chain. * `borChainID` is the chain id of the bor chain. * `recordTime` is the time at which the event was recorded in heimdall state. The bridge will listen to the state-sync events from L1 and generate a txn with `MsgEventRecord` which is responsible for validating events from `StateSender` contract and storing the `EventRecord` on the heimdall state for bor to use. ```protobuf theme={null} message MsgEventRecord { option (amino.name) = "heimdallv2/clerk/MsgEventRecord"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = false; option (cosmos.msg.v1.signer) = "from"; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; string tx_hash = 2 [ (amino.dont_omitempty) = true ]; uint64 log_index = 3 [ (amino.dont_omitempty) = true ]; uint64 block_number = 4 [ (amino.dont_omitempty) = true ]; string contract_address = 5 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; bytes data = 6 [ (amino.dont_omitempty) = true ]; uint64 id = 7 [ (amino.dont_omitempty) = true ]; string chain_id = 8 [ (amino.dont_omitempty) = true ]; } ``` [Handler](https://github.com/0xPolygon/heimdall-v2/blob/develop/x/clerk/keeper/msg_server.go) for this transaction validates for multiple conditions including `TxHash` and `LogIndex` to ensure that the event exists on L1 and the data is not tampered with, It throws `Older invalid tx found` error if the event is already processed. Once the event is validated by the Handler, it will go to `SideHandleMsgEventRecord` in each validator node and after verifying the event, the validators will vote with either a `YES` return an error for failed verification. Only when there is a majority of `YES` votes, The event will be processed by `PostHandleMsgEventRecord` which will persist the event in the state via keeper. ## Adding an event manually A validator can use the CLI to add an event to the state if it is missing and was not processed by the bridge: ```bash theme={null} heimdalld tx clerk handle-msg-event-record [from] [tx-hash] [log-index] [block-number] [contract-address] [data] [id] [chain-id] ``` ## Query commands One can run the following query commands from the clerk module : * `record` - Query for a specific event record by its ID. * `record-list` - Query a list of event records by page and limit. * `is-old-tx` - Query if the event record is already processed. * `latest-record-id` - Query the latest record (state-sync) id from L1. ### CLI commands ```bash theme={null} heimdalld query clerk record [record-id] ``` ```bash theme={null} heimdalld query clerk record-list [page] [limit] ``` ```bash theme={null} heimdalld query clerk record-list-with-time [from-id] [to-time] ``` ```bash theme={null} heimdalld query clerk record-sequence [tx-hash] [log-index] ``` ```bash theme={null} heimdalld query clerk is-old-tx [tx-hash] [log-index] ``` ```bash theme={null} heimdalld query clerk latest-record-id ``` ### GRPC Endpoints The endpoints and the params are defined in the [clerk/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/clerk/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.clerk.Query/GetRecordList ``` ```bash theme={null} grpcurl -plaintext -d '{"record_id": <>}' localhost:9090 heimdallv2.clerk.Query/GetRecordById ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.clerk.Query/GetRecordListWithTime ``` ```bash theme={null} grpcurl -plaintext -d '{"tx_hash": <>, "log_index": <>}' localhost:9090 heimdallv2.clerk.Query/GetRecordSequence ``` ```bash theme={null} grpcurl -plaintext -d '{"tx_hash": <>, "log_index": <>}' localhost:9090 heimdallv2.clerk.Query/IsClerkTxOld ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.clerk.Query/GetLatestRecordId ``` ### REST endpoints The endpoints and the params are defined in the [clerk/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/clerk/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} curl localhost:1317/clerk/event-records/list?page=&limit= ``` ```bash theme={null} curl localhost:1317/clerk/event-records/latest-id ``` ```bash theme={null} curl localhost:1317/clerk/event-records/ ``` ```bash theme={null} curl localhost:1317/clerk/time?from_id=&to_time=&page=&limit= ``` ```bash theme={null} curl localhost:1317/clerk/sequence?tx_hash=&log_index= ``` ```bash theme={null} curl localhost:1317/clerk/is-old-tx?tx_hash=&log_index= ``` # Governance Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/governance How the Heimdall governance module works, including proposal submission, voting, deposit handling, and parameter changes. ## Abstract This page specifies the Governance module of the Cosmos SDK, which was first described in the [Cosmos Whitepaper](https://cosmos.network/about/whitepaper) in June 2016. The module enables Cosmos SDK based blockchain to support an onchain governance system. In this system, holders of the native staking token of the chain can vote on proposals on a 1 token 1 vote basis. Next is a list of features the module currently supports: * **Proposal submission:** Users can submit proposals with a deposit. Once the minimum deposit is reached, the proposal enters voting period. The minimum deposit can be reached by collecting deposits from different users (including proposer) within deposit period. * **Vote:** Participants can vote on proposals that reached MinDeposit and entered voting period. * **Inheritance and penalties:** Delegators inherit their validator's vote if they don't vote themselves. * **Claiming deposit:** Users that deposited on proposals can recover their deposits if the proposal was accepted or rejected. If the proposal was vetoed, or never entered voting period (minimum deposit not reached within deposit period), the deposit is burned. The burn functionality is not supported by Heimdall, hence in any case the deposit is refunded to the proposer.\ Also, Heimdall does not support delegation, hence the inheritance is not to be considered. This module is in use on the Cosmos Hub (a.k.a [gaia](https://github.com/cosmos/gaia)). Features that may be added in the future are described in [Future Improvements](#future-improvements). ## Contents The following specification uses *POL* as the native staking token. The module can be adapted to any Proof-Of-Stake blockchain by replacing *POL* with the native staking token of the chain. * [Concepts](#concepts) * [Proposal submission](#proposal-submission) * [Deposit](#deposit) * [Vote](#vote) * [Software Upgrade](#software-upgrade) * [State](#state) * [Proposals](#proposals) * [Parameters and base types](#parameters-and-base-types) * [Deposit](#deposit-1) * [ValidatorGovInfo](#validatorgovinfo) * [Stores](#stores) * [Proposal Processing Queue](#proposal-processing-queue) * [Legacy Proposal](#legacy-proposal) * [Messages](#messages) * [Proposal Submission](#proposal-submission-1) * [Deposit](#deposit-2) * [Vote](#vote-1) * [Events](#events) * [EndBlocker](#endblocker) * [Handlers](#handlers) * [Parameters](#parameters) * [Client](#client) * [CLI](#cli) * [gRPC](#grpc) * [REST](#rest) * [Metadata](#metadata) * [Proposal](#proposal-3) * [Vote](#vote-5) * [Future Improvements](#future-improvements) ## Concepts *Disclaimer: This is work in progress. Mechanisms are susceptible to change.* The governance process is divided in a few steps that are outlined below: * **Proposal submission:** Proposal is submitted to the blockchain with a deposit. * **Vote:** Once deposit reaches a certain value (`MinDeposit`), proposal is confirmed and vote opens. Polygon Chain validators can then send `TxGovVote` transactions to vote on the proposal. * **Execution** After a period of time, the votes are tallied and depending on the result, the messages in the proposal will be executed. ### Proposal submission #### Right to submit a proposal Every account can submit proposals by sending a `MsgSubmitProposal` transaction. Once a proposal is submitted, it is identified by its unique `proposalID`. #### Proposal Messages A proposal includes an array of `sdk.Msg`s which are executed automatically if the proposal passes. The messages are executed by the governance `ModuleAccount` itself. Modules such as `x/upgrade`, that want to allow certain messages to be executed by governance only should add a whitelist within the respective msg server, granting the governance module the right to execute the message once a quorum has been reached. The governance module uses the `MsgServiceRouter` to check that these messages are correctly constructed and have a respective path to execute on but do not perform a full validity check. ### Deposit To prevent spam, proposals must be submitted with a deposit in the coins defined by the `MinDeposit` param. When a proposal is submitted, it has to be accompanied with a deposit that must be strictly positive, but can be inferior to `MinDeposit`. The submitter doesn't need to pay for the entire deposit on their own. The newly created proposal is stored in an *inactive proposal queue* and stays there until its deposit passes the `MinDeposit`. Other token holders can increase the proposal's deposit by sending a `Deposit` transaction. If a proposal doesn't pass the `MinDeposit` before the deposit end time (the time when deposits are no longer accepted), the proposal will be destroyed: the proposal will be removed from state and the deposit will be refunded (see x/gov `EndBlocker`). When a proposal deposit passes the `MinDeposit` threshold (even during the proposal submission) before the deposit end time, the proposal will be moved into the *active proposal queue* and the voting period will begin. The deposit is kept in escrow and held by the governance `ModuleAccount` until the proposal is finalized (passed or rejected). #### Deposit refund and burn When a proposal is finalized, the coins from the deposit are either refunded or burned according to the final tally of the proposal. In Heimdall, burn is not enabled, hence all the deposits will be refunded. * If the proposal is approved or rejected but *not* vetoed, each deposit will be automatically refunded to its respective depositor (transferred from the governance `ModuleAccount`). * When the proposal is vetoed with greater than 1/3, deposits will be refunded from the governance `ModuleAccount` and the proposal information along with its deposit information will be removed from state. * All refunded deposits are removed from the state. Events are issued when refunding a deposit. ### Vote #### Participants *Participants* are users that have the right to vote on proposals. On the Polygon Chain network, participants are validators. Other holders and users do not get the right to participate in governance. However, they can submit and deposit on proposals. Note that for *participants*, their voting power is calculated from their L1 POL stakes only. #### Voting period Once a proposal reaches `MinDeposit`, it immediately enters `Voting period`. We define `Voting period` as the interval between the moment the vote opens and the moment the vote closes. The initial value of `Voting period` is 2 weeks. #### Option set The option set of a proposal refers to the set of choices a participant can choose from when casting its vote. The initial option set includes the following options: * `Yes` * `No` * `NoWithVeto` * `Abstain` `NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` option allows voters to signal that they do not intend to vote in favor or against the proposal but accept the result of the vote. *Note: from the UI, for urgent proposals we should maybe add a ‘Not Urgent’ option that casts a `NoWithVeto` vote.* #### Weighted Votes *Weighted Votes are not supported in Heimdall*. [ADR-037](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-037-gov-split-vote.md) introduces the weighted vote feature which allows a staker to split their votes into several voting options. For example, it could use 70% of its voting power to vote Yes and 30% of its voting power to vote No. Often times the entity owning that address might not be a single individual. For example, a company might have different stakeholders who want to vote differently, and so it makes sense to allow them to split their voting power. Currently, it is not possible for them to do "passthrough voting" and giving their users voting rights over their tokens. However, with this system, exchanges can poll their users for voting preferences, and then vote onchain proportionally to the results of the poll. To represent weighted vote on chain, we use the following Protobuf message. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/gov.proto#L34-L47](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/gov.proto#L34-L47) protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/gov.proto#L181-L201](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/gov.proto#L181-L201) For a weighted vote to be valid, the `options` field must not contain duplicate vote options, and the sum of weights of all options must be equal to 1. ### Quorum Quorum is defined as the minimum percentage of voting power that needs to be cast on a proposal for the result to be valid. ### Expedited Proposals A proposal can be expedited, making the proposal use shorter voting duration and a higher tally threshold by its default. If an expedited proposal fails to meet the threshold within the scope of shorter voting duration, the expedited proposal is then converted to a regular proposal and restarts voting under regular voting conditions. #### Threshold Threshold is defined as the minimum proportion of `Yes` votes (excluding `Abstain` votes) for the proposal to be accepted. Initially, the threshold is set at 50% of `Yes` votes, excluding `Abstain` votes. A possibility to veto exists if more than 1/3rd of all votes are `NoWithVeto` votes. Note, both of these values are derived from the `TallyParams` onchain parameter, which is modifiable by governance. This means that proposals are accepted iff: * There exist staked tokens. * Quorum has been achieved. * The proportion of `Abstain` votes is inferior to 1/1. * The proportion of `NoWithVeto` votes is inferior to 1/3, including `Abstain` votes. * The proportion of `Yes` votes, excluding `Abstain` votes, at the end of the voting period is superior to 1/2. For expedited proposals, by default, the threshold is higher than with a *normal proposal*, namely, 66.7%. #### Inheritance *Inheritance is not supported in Heimdall, as there's no concept of tokens delegation*. If a delegator does not vote, it will inherit its validator vote. * If the delegator votes before its validator, it will not inherit from the validator's vote. * If the delegator votes after its validator, it will override its validator vote with its own. If the proposal is urgent, it is possible that the vote will close before delegators have a chance to react and override their validator's vote. This is not a problem, as proposals require more than 2/3rd of the total voting power to pass, when tallied at the end of the voting period. Because as little as 1/3 + 1 validation power could collude to censor transactions, non-collusion is already assumed for ranges exceeding this threshold. #### Validator’s punishment for non-voting At present, validators are not punished for failing to vote. #### Governance address Later, we may add permissioned keys that could only sign txs from certain modules. For the MVP, the `Governance address` will be the main validator address generated at account creation. This address corresponds to a different PrivKey than the CometBFT PrivKey which is responsible for signing consensus messages. Validators thus do not have to sign governance transactions with the sensitive CometBFT PrivKey. #### Burnable Params There are three parameters that define if the deposit of a proposal should be burned or returned to the depositors. * `BurnVoteVeto` burns the proposal deposit if the proposal gets vetoed. * `BurnVoteQuorum` burns the proposal deposit if the proposal deposit if the vote does not reach quorum. * `BurnProposalDepositPrevote` burns the proposal deposit if it does not enter the voting phase. > Note: These parameters are modifiable via governance. ## State ### Constitution `Constitution` is found in the genesis state. It is a string field intended to be used to descibe the purpose of a particular blockchain, and its expected norms. A few examples of how the constitution field can be used: * define the purpose of the chain, laying a foundation for its future development * set expectations for delegators (not supported in Heimdall) * set expectations for validators * define the chain's relationship to "meatspace" entities, like a foundation or corporation Since this is more of a social feature than a technical feature, we'll now get into some items that may have been useful to have in a genesis constitution: * What limitations on governance exist, if any? * is it okay for the community to slash the wallet of a whale that they no longer feel that they want around? (viz: Juno Proposal 4 and 16) * can governance "socially slash" a validator who is using unapproved MEV? (viz: commonwealth.im/osmosis) * In the event of an economic emergency, what should validators do? * Terra crash of May, 2022, saw validators choose to run a new binary with code that had not been approved by governance, because the governance token had been inflated to nothing. * What is the purpose of the chain, specifically? * best example of this is the Cosmos hub, where different founding groups, have different interpretations of the purpose of the network. This genesis entry, "constitution" hasn't been designed for existing chains, who should likely just ratify a constitution using their governance system. Instead, this is for new chains. It will allow for validators to have a much clearer idea of purpose and the expectations placed on them while operating their nodes. Likewise, for community members, the constitution will give them some idea of what to expect from both the "chain team" and the validators, respectively. This constitution is designed to be immutable, and placed only in genesis, though that could change over time by a pull request to the cosmos-sdk that allows for the constitution to be changed by governance. Communities wishing to make amendments to their original constitution should use the governance mechanism and a "signaling proposal" to do exactly that. **Ideal use scenario for a cosmos chain constitution** As a chain developer, you decide that you'd like to provide clarity to your key user groups: * validators * token holders * developers (yourself) You use the constitution to immutably store some Markdown in genesis, so that when difficult questions come up, the constutituon can provide guidance to the community. ### Proposals `Proposal` objects are used to tally votes and generally track the proposal's state. They contain an array of arbitrary `sdk.Msg`'s which the governance module will attempt to resolve and then execute if the proposal passes. `Proposal`'s are identified by a unique id and contains a series of timestamps: `submit_time`, `deposit_end_time`, `voting_start_time`, `voting_end_time` which track the lifecycle of a proposal protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L51-L99](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L51-L99) A proposal will generally require more than just a set of messages to explain its purpose but need some greater justification and allow a means for interested participants to discuss and debate the proposal. In most cases, **it is encouraged to have an off-chain system that supports the onchain governance process**. To accommodate for this, a proposal contains a special **`metadata`** field, a string, which can be used to add context to the proposal. The `metadata` field allows custom use for networks, however, it is expected that the field contains a URL or some form of CID using a system such as [IPFS](https://docs.ipfs.io/concepts/content-addressing/). To support the case of interoperability across networks, the SDK recommends that the `metadata` represents the following `JSON` template: ```json theme={null} { "title": "...", "description": "...", "forum": "...", // a link to the discussion platform (i.e. Discord) "other": "..." // any extra data that doesn't correspond to the other fields } ``` This makes it far easier for clients to support multiple networks. The metadata has a maximum length that is chosen by the app developer, and passed into the gov keeper as a config. The default maximum length in the SDK is 255 characters. #### Writing a module that uses governance There are many aspects of a chain, or of the individual modules that you may want to use governance to perform such as changing various parameters. This is very simple to do. First, write out your message types and `MsgServer` implementation. Add an `authority` field to the keeper which will be populated in the constructor with the governance module account: `govKeeper.GetGovernanceAccount().GetAddress()`. Then for the methods in the `msg_server.go`, perform a check on the message that the signer matches `authority`. This will prevent any user from executing that message. ### Parameters and base types `Parameters` define the rules according to which votes are run. There can only be one active parameter set at any given time. If governance wants to change a parameter set, either to modify a value or add/remove a parameter field, a new parameter set has to be created and the previous one rendered inactive. #### DepositParams protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L152-L162](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L152-L162) #### VotingParams protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L164-L168](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L164-L168) #### TallyParams protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L170-L182](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L170-L182) Parameters are stored in a global `GlobalParams` KVStore. Additionally, we introduce some basic types: ```go theme={null} type Vote byte const ( VoteYes = 0x1 VoteNo = 0x2 VoteNoWithVeto = 0x3 VoteAbstain = 0x4 ) type ProposalType string const ( ProposalTypePlainText = "Text" ProposalTypeSoftwareUpgrade = "SoftwareUpgrade" // currently not supported in Heimdall ) type ProposalStatus byte const ( StatusNil ProposalStatus = 0x00 StatusDepositPeriod ProposalStatus = 0x01 // Proposal is submitted. Participants can deposit on it but not vote StatusVotingPeriod ProposalStatus = 0x02 // MinDeposit is reached, participants can vote StatusPassed ProposalStatus = 0x03 // Proposal passed and successfully executed StatusRejected ProposalStatus = 0x04 // Proposal has been rejected StatusFailed ProposalStatus = 0x05 // Proposal passed but failed execution ) ``` ### Deposit protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L38-L49](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L38-L49) ### ValidatorGovInfo This type is used in a temp map when tallying ```go theme={null} type ValidatorGovInfo struct { Minus sdk.Dec Vote Vote } ``` ## Stores :::note Stores are KVStores in the multi-store. The key to find the store is the first parameter in the list ::: We will use one KVStore `Governance` to store four mappings: * A mapping from `proposalID|'proposal'` to `Proposal`. * A mapping from `proposalID|'addresses'|address` to `Vote`. This mapping allows us to query all addresses that voted on the proposal along with their vote by doing a range query on `proposalID:addresses`. * A mapping from `ParamsKey|'Params'` to `Params`. This map allows to query all x/gov params. * A mapping from `VotingPeriodProposalKeyPrefix|proposalID` to a single byte. This allows us to know if a proposal is in the voting period or not with very low gas cost. For pseudocode purposes, here are the two function we will use to read or write in stores: * `load(StoreKey, Key)`: Retrieve item stored at key `Key` in store found at key `StoreKey` in the multistore * `store(StoreKey, Key, value)`: Write value `Value` at key `Key` in store found at key `StoreKey` in the multistore ### Proposal Processing Queue **Store:** * `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the `ProposalIDs` of proposals that reached `MinDeposit`. During each `EndBlock`, all the proposals that have reached the end of their voting period are processed. To process a finished proposal, the application tallies the votes, computes the votes of each validator and checks if every validator in the validator set has voted. If the proposal is accepted/rejected, deposits are refunded. Finally, the proposal content `Handler` is executed. And the pseudocode for the `ProposalProcessingQueue`: ```go theme={null} in EndBlock do for finishedProposalID in GetAllFinishedProposalIDs(block.Time) proposal = load(Governance, ) // proposal is a const key validators = Keeper.getAllValidators() tmpValMap := map(sdk.AccAddress)ValidatorGovInfo // Initiate mapping at 0. This is the amount of shares of the validator's vote that will be overridden by their delegator's votes for each validator in validators tmpValMap(validator.OperatorAddr).Minus = 0 // Tally voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal for each (voterAddress, vote) in voterIterator _, isVal = stakingKeeper.getValidator(voterAddress) if (isVal) tmpValMap(voterAddress).Vote = vote tallyingParam = load(GlobalParams, 'TallyingParam') // Update tally if validator voted for each validator in validators if tmpValMap(validator).HasVoted proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) // Check if proposal is accepted or rejected totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes if (proposal.Votes.YesVotes/totalNonAbstain > tallyingParam.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < tallyingParam.Veto) // proposal was accepted at the end of the voting period // refund deposits (non-voters already punished) for each (amount, depositor) in proposal.Deposits depositor.PolBalance += amount stateWriter, err := proposal.Handler() if err != nil // proposal passed but failed during state execution proposal.CurrentStatus = ProposalStatusFailed else // proposal pass and state is persisted proposal.CurrentStatus = ProposalStatusAccepted stateWriter.save() else // proposal was rejected proposal.CurrentStatus = ProposalStatusRejected store(Governance, , proposal) ``` ### Legacy Proposal :::warning Legacy proposals are deprecated. Use the new proposal flow by granting the governance module the right to execute the message. ::: A legacy proposal is the old implementation of governance proposal. Contrary to proposal that can contain any messages, a legacy proposal allows to submit a set of pre-defined proposals. These proposals are defined by their types and handled by handlers that are registered in the gov v1beta1 router. More information on how to submit proposals in the [client section](#client). ## Messages ### Proposal Submission Proposals can be submitted by any account via a `MsgSubmitProposal` transaction. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L42-L69](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L42-L69) All `sdk.Msgs` passed into the `messages` field of a `MsgSubmitProposal` message must be registered in the app's `MsgServiceRouter`. Each of these messages must have one signer, namely the gov module account. And finally, the metadata length must not be larger than the `maxMetadataLen` config passed into the gov keeper. The `initialDeposit` must be strictly positive and conform to the accepted denom of the `MinDeposit` param. **State modifications:** * Generate new `proposalID` * Create new `Proposal` * Initialise `Proposal`'s attributes * Decrease balance of sender by `InitialDeposit` * If `MinDeposit` is reached: * Push `proposalID` in `ProposalProcessingQueue` * Transfer `InitialDeposit` from the `Proposer` to the governance `ModuleAccount` ### Deposit Once a proposal is submitted, if `Proposal.TotalDeposit < ActiveParam.MinDeposit`, POL holders can send `MsgDeposit` transactions to increase the proposal's deposit. A deposit is accepted iff: * The proposal exists * The proposal is not in the voting period * The deposited coins are conform to the accepted denom from the `MinDeposit` param protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L134-L147](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L134-L147) **State modifications:** * Decrease balance of sender by `deposit` * Add `deposit` of sender in `proposal.Deposits` * Increase `proposal.TotalDeposit` by sender's `deposit` * If `MinDeposit` is reached: * Push `proposalID` in `ProposalProcessingQueueEnd` * Transfer `Deposit` from the `proposer` to the governance `ModuleAccount` ### Vote Once `ActiveParam.MinDeposit` is reached, voting period starts. From there, Polygon Chain validators are able to send `MsgVote` transactions to cast their vote on the proposal. protobuf reference [https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L92-L108](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L92-L108) **State modifications:** * Record `Vote` of sender :::note Gas cost for this message has to take into account the future tallying of the vote in EndBlocker. ::: ## Events The governance module emits the following events: ### EndBlocker | Type | Attribute Key | Attribute Value | | ------------------ | ---------------- | ------------------ | | inactive\_proposal | proposal\_id | `{proposalID}` | | inactive\_proposal | proposal\_result | `{proposalResult}` | | active\_proposal | proposal\_id | `{proposalID}` | | active\_proposal | proposal\_result | `{proposalResult}` | ### Handlers #### MsgSubmitProposal | Type | Attribute Key | Attribute Value | | --------------------- | --------------------- | ----------------- | | submit\_proposal | proposal\_id | `{proposalID}` | | submit\_proposal \[0] | voting\_period\_start | `{proposalID}` | | proposal\_deposit | amount | `{depositAmount}` | | proposal\_deposit | proposal\_id | `{proposalID}` | | message | module | governance | | message | action | submit\_proposal | | message | sender | `{senderAddress}` | * \[0] Event only emitted if the voting period starts during the submission. #### MsgVote | Type | Attribute Key | Attribute Value | | -------------- | ------------- | ----------------- | | proposal\_vote | option | `{voteOption}` | | proposal\_vote | proposal\_id | `{proposalID}` | | message | module | governance | | message | action | vote | | message | sender | `{senderAddress}` | #### MsgVoteWeighted | Type | Attribute Key | Attribute Value | | -------------- | ------------- | ----------------------- | | proposal\_vote | option | `{weightedVoteOptions}` | | proposal\_vote | proposal\_id | `{proposalID}` | | message | module | governance | | message | action | vote | | message | sender | `{senderAddress}` | #### MsgDeposit | Type | Attribute Key | Attribute Value | | ---------------------- | --------------------- | ----------------- | | proposal\_deposit | amount | `{depositAmount}` | | proposal\_deposit | proposal\_id | `{proposalID}` | | proposal\_deposit \[0] | voting\_period\_start | `{proposalID}` | | message | module | governance | | message | action | deposit | | message | sender | `{senderAddress}` | * \[0] Event only emitted if the voting period starts during the submission. ## Parameters The governance module contains the following parameters: | Key | Type | Example | | -------------------------------- | ---------------- | --------------------------------------- | | min\_deposit | array (coins) | `[{"denom":"pol","amount":"10000000"}]` | | max\_deposit\_period | string (time ns) | "172800000000000" (17280s) | | voting\_period | string (time ns) | "172800000000000" (17280s) | | quorum | string (dec) | "0.334000000000000000" | | threshold | string (dec) | "0.500000000000000000" | | veto | string (dec) | "0.334000000000000000" | | expedited\_threshold | string (time ns) | "0.667000000000000000" | | expedited\_voting\_period | string (time ns) | "86400000000000" (8600s) | | expedited\_min\_deposit | array (coins) | `[{"denom":"pol","amount":"50000000"}]` | | burn\_proposal\_deposit\_prevote | bool | false | | burn\_vote\_quorum | bool | false | | burn\_vote\_veto | bool | false | | min\_initial\_deposit\_ratio | string | "0.1" | **NOTE**: The governance module contains parameters that are objects unlike other modules. If only a subset of parameters are desired to be changed, only they need to be included and not the entire parameter object structure. ## Client ### CLI A user can query and interact with the `gov` module using the CLI. #### Query The `query` commands allow users to query `gov` state. ```bash theme={null} heimdalld query gov --help ``` ##### deposit The `deposit` command allows users to query a deposit for a given proposal from a given depositor. ```bash theme={null} heimdalld query gov deposit [proposal-id] [depositer-addr] [flags] ``` Example: ```bash theme={null} heimdalld query gov deposit 1 0x... ``` Example Output: ```bash theme={null} amount: - amount: "100" denom: pol depositor: 0x... proposal_id: "1" ``` ##### deposits The `deposits` command allows users to query all deposits for a given proposal. ```bash theme={null} heimdalld query gov deposits [proposal-id] [flags] ``` Example: ```bash theme={null} heimdalld query gov deposits 1 ``` Example Output: ```bash theme={null} deposits: - amount: - amount: "100" denom: pol depositor: 0x... proposal_id: "1" pagination: next_key: null total: "0" ``` ##### param The `param` command allows users to query a given parameter for the `gov` module. ```bash theme={null} heimdalld query gov param [param-type] [flags] ``` Example: ```bash theme={null} heimdalld query gov param voting ``` Example Output: ```bash theme={null} voting_period: "172800000000000" ``` ##### params The `params` command allows users to query all parameters for the `gov` module. ```bash theme={null} heimdalld query gov params [flags] ``` Example: ```bash theme={null} heimdalld query gov params ``` Example Output: ```bash theme={null} deposit_params: max_deposit_period: 86400s min_deposit: - amount: "10000000" denom: pol params: expedited_min_deposit: - amount: "50000000" denom: pol expedited_threshold: "0.670000000000000000" expedited_voting_period: 43200s max_deposit_period: 86400s min_deposit: - amount: "10000000" denom: pol min_initial_deposit_ratio: "0.000000000000000000" proposal_cancel_burn_rate: "0.500000000000000000" quorum: "0.334000000000000000" threshold: "0.500000000000000000" veto_threshold: "0.334000000000000000" voting_period: 86400s tally_params: quorum: "0.334000000000000000" threshold: "0.500000000000000000" veto_threshold: "0.334000000000000000" voting_params: voting_period: 86400s ``` ##### proposal The `proposal` command allows users to query a given proposal. ```bash theme={null} heimdalld query gov proposal [proposal-id] [flags] ``` Example: ```bash theme={null} heimdalld query gov proposal 1 ``` Example Output: ```bash theme={null} deposit_end_time: "2022-03-30T11:50:20.819676256Z" final_tally_result: abstain_count: "0" no_count: "0" no_with_veto_count: "0" yes_count: "0" id: "1" messages: - '@type': /cosmos.bank.v1beta1.MsgSend amount: - amount: "10" denom: pol from_address: 0x... to_address: 0x... metadata: AQ== status: PROPOSAL_STATUS_DEPOSIT_PERIOD submit_time: "2022-03-28T11:50:20.819676256Z" total_deposit: - amount: "10" denom: pol voting_end_time: null voting_start_time: null ``` ##### proposals The `proposals` command allows users to query all proposals with optional filters. ```bash theme={null} heimdalld query gov proposals [flags] ``` Example: ```bash theme={null} heimdalld query gov proposals ``` Example Output: ```bash theme={null} pagination: next_key: null total: "0" proposals: - deposit_end_time: "2022-03-30T11:50:20.819676256Z" final_tally_result: abstain_count: "0" no_count: "0" no_with_veto_count: "0" yes_count: "0" id: "1" messages: - '@type': /cosmos.bank.v1beta1.MsgSend amount: - amount: "10" denom: pol from_address: 0x... to_address: 0x... metadata: AQ== status: PROPOSAL_STATUS_DEPOSIT_PERIOD submit_time: "2022-03-28T11:50:20.819676256Z" total_deposit: - amount: "10" denom: pol voting_end_time: null voting_start_time: null - deposit_end_time: "2022-03-30T14:02:41.165025015Z" final_tally_result: abstain_count: "0" no_count: "0" no_with_veto_count: "0" yes_count: "0" id: "2" messages: - '@type': /cosmos.bank.v1beta1.MsgSend amount: - amount: "10" denom: pol from_address: 0x... to_address: 0x... metadata: AQ== status: PROPOSAL_STATUS_DEPOSIT_PERIOD submit_time: "2022-03-28T14:02:41.165025015Z" total_deposit: - amount: "10" denom: pol voting_end_time: null voting_start_time: null ``` ##### proposer The `proposer` command allows users to query the proposer for a given proposal. ```bash theme={null} heimdalld query gov proposer [proposal-id] [flags] ``` Example: ```bash theme={null} heimdalld query gov proposer 1 ``` Example Output: ```bash theme={null} proposal_id: "1" proposer: 0x... ``` ##### tally The `tally` command allows users to query the tally of a given proposal vote. ```bash theme={null} heimdalld query gov tally [proposal-id] [flags] ``` Example: ```bash theme={null} heimdalld query gov tally 1 ``` Example Output: ```bash theme={null} abstain: "0" "no": "0" no_with_veto: "0" "yes": "1" ``` ##### vote The `vote` command allows users to query a vote for a given proposal. ```bash theme={null} heimdalld query gov vote [proposal-id] [voter-addr] [flags] ``` Example: ```bash theme={null} heimdalld query gov vote 1 0x... ``` Example Output: ```bash theme={null} option: VOTE_OPTION_YES options: - option: VOTE_OPTION_YES weight: "1.000000000000000000" proposal_id: "1" voter: 0x... ``` ##### votes The `votes` command allows users to query all votes for a given proposal. ```bash theme={null} heimdalld query gov votes [proposal-id] [flags] ``` Example: ```bash theme={null} heimdalld query gov votes 1 ``` Example Output: ```bash theme={null} pagination: next_key: null total: "0" votes: - option: VOTE_OPTION_YES options: - option: VOTE_OPTION_YES weight: "1.000000000000000000" proposal_id: "1" voter: 0x... ``` #### Transactions The `tx` commands allow users to interact with the `gov` module. ```bash theme={null} heimdalld tx gov --help ``` ##### deposit The `deposit` command allows users to deposit tokens for a given proposal. ```bash theme={null} heimdalld tx gov deposit [proposal-id] [deposit] [flags] ``` Example: ```bash theme={null} heimdalld tx gov deposit 1 10000000pol --from 0x... ``` ##### draft-proposal The `draft-proposal` command allows users to draft any type of proposal. The command returns a `draft_proposal.json`, to be used by `submit-proposal` after being completed. The `draft_metadata.json` is meant to be uploaded to [IPFS](#metadata). ```bash theme={null} heimdalld tx gov draft-proposal ``` ##### submit-proposal The `submit-proposal` command allows users to submit a governance proposal along with some messages and metadata. Messages, metadata and deposit are defined in a JSON file. ```bash theme={null} heimdalld tx gov submit-proposal [path-to-proposal-json] [flags] ``` Example: ```bash theme={null} heimdalld tx gov submit-proposal /path/to/proposal.json --from 0x... ``` where `proposal.json` contains: ```json theme={null} { "messages": [ { "@type": "/cosmos.bank.v1beta1.MsgSend", "from_address": "0x...", // The gov module module address "to_address": "0x...", "amount":[{"denom": "pol","amount": "10"}] } ], "metadata": "AQ==", "deposit": "10pol", "title": "Proposal Title", "summary": "Proposal Summary" } ``` :::note By default the metadata, summary and title are both limited by 255 characters, this can be overridden by the application developer. ::: :::tip When metadata is not specified, the title is limited to 255 characters and the summary 40x the title length. ::: ##### submit-legacy-proposal The `submit-legacy-proposal` command allows users to submit a governance legacy proposal along with an initial deposit. ```bash theme={null} heimdalld tx gov submit-legacy-proposal [command] [flags] ``` Example: ```bash theme={null} heimdalld tx gov submit-legacy-proposal --title="Test Proposal" --description="testing" --type="Text" --deposit="100000000pol" --from 0x... ``` Example (`param-change`): ```bash theme={null} heimdalld tx gov submit-legacy-proposal param-change proposal.json --from 0x... ``` ```json theme={null} { "title": "Test Proposal", "description": "testing, testing, 1, 2, 3", "changes": [ { "subspace": "staking", "key": "MaxValidators", "value": 100 } ], "deposit": "10000000pol" } ``` #### cancel-proposal Once proposal is canceled, from the deposits of proposal `deposits * proposal_cancel_ratio` will be sent to `ProposalCancelDest` address , if `ProposalCancelDest` is empty then deposits will be refunded. The `remaining deposits` will be sent to depositers. ```bash theme={null} heimdalld tx gov cancel-proposal [proposal-id] [flags] ``` Example: ```bash theme={null} heimdalld tx gov cancel-proposal 1 --from 0x... ``` ##### vote The `vote` command allows users to submit a vote for a given governance proposal. ```bash theme={null} heimdalld tx gov vote [command] [flags] ``` Example: ```bash theme={null} heimdalld tx gov vote 1 yes --from 0x... ``` ##### weighted-vote *Currently not supported in Heimdall.* The `weighted-vote` command allows users to submit a weighted vote for a given governance proposal. ```bash theme={null} heimdalld tx gov weighted-vote [proposal-id] [weighted-options] [flags] ``` Example: ```bash theme={null} heimdalld tx gov weighted-vote 1 yes=0.5,no=0.5 --from 0x... ``` ### gRPC A user can query the `gov` module using gRPC endpoints. #### Proposal The `Proposal` endpoint allows users to query a given proposal. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Proposal ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/Proposal ``` Example Output: ```bash theme={null} { "proposal": { "proposalId": "1", "content": {"@type":"/cosmos.gov.v1beta1.TextProposal","description":"testing, testing, 1, 2, 3","title":"Test Proposal"}, "status": "PROPOSAL_STATUS_VOTING_PERIOD", "finalTallyResult": { "yes": "0", "abstain": "0", "no": "0", "noWithVeto": "0" }, "submitTime": "2021-09-16T19:40:08.712440474Z", "depositEndTime": "2021-09-18T19:40:08.712440474Z", "totalDeposit": [ { "denom": "pol", "amount": "10000000" } ], "votingStartTime": "2021-09-16T19:40:08.712440474Z", "votingEndTime": "2021-09-18T19:40:08.712440474Z", "title": "Test Proposal", "summary": "testing, testing, 1, 2, 3" } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Proposal ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1.Query/Proposal ``` Example Output: ```bash theme={null} { "proposal": { "id": "1", "messages": [ {"@type":"/cosmos.bank.v1beta1.MsgSend","amount":[{"denom":"pol","amount":"10"}],"fromAddress":"0x...","toAddress":"0x..."} ], "status": "PROPOSAL_STATUS_VOTING_PERIOD", "finalTallyResult": { "yesCount": "0", "abstainCount": "0", "noCount": "0", "noWithVetoCount": "0" }, "submitTime": "2022-03-28T11:50:20.819676256Z", "depositEndTime": "2022-03-30T11:50:20.819676256Z", "totalDeposit": [ { "denom": "pol", "amount": "10000000" } ], "votingStartTime": "2022-03-28T14:25:26.644857113Z", "votingEndTime": "2022-03-30T14:25:26.644857113Z", "metadata": "AQ==", "title": "Test Proposal", "summary": "testing, testing, 1, 2, 3" } } ``` #### Proposals The `Proposals` endpoint allows users to query all proposals with optional filters. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Proposals ``` Example: ```bash theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.gov.v1beta1.Query/Proposals ``` Example Output: ```bash theme={null} { "proposals": [ { "proposalId": "1", "status": "PROPOSAL_STATUS_VOTING_PERIOD", "finalTallyResult": { "yes": "0", "abstain": "0", "no": "0", "noWithVeto": "0" }, "submitTime": "2022-03-28T11:50:20.819676256Z", "depositEndTime": "2022-03-30T11:50:20.819676256Z", "totalDeposit": [ { "denom": "pol", "amount": "10000000010" } ], "votingStartTime": "2022-03-28T14:25:26.644857113Z", "votingEndTime": "2022-03-30T14:25:26.644857113Z" }, { "proposalId": "2", "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", "finalTallyResult": { "yes": "0", "abstain": "0", "no": "0", "noWithVeto": "0" }, "submitTime": "2022-03-28T14:02:41.165025015Z", "depositEndTime": "2022-03-30T14:02:41.165025015Z", "totalDeposit": [ { "denom": "pol", "amount": "10" } ], "votingStartTime": "0001-01-01T00:00:00Z", "votingEndTime": "0001-01-01T00:00:00Z" } ], "pagination": { "total": "2" } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Proposals ``` Example: ```bash theme={null} grpcurl -plaintext \ localhost:9090 \ cosmos.gov.v1.Query/Proposals ``` Example Output: ```bash theme={null} { "proposals": [ { "id": "1", "messages": [ {"@type":"/cosmos.bank.v1beta1.MsgSend","amount":[{"denom":"pol","amount":"10"}],"fromAddress":"0x...","toAddress":"0x..."} ], "status": "PROPOSAL_STATUS_VOTING_PERIOD", "finalTallyResult": { "yesCount": "0", "abstainCount": "0", "noCount": "0", "noWithVetoCount": "0" }, "submitTime": "2022-03-28T11:50:20.819676256Z", "depositEndTime": "2022-03-30T11:50:20.819676256Z", "totalDeposit": [ { "denom": "pol", "amount": "10000000010" } ], "votingStartTime": "2022-03-28T14:25:26.644857113Z", "votingEndTime": "2022-03-30T14:25:26.644857113Z", "metadata": "AQ==", "title": "Proposal Title", "summary": "Proposal Summary" }, { "id": "2", "messages": [ {"@type":"/cosmos.bank.v1beta1.MsgSend","amount":[{"denom":"pol","amount":"10"}],"fromAddress":"0x...","toAddress":"0x..."} ], "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", "finalTallyResult": { "yesCount": "0", "abstainCount": "0", "noCount": "0", "noWithVetoCount": "0" }, "submitTime": "2022-03-28T14:02:41.165025015Z", "depositEndTime": "2022-03-30T14:02:41.165025015Z", "totalDeposit": [ { "denom": "pol", "amount": "10" } ], "metadata": "AQ==", "title": "Proposal Title", "summary": "Proposal Summary" } ], "pagination": { "total": "2" } } ``` #### Vote The `Vote` endpoint allows users to query a vote for a given proposal. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Vote ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1","voter":"0x..."}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/Vote ``` Example Output: ```bash theme={null} { "vote": { "proposalId": "1", "voter": "0x...", "option": "VOTE_OPTION_YES", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1000000000000000000" } ] } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Vote ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1","voter":"0x..."}' \ localhost:9090 \ cosmos.gov.v1.Query/Vote ``` Example Output: ```bash theme={null} { "vote": { "proposalId": "1", "voter": "0x...", "option": "VOTE_OPTION_YES", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1.000000000000000000" } ] } } ``` #### Votes The `Votes` endpoint allows users to query all votes for a given proposal. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Votes ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/Votes ``` Example Output: ```bash theme={null} { "votes": [ { "proposalId": "1", "voter": "0x...", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1000000000000000000" } ] } ], "pagination": { "total": "1" } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Votes ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1.Query/Votes ``` Example Output: ```bash theme={null} { "votes": [ { "proposalId": "1", "voter": "0x...", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1.000000000000000000" } ] } ], "pagination": { "total": "1" } } ``` #### Params The `Params` endpoint allows users to query all parameters for the `gov` module. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Params ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"params_type":"voting"}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/Params ``` Example Output: ```bash theme={null} { "votingParams": { "votingPeriod": "86400s" }, "depositParams": { "maxDepositPeriod": "0s" }, "tallyParams": { "quorum": "MA==", "threshold": "MA==", "vetoThreshold": "MA==" } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Params ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"params_type":"voting"}' \ localhost:9090 \ cosmos.gov.v1.Query/Params ``` Example Output: ```bash theme={null} { "votingParams": { "votingPeriod": "86400s" } } ``` #### Deposit The `Deposit` endpoint allows users to query a deposit for a given proposal from a given depositor. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Deposit ``` Example: ```bash theme={null} grpcurl -plaintext \ '{"proposal_id":"1","depositor":"0x..."}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/Deposit ``` Example Output: ```bash theme={null} { "deposit": { "proposalId": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Deposit ``` Example: ```bash theme={null} grpcurl -plaintext \ '{"proposal_id":"1","depositor":"0x..."}' \ localhost:9090 \ cosmos.gov.v1.Query/Deposit ``` Example Output: ```bash theme={null} { "deposit": { "proposalId": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } } ``` #### deposits The `Deposits` endpoint allows users to query all deposits for a given proposal. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/Deposits ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/Deposits ``` Example Output: ```bash theme={null} { "deposits": [ { "proposalId": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } ], "pagination": { "total": "1" } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/Deposits ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1.Query/Deposits ``` Example Output: ```bash theme={null} { "deposits": [ { "proposalId": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } ], "pagination": { "total": "1" } } ``` #### TallyResult The `TallyResult` endpoint allows users to query the tally of a given proposal. Using legacy v1beta1: ```bash theme={null} cosmos.gov.v1beta1.Query/TallyResult ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1beta1.Query/TallyResult ``` Example Output: ```bash theme={null} { "tally": { "yes": "1000000", "abstain": "0", "no": "0", "noWithVeto": "0" } } ``` Using v1: ```bash theme={null} cosmos.gov.v1.Query/TallyResult ``` Example: ```bash theme={null} grpcurl -plaintext \ -d '{"proposal_id":"1"}' \ localhost:9090 \ cosmos.gov.v1.Query/TallyResult ``` Example Output: ```bash theme={null} { "tally": { "yes": "1000000", "abstain": "0", "no": "0", "noWithVeto": "0" } } ``` ### REST A user can query the `gov` module using REST endpoints. #### proposal The `proposals` endpoint allows users to query a given proposal. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals/{proposal_id} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals/1 ``` Example Output: ```bash theme={null} { "proposal": { "proposal_id": "1", "content": null, "status": "PROPOSAL_STATUS_VOTING_PERIOD", "final_tally_result": { "yes": "0", "abstain": "0", "no": "0", "no_with_veto": "0" }, "submit_time": "2022-03-28T11:50:20.819676256Z", "deposit_end_time": "2022-03-30T11:50:20.819676256Z", "total_deposit": [ { "denom": "pol", "amount": "10000000010" } ], "voting_start_time": "2022-03-28T14:25:26.644857113Z", "voting_end_time": "2022-03-30T14:25:26.644857113Z" } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals/{proposal_id} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals/1 ``` Example Output: ```bash theme={null} { "proposal": { "id": "1", "messages": [ { "@type": "/cosmos.bank.v1beta1.MsgSend", "from_address": "0x...", "to_address": "0x...", "amount": [ { "denom": "pol", "amount": "10" } ] } ], "status": "PROPOSAL_STATUS_VOTING_PERIOD", "final_tally_result": { "yes_count": "0", "abstain_count": "0", "no_count": "0", "no_with_veto_count": "0" }, "submit_time": "2022-03-28T11:50:20.819676256Z", "deposit_end_time": "2022-03-30T11:50:20.819676256Z", "total_deposit": [ { "denom": "pol", "amount": "10000000" } ], "voting_start_time": "2022-03-28T14:25:26.644857113Z", "voting_end_time": "2022-03-30T14:25:26.644857113Z", "metadata": "AQ==", "title": "Proposal Title", "summary": "Proposal Summary" } } ``` #### proposals The `proposals` endpoint also allows users to query all proposals with optional filters. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals ``` Example Output: ```bash theme={null} { "proposals": [ { "proposal_id": "1", "content": null, "status": "PROPOSAL_STATUS_VOTING_PERIOD", "final_tally_result": { "yes": "0", "abstain": "0", "no": "0", "no_with_veto": "0" }, "submit_time": "2022-03-28T11:50:20.819676256Z", "deposit_end_time": "2022-03-30T11:50:20.819676256Z", "total_deposit": [ { "denom": "pol", "amount": "10000000" } ], "voting_start_time": "2022-03-28T14:25:26.644857113Z", "voting_end_time": "2022-03-30T14:25:26.644857113Z" }, { "proposal_id": "2", "content": null, "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", "final_tally_result": { "yes": "0", "abstain": "0", "no": "0", "no_with_veto": "0" }, "submit_time": "2022-03-28T14:02:41.165025015Z", "deposit_end_time": "2022-03-30T14:02:41.165025015Z", "total_deposit": [ { "denom": "pol", "amount": "10" } ], "voting_start_time": "0001-01-01T00:00:00Z", "voting_end_time": "0001-01-01T00:00:00Z" } ], "pagination": { "next_key": null, "total": "2" } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals ``` Example Output: ```bash theme={null} { "proposals": [ { "id": "1", "messages": [ { "@type": "/cosmos.bank.v1beta1.MsgSend", "from_address": "0x...", "to_address": "0x...", "amount": [ { "denom": "pol", "amount": "10" } ] } ], "status": "PROPOSAL_STATUS_VOTING_PERIOD", "final_tally_result": { "yes_count": "0", "abstain_count": "0", "no_count": "0", "no_with_veto_count": "0" }, "submit_time": "2022-03-28T11:50:20.819676256Z", "deposit_end_time": "2022-03-30T11:50:20.819676256Z", "total_deposit": [ { "denom": "pol", "amount": "10000000010" } ], "voting_start_time": "2022-03-28T14:25:26.644857113Z", "voting_end_time": "2022-03-30T14:25:26.644857113Z", "metadata": "AQ==", "title": "Proposal Title", "summary": "Proposal Summary" }, { "id": "2", "messages": [ { "@type": "/cosmos.bank.v1beta1.MsgSend", "from_address": "0x...", "to_address": "0x...", "amount": [ { "denom": "pol", "amount": "10" } ] } ], "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", "final_tally_result": { "yes_count": "0", "abstain_count": "0", "no_count": "0", "no_with_veto_count": "0" }, "submit_time": "2022-03-28T14:02:41.165025015Z", "deposit_end_time": "2022-03-30T14:02:41.165025015Z", "total_deposit": [ { "denom": "pol", "amount": "10" } ], "voting_start_time": null, "voting_end_time": null, "metadata": "AQ==", "title": "Proposal Title", "summary": "Proposal Summary" } ], "pagination": { "next_key": null, "total": "2" } } ``` #### voter vote The `votes` endpoint allows users to query a vote for a given proposal. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals/{proposal_id}/votes/{voter} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals/1/votes/0x... ``` Example Output: ```bash theme={null} { "vote": { "proposal_id": "1", "voter": "0x...", "option": "VOTE_OPTION_YES", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1.000000000000000000" } ] } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals/{proposal_id}/votes/{voter} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals/1/votes/0x... ``` Example Output: ```bash theme={null} { "vote": { "proposal_id": "1", "voter": "0x...", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1.000000000000000000" } ], "metadata": "" } } ``` #### votes The `votes` endpoint allows users to query all votes for a given proposal. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals/{proposal_id}/votes ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals/1/votes ``` Example Output: ```bash theme={null} { "votes": [ { "proposal_id": "1", "voter": "0x...", "option": "VOTE_OPTION_YES", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1.000000000000000000" } ] } ], "pagination": { "next_key": null, "total": "1" } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals/{proposal_id}/votes ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals/1/votes ``` Example Output: ```bash theme={null} { "votes": [ { "proposal_id": "1", "voter": "0x...", "options": [ { "option": "VOTE_OPTION_YES", "weight": "1.000000000000000000" } ], "metadata": "" } ], "pagination": { "next_key": null, "total": "1" } } ``` #### params The `params` endpoint allows users to query all parameters for the `gov` module. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/params/{params_type} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/params/voting ``` Example Output: ```bash theme={null} { "voting_params": { "voting_period": "86400s" }, "deposit_params": { "min_deposit": [ ], "max_deposit_period": "0s" }, "tally_params": { "quorum": "0.000000000000000000", "threshold": "0.000000000000000000", "veto_threshold": "0.000000000000000000" } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/params/{params_type} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/params/voting ``` Example Output: ```bash theme={null} { "voting_params": { "voting_period": "86400s" }, "deposit_params": { "min_deposit": [ ], "max_deposit_period": "0s" }, "tally_params": { "quorum": "0.000000000000000000", "threshold": "0.000000000000000000", "veto_threshold": "0.000000000000000000" } } ``` #### deposits The `deposits` endpoint allows users to query a deposit for a given proposal from a given depositor. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals/{proposal_id}/deposits/{depositor} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals/1/deposits/0x... ``` Example Output: ```bash theme={null} { "deposit": { "proposal_id": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals/{proposal_id}/deposits/{depositor} ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals/1/deposits/0x... ``` Example Output: ```bash theme={null} { "deposit": { "proposal_id": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } } ``` #### proposal deposits The `deposits` endpoint allows users to query all deposits for a given proposal. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals/{proposal_id}/deposits ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals/1/deposits ``` Example Output: ```bash theme={null} { "deposits": [ { "proposal_id": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } ], "pagination": { "next_key": null, "total": "1" } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals/{proposal_id}/deposits ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals/1/deposits ``` Example Output: ```bash theme={null} { "deposits": [ { "proposal_id": "1", "depositor": "0x...", "amount": [ { "denom": "pol", "amount": "10000000" } ] } ], "pagination": { "next_key": null, "total": "1" } } ``` #### tally The `tally` endpoint allows users to query the tally of a given proposal. Using legacy v1beta1: ```bash theme={null} /cosmos/gov/v1beta1/proposals/{proposal_id}/tally ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1beta1/proposals/1/tally ``` Example Output: ```bash theme={null} { "tally": { "yes": "1000000", "abstain": "0", "no": "0", "no_with_veto": "0" } } ``` Using v1: ```bash theme={null} /cosmos/gov/v1/proposals/{proposal_id}/tally ``` Example: ```bash theme={null} curl localhost:1317/cosmos/gov/v1/proposals/1/tally ``` Example Output: ```bash theme={null} { "tally": { "yes": "1000000", "abstain": "0", "no": "0", "no_with_veto": "0" } } ``` ## Metadata The gov module has two locations for metadata where users can provide further context about the onchain actions they are taking. By default all metadata fields have a 255 character length field where metadata can be stored in json format, either onchain or off-chain depending on the amount of data required. Here we provide a recommendation for the json structure and where the data should be stored. There are two important factors in making these recommendations. First, that the gov and group modules are consistent with one another, note the number of proposals made by all groups may be quite large. Second, that client applications such as block explorers and governance interfaces have confidence in the consistency of metadata structure accross chains. ### Proposal Location: off-chain as json object stored on IPFS ```json theme={null} { "title": "", "authors": [""], "summary": "", "details": "", "proposal_forum_url": "", "vote_option_context": "", } ``` :::note The `authors` field is an array of strings, this is to allow for multiple authors to be listed in the metadata. In v0.46, the `authors` field is a comma-separated string. Frontends are encouraged to support both formats for backwards compatibility. ::: ### Vote Location: onchain as json within 255 character limit ```json theme={null} { "justification": "", } ``` ## Future Improvements These improvements are being discussed in the upstream cosmos-sdk, hence not really part of any heimdall future plan. The current documentation only describes the minimum viable product for the governance module. Future improvements may include: * **`BountyProposals`:** If accepted, a `BountyProposal` creates an open bounty. The `BountyProposal` specifies how many tokens will be given upon completion. These POL will be taken from the `reserve pool`. After a `BountyProposal` is accepted by governance, anybody can submit a `SoftwareUpgradeProposal` with the code to claim the bounty. Note that once a `BountyProposal` is accepted, the corresponding funds in the `reserve pool` are locked so that payment can always be honored. In order to link a `SoftwareUpgradeProposal` to an open bounty, the submitter of the `SoftwareUpgradeProposal` will use the `Proposal.LinkedProposal` attribute. If a `SoftwareUpgradeProposal` linked to an open bounty is accepted by governance, the funds that were reserved are automatically transferred to the submitter. * **Complex delegation:** Delegators could choose other representatives than their validators. Ultimately, the chain of representatives would always end up to a validator, but delegators could inherit the vote of their chosen representative before they inherit the vote of their validator. In other words, they would only inherit the vote of their validator if their other appointed representative did not vote. * **Better process for proposal review:** There would be two parts to `proposal.Deposit`, one for anti-spam (same as in MVP) and an other one to reward third party auditors. # Heimdall Bor module Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/heimdall-bor-module How the Heimdall bor module manages span intervals, selects block producers for the Bor chain, and coordinates span proposals through the ABCI++ side-transaction flow. Heimdall's `bor` module is responsible for managing span intervals and coordinating interactions with the Bor chain. Specifically, it determines when a new span is created and which producer it assigns, based on the current block number and the current span. Since the Rio hardfork, each span has exactly one selected producer, and spans are created in-consensus during Heimdall's block processing (`AddNewVeBlopSpan`) rather than through the `MsgProposeSpan` side-transaction flow described below. The `MsgProposeSpan` path is pre-Rio: its side-handler now votes `NO` for any span at or after the Rio height, so it no longer produces spans in production. The data structures and query commands on this page remain current; the proposal/voting mechanics describe the legacy flow. ## Preliminary terminology * A `side-transaction` is a normal heimdall transaction but the data with which the message is composed needs to be voted on by the validators since the data is obscure to the consensus protocol itself, and it has no way of validating the data's correctness. * A `sprint` comprises of 16 bor blocks (configured in [bor](https://github.com/0xPolygon/launch/blob/fe86ba6cd16e5c36067a5ae49c0bad62ce8b1c3f/mainnet-v1/sentry/validator/bor/genesis.json#L26C18-L28)). * A `span` comprises 400 sprints in bor (check heimdall's bor [params](https://heimdall-api.polygon.technology/bor/params) endpoint). ## Overview The validators on the bor chain produce blocks within spans. Hence, it is imperative for the protocol to formalize the producer for a range of blocks (`span`). The `bor` module in heimdall facilitates this by selecting the producer for each span from the producer pool, which is determined on-chain from stake-weighted validator producer votes. The bor chain fetches and persists this information before the next span begins. The `bor` module is a crucial component in heimdall since the PoS chain "liveness" depends on it. ## How it works A `Span` is defined by the data structure: ```protobuf theme={null} message Span { uint64 id = 1 [ (amino.dont_omitempty) = true ]; uint64 start_block = 2 [ (amino.dont_omitempty) = true ]; uint64 end_block = 3 [ (amino.dont_omitempty) = true ]; heimdallv2.stake.ValidatorSet validator_set = 4 [ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ]; repeated heimdallv2.stake.Validator selected_producers = 5 [ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ]; string bor_chain_id = 6 [ (amino.dont_omitempty) = true ]; } ``` where * `id` means the id of the span, calculated by monotonically incrementing the id of the previous span. * `start_block` corresponds to the block in bor from which the given span would begin. * `end_block` corresponds to the block in bor at which the given span would conclude. * `validator_set` defines the set of active validators. * `selected_producers` are the validators selected to produce blocks in bor from the validator set. * `bor_chain_id` corresponds to bor chain ID. A validator on heimdall can construct a span proposal message: ```protobuf theme={null} message MsgProposeSpan { option (amino.name) = "heimdallv2/bor/MsgProposeSpan"; option (cosmos.msg.v1.signer) = "proposer"; uint64 span_id = 1; string proposer = 2 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 start_block = 3; uint64 end_block = 4; string chain_id = 5; bytes seed = 6; string seed_author = 7 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; } ``` The message is generally constructed and broadcast by the validator's bridge process periodically, but the CLI can also be used to do the same manually (see [below](#proposing-a-span-manually)). Upon broadcasting the message, it is initially checked by `ProposeSpan` handler for basic sanity (verify whether the proposed span is in continuity, appropriate span duration, correct chain ID, etc.). Since this is a side-transaction, the validators then vote on the data present in `MsgProposeSpan` on the basis of its correctness. All these checks are done in `SideHandleMsgSpan` (verifying `seed`, span continuity, etc.) and if correct, the validator would vote `YES`. Finally, if there are 2/3+ `YES` votes, the `PostHandleMsgSpan` persists the proposed span in the state via the keeper : ```go theme={null} // freeze for new span err = s.k.FreezeSet(ctx, msg.SpanId, msg.StartBlock, msg.EndBlock, msg.ChainId, common.Hash(msg.Seed)) if err != nil { logger.Error("unable to freeze validator set for span", "span id", msg.SpanId, "error", err) return err } ``` `FreezeSet` internally invokes `SelectNextProducers`, which pseudo-randomly picks producers from the validator set, leaning more towards validators with higher voting power based on stake: ```go theme={null} // select next producers newProducers, err := k.SelectNextProducers(ctx, seed, prevVals) if err != nil { return err } ``` and then initializes and stores the span: ```go theme={null} // generate new span newSpan := &types.Span{ Id: id, StartBlock: startBlock, EndBlock: endBlock, ValidatorSet: valSet, SelectedProducers: newProducers, BorChainId: borChainID, } logger.Info("Freezing new span", "id", id, "span", newSpan) return k.AddNewSpan(ctx, newSpan) ``` ### Proposing a span manually A validator can use the CLI to propose a span: ```bash theme={null} heimdalld tx bor propose-span --proposer --start-block --span-id --bor-chain-id ``` ## Query commands One can run the following query commands from the bor module: * `span` - Query the span corresponding to the given span id. * `span-list` - Fetch span list. * `latest-span` - Query the latest span. * `next-span-seed` - Query the seed for the next span. * `next-span` - Query the next span. * `params` - Fetch the parameters associated with the bor module. ### CLI commands ```bash theme={null} heimdalld query bor span-by-id ``` ```bash theme={null} heimdalld query bor span-list ``` ```bash theme={null} heimdalld query bor latest-span ``` ```bash theme={null} heimdalld query bor next-span-seed [id] ``` ```bash theme={null} heimdalld query bor next-span ``` ```bash theme={null} heimdalld query bor params ``` ### GRPC Endpoints The endpoints and the params are defined in the [bor/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/bor/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.bor.Query/GetSpanList ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.bor.Query/GetLatestSpan ``` ```bash theme={null} grpcurl -plaintext -d '{"id": <>}' localhost:9090 heimdallv2.bor.Query/GetNextSpanSeed ``` ```bash theme={null} grpcurl -plaintext -d '{"span_id": <>, "start_block": <>, "bor_chain_id": "<>"}' localhost:9090 heimdallv2.bor.Query/GetNextSpan ``` ```bash theme={null} grpcurl -plaintext -d '{"id": "<>"}' localhost:9090 heimdallv2.bor.Query/GetSpanById ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.bor.Query/GetBorParams ``` ### REST endpoints The endpoints and the params are defined in the [bor/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/bor/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} curl localhost:1317/bor/spans/list ``` ```bash theme={null} curl localhost:1317/bor/spans/latest ``` ```bash theme={null} curl localhost:1317/bor/spans/seed/ ``` ```bash theme={null} curl "localhost:1317/bor/spans/prepare?span_id=&start_block=&bor_chain_id=" ``` ```bash theme={null} curl localhost:1317/bor/spans/ ``` ```bash theme={null} curl localhost:1317/bor/params ``` # Introduction Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/introduction What Heimdall-v2 is, what it does in the Polygon Chain architecture, and how it differs from the original Heimdall. Heimdall-v2 is the consensus client at the core of the Polygon Chain network. It is a complete rewrite of the original Heimdall, built on a fork of [Cosmos SDK](https://github.com/0xPolygon/cosmos-sdk) (`v0.50.13`) and a fork of [CometBFT](https://github.com/0xPolygon/cometbft/) (`v0.38.17`). Heimdall-v2 performs the following functions: * Manages validators and their stake. * Handles block producer selection for the Bor layer. * Facilitates span management. * Orchestrates the state sync mechanism between Ethereum and Polygon Chain. * Handles checkpoints: periodic Merkle root submissions of Bor block data to Ethereum. * Handles milestones: fast deterministic finality within 2 to 5 seconds, using vote extensions. ## Modules Heimdall-v2 uses modified versions of some Cosmos SDK modules and a set of fully custom modules: * **Modified Cosmos SDK modules**: `auth`, `bank`, `gov` * **Custom modules**: `bor`, `chainmanager`, `checkpoint`, `clerk`, `milestone`, `stake`, `topup` ## GitHub repository For setup instructions and configuration details, see the [Heimdall-v2 README](https://github.com/0xPolygon/heimdall-v2/blob/develop/README.md). # Milestones Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/milestones How Heimdall milestones provide deterministic finality on Polygon Chain without waiting for checkpoints, using CometBFT vote extensions and 2/3 majority agreement. ## Overview This module enables deterministic finality using Polygon Chain’s dual client architecture.\ This is done using a hybrid system that uses CometBFT layer consensus,\ along with an additional fork choice rule within the execution layer. With the introduction of milestones, finality is deterministic even before a checkpoint is submitted to L1.\ After a certain number of blocks (minimum 1), a milestone is proposed and voted by Heimdall.\ Once 2/3+ of the network agrees, the milestone is finalized, and all transactions up to that milestone are considered final, with no chance of reorganization. ## Flow Milestones are a lightweight alternative to checkpoints in Heimdall, used to finalize blocks more efficiently.\ With the introduction of milestones, finality is deterministic even before a checkpoint is submitted to L1.\ Unlike the original transaction-based design in Heimdall-v1, the current design operates without transactions, relying entirely on ABCI++ flow and validator vote extensions.\ Each validator proposes a milestone independently. Milestones are proposed as a series of recent, up to 10 block hashes, and a majority (2/3 of voting power) agreement on a consecutive sequence of these hashes is required to finalize a milestone.\ The system tolerates duplication to increase reliability and includes logic for resolving forks and ensuring milestone continuity. ### Milestone Proposals and Duplication Validators independently propose a sequence of block hashes, at most `MaxMilestonePropositionLength` starting from the last finalized milestone: ```protobuf theme={null} message MilestoneProposition { option (gogoproto.equal) = true; option (gogoproto.goproto_getters) = true; repeated bytes block_hashes = 1 [ (amino.dont_omitempty) = true ]; uint64 start_block_number = 2 [ (amino.dont_omitempty) = true ]; } ``` Proposals are handled in the `ExtendVoteHandler`, executed at each block.\ A milestone proposed in block `N` is finalized in block `N+1` (or later), introducing acceptable duplication of proposed milestones to improve reliability.\ This duplication ensures that even if a milestone isn’t finalized in `N+1`, it may succeed in `N+2` or later.\ In cases of failed milestone propositions, the node still participates in the consensus. ### Proposed Milestone validation checks Proposed milestone validation is performed in N block with `ValidateMilestoneProposition` function in`ExtendVoteHandler` and in `VerifyVoteExtensionHandler`: * Length is validated: the milestone proposal, if created should not contain more block hashes than `MaxMilestonePropositionLength`; * Each block hash length is validated to be of the appropriate length. ### Majority Determination in the following block Vote extensions from other validators are collected and unmarshalled.\ Duplicate vote extensions from the same validator are ignored.\ The algorithm uses data structures keyed by `(block_number, block_hash)` to handle forks (same block number may have different hashes), so that fork-resilience is achieved by: * Separating vote data by hash and block number. * Ensuring the finalized milestones continue from the last one with no gaps. The core algorithm looks for: * The longest consecutive sequence of block hashes. * Supported by >= 2/3 of the total voting power. ### Milestones Validation and Finalization Once a consensus over the milestone is reached, the majority milestone is validated with `ValidateMilestoneProposition` checks again for integrity in `PreBlocker`.\ If the validation passes, the milestone is persisted. ## Messages ### Milestone `Milestone` defines a message for submitting a milestone ```protobuf theme={null} message Milestone { option (gogoproto.equal) = true; option (gogoproto.goproto_getters) = true; string proposer = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 start_block = 2 [ (amino.dont_omitempty) = true ]; uint64 end_block = 3 [ (amino.dont_omitempty) = true ]; bytes hash = 4 [ (amino.dont_omitempty) = true ]; string bor_chain_id = 5 [ (amino.dont_omitempty) = true ]; string milestone_id = 6 [ (amino.dont_omitempty) = true ]; uint64 timestamp = 7 [ (amino.dont_omitempty) = true ]; } ``` ## Interact with the Node ### Tx Commands #### Send Milestone Transaction ```bash theme={null} heimdalld tx milestone milestone [proposer] [startBlock] [endBlock] [hash] [borChainId] [milestoneId] ``` ### CLI Query Commands One can run the following query commands from the milestone module: * `get-params` - Get milestone params * `get-count` - Get milestone count * `get-latest-milestone` - Get latest milestone * `get-milestone-by-number` - Get the milestone by number * `get-milestone-proposer` - Get the milestone proposer * `get-latest-no-ack-milestone` - Get the latest no ack milestone * `get-no-ack-milestone-by-id` - Get the no ack milestone by id ```bash theme={null} heimdalld query milestone get-params ``` ```bash theme={null} heimdalld query milestone get-count ``` ```bash theme={null} heimdalld query milestone get-latest-milestone ``` ```bash theme={null} heimdalld query milestone get-milestone-by-number ``` ```bash theme={null} heimdalld query milestone get-milestone-proposer ``` ```bash theme={null} heimdalld query milestone get-latest-no-ack-milestone ``` ```bash theme={null} heimdalld query milestone get-no-ack-milestone-by-id ``` ### GRPC Endpoints The endpoints and the params are defined in the [milestone/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/milestone/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.milestone.Query/GetMilestoneParams ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.milestone.Query/GetMilestoneCount ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.milestone.Query/GetLatestMilestone ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.milestone.Query/GetMilestoneByNumber ``` ### REST APIs The endpoints and the params are defined in the [milestone/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/milestone/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} curl localhost:1317/milestones/params ``` ```bash theme={null} curl localhost:1317/milestones/count ``` ```bash theme={null} curl localhost:1317/milestones/latest ``` ```bash theme={null} curl localhost:1317/milestones/{number} ``` # Staking Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/staking How the Heimdall stake module manages validator state, synchronizes Ethereum stake events to Heimdall, and updates the validator set. ## Overview This module manages the validators' related transactions and state for Heimdall.\ Validators stake their tokens on the Ethereum chain and send the transactions on Heimdall using the necessary parameters to acknowledge the Ethereum stake change.\ Once the majority of the validators agree on the change on the stake, this module saves the validator information on Heimdall state. Stake Flow ## Flow The x/stake module manages validator-related transactions and validator set management for Heimdall. Validators stake their tokens on the Ethereum chain to participate in consensus. To synchronize these changes with Heimdall, the bridge processor broadcasts the corresponding transaction for an Ethereum-emitted event, choosing from one of the following messages each with the necessary parameters: * `MsgValidatorJoin`: This message is triggered when a new validator joins the system by interacting with `StakingManager.sol` on Ethereum. The action emits a `Staked` event to recognize and process the validator’s participation. * `MsgStakeUpdate`: Used to handle stake modifications, this message is sent when a validator re-stakes or receives additional delegation. Both scenarios trigger a `StakeUpdate` event on Ethereum, ensuring Heimdall accurately updates the validator’s stake information. * `MsgValidatorExit`: When a validator decides to exit, they initiate the process on Ethereum, leading to the emission of a `UnstakeInit` event. This message ensures that Heimdall records the validator’s departure accordingly. * `MsgSignerUpdate`: This message is responsible for processing changes to a validator’s signer key. When a validator updates their signer key on Ethereum, it emits a `SignerUpdate` event, prompting Heimdall to reflect the new signer key in its records. Each of these transactions in Heimdall follows the same processing mechanisms, leveraging ABCI++ phases. During the `ExtendVote` phase, side transaction handlers are triggered, and a vote is injected as a vote extension after validating the Ethereum-emitted event and ensuring its alignment with the data in the processed message. Once more than two-thirds of the validators (by voting power) confirm that the action described in the message has occurred on Ethereum, the x/stake module updates the validator’s state in Heimdall during the `FinalizeBlock`’s `PreBlocker` execution. ### Replay Prevention Mechanism Heimdall employs a replay prevention mechanism in the post-tx handler functions to ensure that validator update messages derived from Ethereum events are not processed multiple times. This mechanism prevents replay attacks by assigning a unique sequence number to each transaction and verifying whether it has already been processed.\ The sequence number is constructed using the Ethereum block number and log index, following the formula: * `sequence = (block number × DefaultLogIndexUnit) + log index` where: * `msg.BlockNumber` represents the Ethereum block where the event was emitted. * `msg.LogIndex` is the position of the log entry within that block. * `DefaultLogIndexUnit` ensures uniqueness when combining block numbers and log indexes. Before processing a transaction, Heimdall checks its stake keeper to determine if the sequence number has been recorded. If the sequence is found, the transaction is rejected as a duplicate. Once the post-tx handler completes successfully, the sequence is stored, ensuring that any future message with the same sequence is recognized and ignored. This approach guarantees that Heimdall only processes each valid Ethereum signer update once, preventing unintended state changes due to replayed messages. ### Updating the Validator Set In the x/stake `EndBlocker`, Heimdall updates the validator set (through the `ApplyAndReturnValidatorSetUpdates`function), ensuring consensus reflects the latest validator changes. Before any updates, the current block’s validator set is stored as the previous block’s set. The system retrieves all existing validators, the current validator set, and the acknowledgment count from the x/checkpoint state. Using `GetUpdatedValidators`, a list of validators that require updates (`setUpdates`) is identified and applied through `UpdateWithChangeSet`, storing the new set under `CurrentValidatorSetKey`. To maintain fair block proposer selection, Heimdall implements a proposer priority system, ensuring all validators have a fair chance to propose new blocks. The proposer priority is dynamically adjusted using `IncrementProposerPriority(times int)`, which prevents any validator from monopolizing block proposals. This function limits priority differences by re-scaling priorities (`RescalePriorities(diffMax)`) and shifting values based on the average proposer priority (`shiftByAvgProposerPriority()`). During each round, the validator with the highest priority is selected as the proposer, after which their priority is adjusted to prevent indefinite accumulation.\ These mechanisms collectively ensure efficient and fair validator rotation, maintaining a balanced consensus process while preventing priority overflows and unfair selection biases. Stake ABCI_Diagram.png ## Messages ### MsgValidatorJoin `MsgValidatorJoin` defines a message for a node to join the network as validator. Here is the structure for the transaction message: ```protobuf theme={null} // MsgValidatorJoin defines a message for a new validator to join the network message MsgValidatorJoin { option (cosmos.msg.v1.signer) = "from"; option (amino.name) = "heimdallv2/stake/MsgValidatorJoin"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = true; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 val_id = 2 [ (amino.dont_omitempty) = true ]; uint64 activation_epoch = 3 [ (amino.dont_omitempty) = true ]; string amount = 4 [ (gogoproto.nullable) = false, (gogoproto.customtype) = "cosmossdk.io/math.Int", (amino.dont_omitempty) = true ]; bytes signer_pub_key = 5 [ (amino.dont_omitempty) = true ]; bytes tx_hash = 6 [ (amino.dont_omitempty) = true ]; uint64 log_index = 7 [ (amino.dont_omitempty) = true ]; uint64 block_number = 8 [ (amino.dont_omitempty) = true ]; uint64 nonce = 9 [ (amino.dont_omitempty) = true ]; } ``` ### MsgStakeUpdate `MsgStakeUpdate` defines a message for a validator to perform a stake update on Ethereum network. ```protobuf theme={null} message MsgStakeUpdate { option (cosmos.msg.v1.signer) = "from"; option (amino.name) = "heimdallv2/stake/MsgStakeUpdate"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = true; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 val_id = 2 [ (amino.dont_omitempty) = true ]; string new_amount = 3 [ (gogoproto.nullable) = false, (amino.dont_omitempty) = true, (gogoproto.customtype) = "cosmossdk.io/math.Int" ]; bytes tx_hash = 4 [ (amino.dont_omitempty) = true ]; uint64 log_index = 5 [ (amino.dont_omitempty) = true ]; uint64 block_number = 6 [ (amino.dont_omitempty) = true ]; uint64 nonce = 7 [ (amino.dont_omitempty) = true ]; } ``` ### MsgSignerUpdate `MsgSignerUpdate` defines a message for updating the signer of the existing validator. ```protobuf theme={null} message MsgSignerUpdate { option (cosmos.msg.v1.signer) = "from"; option (amino.name) = "heimdallv2/stake/MsgSignerUpdate"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = true; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 val_id = 2 [ (amino.dont_omitempty) = true ]; bytes new_signer_pub_key = 3 [ (amino.dont_omitempty) = true ]; bytes tx_hash = 4 [ (amino.dont_omitempty) = true ]; uint64 log_index = 5 [ (amino.dont_omitempty) = true ]; uint64 block_number = 6 [ (amino.dont_omitempty) = true ]; uint64 nonce = 7 [ (amino.dont_omitempty) = true ]; } ``` ### MsgValidatorExit `MsgValidatorExit` defines a message for a validator to exit the network. ```protobuf theme={null} message MsgValidatorExit { option (cosmos.msg.v1.signer) = "from"; option (amino.name) = "heimdallv2/stake/MsgValidatorExit"; option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = true; string from = 1 [ (amino.dont_omitempty) = true, (cosmos_proto.scalar) = "cosmos.AddressString" ]; uint64 val_id = 2 [ (amino.dont_omitempty) = true ]; uint64 deactivation_epoch = 3 [ (amino.dont_omitempty) = true ]; bytes tx_hash = 4 [ (amino.dont_omitempty) = true ]; uint64 log_index = 5 [ (amino.dont_omitempty) = true ]; uint64 block_number = 6 [ (amino.dont_omitempty) = true ]; uint64 nonce = 7 [ (amino.dont_omitempty) = true ]; } ``` ## Interact with the Node ### Tx Commands #### Validator Join ```bash theme={null} heimdalld tx stake validator-join --proposer {proposer address} --signer-pubkey {signer pubkey with 04 prefix} --tx-hash {tx hash} --block-number {L1 block number} --staked-amount {total stake amount} --activation-epoch {activation epoch} --home="{path to home}" ``` #### Signer Update ```bash theme={null} heimdalld tx stake signer-update --proposer {proposer address} --id {val id} --new-pubkey {new pubkey with 04 prefix} --tx-hash {tx hash} --log-index {log index} --block-number {L1 block number} --nonce {nonce} --home="{path to home}" ``` #### Stake Update ```bash theme={null} heimdalld tx stake stake-update [valAddress] [valId] [amount] [txHash] [logIndex] [blockNumber] [nonce] ``` #### Validator Exit ```bash theme={null} heimdalld tx stake validator-exit [valAddress] [valId] [deactivationEpoch] [txHash] [logIndex] [blockNumber] [nonce] ``` ### CLI Query Commands One can run the following query commands from the stake module: * `current-validator-set` - Query all validators that are currently active in the validators' set * `signer` - Query validator info for given validator address * `validator` - Query validator info for a given validator id * `validator-status` - Query validator status for given validator address * `total-power` - Query total power of the validator set * `is-old-tx` - Check if a tx is old (already submitted) ```bash theme={null} heimdalld query stake current-validator-set ``` ```bash theme={null} heimdalld query stake signer [val_address] ``` ```bash theme={null} heimdalld query stake validator [id] ``` ```bash theme={null} heimdalld query stake validator-status [val_address] ``` ```bash theme={null} heimdalld query stake total-power ``` ```bash theme={null} heimdalld query stake is-old-tx [txHash] [logIndex] ``` ### GRPC Endpoints The endpoints and the params are defined in the [stake/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/stake/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.stake.Query/GetCurrentValidatorSet ``` ```bash theme={null} grpcurl -plaintext -d '{"val_address": <>}' localhost:9090 heimdallv2.stake.Query/GetSignerByAddress ``` ```bash theme={null} grpcurl -plaintext -d '{"id": <>}' localhost:9090 heimdallv2.stake.Query/GetValidatorById ``` ```bash theme={null} grpcurl -plaintext -d '{"val_address": <>}' localhost:9090 heimdallv2.stake.Query/GetValidatorStatusByAddress ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.stake.Query/GetTotalPower ``` ```bash theme={null} grpcurl -plaintext -d '{"tx_hash": <>, "log_index": <>}' localhost:9090 heimdallv2.stake.Query/IsStakeTxOld ``` ```bash theme={null} grpcurl -plaintest -d '{"times": <>}' localhost:9090 heimdallv2.stake.Query/GetProposersByTimes ``` ## REST APIs The endpoints and the params are defined in the [stake/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/stake/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} curl localhost:1317/stake/validators-set ``` ```bash theme={null} curl localhost:1317/stake/signer/{val_address} ``` ```bash theme={null} curl localhost:1317/stake/validator/{id} ``` ```bash theme={null} curl localhost:1317/stake/validator-status/{val_address} ``` ```bash theme={null} curl localhost:1317/stake/total-power ``` ```bash theme={null} curl localhost:1317/stake/is-old-tx?tx_hash=&log_index= ``` ```bash theme={null} curl localhost:1317/stake/proposers/{times} ``` # Topup Source: https://docs.polygon.technology/pos/architecture/heimdall_v2/topup How the Heimdall topup module manages validator fee balances on Heimdall, including funding methods, bridge processing, and fee withdrawal. ## Overview Heimdall Topup is an amount that will be used to pay fees on the heimdall chain. There are two ways to top up your account: 1. When new validator joins, they can mention a `topup` amount as top-up in addition to the staked amount, which will be moved as balance on the heimdall chain to pay fees on Heimdall. 2. A user can directly call the top-up function on the staking smart contract on Ethereum to increase top-up balance on Heimdall. ## Flow The Heimdall Top-up Mechanism facilitates the management of validator fees on the Heimdall chain by allowing deposits from the Ethereum (L1) root chain.\ This mechanism ensures validators have enough balances on Heimdall to cover operational fees.\ The system integrates the Ethereum staking contract with Heimdall's x/topup and x/checkpoint modules and a bridge component for cross-chain fee management. ### Top-Up Funding Methods There are two primary ways to fund a validator’s fee balance on Heimdall: * During Validator Initialization: When a new validator joins, they can specify a top-up amount in addition to the staked amount. This top-up is transferred to Heimdall as an initial balance. * Direct Top-Up: Any user can invoke the top-up function on the Ethereum staking smart contract to increase the validators’ top-up balance on Heimdall. ### Bridge Processing Top-up events on the Ethereum layer trigger automated processing through the bridge process: * The Root Chain Log Listener monitors for `StakinginfoTopUpFee` events. * the Top-up Fee Processor task, upon detecting such an event, triggers the `sendTopUpFeeToHeimdall` execution. The task: * Decodes the Ethereum log. * Verifies the event hasn’t already been processed. * Broadcasts a `MsgTopupTx` to the Heimdall chain. In addition to the automated broadcasting of `MsgTopupTx` transactions, it is also possible to manually craft and submit these transactions at the Heimdall layer.\ This fallback mechanism is used in scenarios where issues arise in bridging or processing Ethereum events. ### Heimdall x/topup Module Implementation Two core messages are defined in the x/topup module for fee management: * `MsgTopupTx`: Handles minting the top-up amount on Heimdall based on Ethereum events.\ Each top-up is uniquely identified by a sequence number built from `TxHash` and `LogIndex` to prevent duplicate processing. `MsgTopupTx` is a side-transaction, ensuring state changes only after the successful pre-commit majority of the votes are collected and final validation and post-tx handler execution in the following block height.\ When broadcasting the `MsgTopupTx` - sender (proposer of the topup) must sign it, and additional user address must be sent. For the top-up to be accepted, the `MsgTopupTx.Fee` must be at least equal to the `DefaultFeeWantedPerTx` amount. The top-up processing on Heimdall involves: * Minting the top-up number of pol tokens to the top-up module account. * Transferring the entire amount from the top-up module account to the user account. * Transferring the `DefaultFeeWantedPerTx` amount from the user account to the proposer validator account. The remaining top-up amount stays on the user account. * `MsgWithdrawFeeTx`: Allows validators to withdraw fees from Heimdall back to Ethereum. The withdrawal process involves: * Transferring the amount from the validator to the top-up module account. * Burning the amount from the top-up module account. * Updating the validator’s dividend “account” with the withdrawn amount. * No impact on the user account used during the `MsgTopupTx`. ## Messages ### MsgTopupTx `MsgTopupTx` is responsible for minting balance to an address on Heimdall based on Ethereum chain's `TopUpEvent` on staking manager contract. Handler for this transaction processes top-up and increases the balance only once for any given `msg.TxHash` and `msg.LogIndex`. It throws an error if trying to process the top-up more than once. Here is the structure for the top-up transaction message: ```protobuf theme={null} message MsgTopupTx { option (cosmos.msg.v1.signer) = "proposer"; option (amino.name) = "heimdallv2/topup/MsgTopupTx"; string proposer = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString", (amino.dont_omitempty) = true ]; string user = 2 [ (cosmos_proto.scalar) = "cosmos.AddressString", (amino.dont_omitempty) = true ]; string fee = 3 [ (gogoproto.customtype) = "cosmossdk.io/math.Int", (gogoproto.nullable) = false, (amino.dont_omitempty) = true ]; bytes tx_hash = 4 [ (amino.dont_omitempty) = true ]; uint64 log_index = 5 [ (amino.dont_omitempty) = true ]; uint64 block_number = 6 [ (amino.dont_omitempty) = true ]; } ``` ### MsgWithdrawFeeTx `MsgWithdrawFeeTx` is responsible for withdrawing balance from Heimdall to the Ethereum chain. A Validator can withdraw any amount from Heimdall. Handler processes the withdrawal by deducting the balance from the given validator and prepares the state to send the next checkpoint. The next possible checkpoint will contain the withdrawal-related state for the specific validator. Handler gets validator information based on `ValidatorAddress` and processes the withdrawal. ```protobuf theme={null} message MsgWithdrawFeeTx { option (cosmos.msg.v1.signer) = "proposer"; option (amino.name) = "heimdallv2/topup/MsgWithdrawFeeTx"; string proposer = 1 [ (amino.dont_omitempty) = true ]; string amount = 2 [ (gogoproto.customtype) = "cosmossdk.io/math.Int", (gogoproto.nullable) = false, (amino.dont_omitempty) = true ]; } ``` ## Interact with the Node ### Tx Commands #### Topup fee ```bash theme={null} heimdalld tx topup handle-topup-tx [proposer] [user] [fee] [tx_hash] [log_index] [block_number] ``` #### Withdraw fee ```bash theme={null} heimdalld tx topup withdraw-fee [proposer] [amount] ``` ### CLI Query Commands One can run the following query commands from the topup module: * `topup-sequence` - Query the sequence of a topup tx * `is-old-tx` - Check if a tx is old (already submitted) * `dividend-account` - Query a dividend account by its address * `dividend-account-root` - Query dividend account root hash * `account-proof` - Query account proof * `verify-account-proof` - Verify account proof ```bash theme={null} heimdalld query topup topup-sequence [tx_hash] [log_index] ``` ```bash theme={null} heimdalld query topup is-old-tx [tx_hash] [log_index] ``` ```bash theme={null} heimdalld query topup dividend-account [address] ``` ```bash theme={null} heimdalld query topup dividend-account-root ``` ```bash theme={null} heimdalld query topup account-proof [address] ``` ```bash theme={null} heimdalld query topup verify-account-proof [address] [proof] ``` ### GRPC Endpoints The endpoints and the params are defined in the [topup/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/topup/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} grpcurl -plaintext -d '{"tx_hash": <>, "log_index": <>}' localhost:9090 heimdallv2.topup.Query/IsTopupTxOld ``` ```bash theme={null} grpcurl -plaintext -d '{"tx_hash": <>, "log_index": <>}' localhost:9090 heimdallv2.topup.Query/GetTopupTxSequence ``` ```bash theme={null} grpcurl -plaintext -d '{"address": <>}' localhost:9090 heimdallv2.topup.Query/GetDividendAccountByAddress ``` ```bash theme={null} grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.topup.Query/GetDividendAccountRootHash ``` ```bash theme={null} grpcurl -plaintext -d '{"address": <>, "proof": <>}' localhost:9090 heimdallv2.topup.Query/VerifyAccountProofByAddress ``` ```bash theme={null} grpcurl -plaintext -d '{"address": <>}' localhost:9090 heimdallv2.topup.Query/GetAccountProofByAddress ``` ### REST APIs The endpoints and the params are defined in the [topup/query.proto](https://github.com/0xPolygon/heimdall-v2/blob/main/proto/heimdallv2/topup/query.proto) file. Please refer to them for more information about the optional params. ```bash theme={null} curl localhost:1317/topup/is-old-tx?tx_hash=&log_index= ``` ```bash theme={null} curl localhost:1317/topup/sequence?tx_hash=&log_index= ``` ```bash theme={null} curl localhost:1317/topup/dividend-account/{address} ``` ```bash theme={null} curl localhost:1317/topup/dividend-account-root ``` ```bash theme={null} curl localhost:1317/topup/account-proof/{address}/verify ``` ```bash theme={null} curl localhost:1317/topup/account-proof/{address} ``` # Overview Source: https://docs.polygon.technology/pos/architecture/overview Architectural overview of Polygon Chain from a node perspective, covering the Heimdall-v2 consensus layer and Bor execution layer. Polygon Chain is structured as a two-layer system: a consensus layer called Heimdall-v2 and an execution layer called Bor. Nodes on Polygon are designed around this two-layer split. Bor handles block production; Heimdall-v2 handles validation, checkpoint submission, and coordination with Ethereum. Figure: Ethereum, Bor and Heimdall architecture ## Three-layer view The network can be described as three layers: * **Ethereum layer**: a set of contracts on Ethereum mainnet. These include staking contracts, checkpoint storage, and the bridge. * **Heimdall-v2 layer**: a set of proof-of-stake nodes running in parallel to Ethereum mainnet. Heimdall-v2 monitors staking contracts on Ethereum, validates Bor block data, and submits checkpoints to Ethereum. Built on Cosmos SDK and CometBFT. * **Bor layer**: a set of block-producing nodes whose per-span producer is selected by Heimdall. Built on Go Ethereum. ## Staking contracts on Ethereum The PoS mechanism relies on staking management contracts deployed on Ethereum mainnet. These contracts: * Allow anyone to stake POL tokens and register as a validator. * Distribute staking rewards for validating state transitions on the network. * Record checkpoints submitted by Heimdall. The PoS mechanism also provides a partial mitigation for the data unavailability problem on Polygon sidechains, since checkpoint data is anchored to Ethereum. ## Heimdall-v2: validation layer Heimdall-v2 aggregates blocks produced by Bor into Merkle trees and publishes the Merkle root periodically to Ethereum. These periodic snapshots are called checkpoints. For every set of Bor blocks, a Heimdall-v2 validator: 1. Validates all blocks since the last checkpoint. 2. Creates a Merkle tree of the block hashes. 3. Publishes the Merkle root hash to Ethereum mainnet. Checkpoints serve two purposes: * Providing finality on Ethereum for cross-chain withdrawals. * Providing proof of burn for asset withdrawal to Ethereum. Block producer selection works as follows: * A single producer is selected from a small pool of eligible validators for each span. This producer creates and broadcasts blocks for that span. * A checkpoint includes the Merkle root hash of all blocks in a given interval. All nodes validate the hash and attach their signatures. * A proposer from the validator set collects all signatures and submits the checkpoint to Ethereum mainnet. * Producer pool eligibility is determined on-chain from validator producer votes, weighted by stake. Heimdall-v2 is a complete rewrite of the original Heimdall. It is based on Cosmos SDK and CometBFT. The migration is specified in: * [PIP-43: Replacing Tendermint with CometBFT](https://github.com/maticnetwork/Polygon-Improvement-Proposals/blob/cb371136414b5e198c44750cd4c30f7aad16043a/PIPs/PIP-43.md) * [PIP-44: Upgrade Cosmos-SDK](https://github.com/maticnetwork/Polygon-Improvement-Proposals/blob/cb371136414b5e198c44750cd4c30f7aad16043a/PIPs/PIP-44.md) * [PIP-62: Heimdall-v2 Migration](https://github.com/maticnetwork/Polygon-Improvement-Proposals/blob/cb371136414b5e198c44750cd4c30f7aad16043a/PIPs/PIP-62.md) Throughout these docs, "Heimdall" refers to Heimdall-v2 unless otherwise specified. ## Bor: block production layer Bor is Polygon Chain's block producer, responsible for aggregating transactions into blocks. Each span has a single block producer, selected by Heimdall from a small pool of eligible validators. See [Bor architecture](/pos/architecture/bor/introduction/) for detailed mechanics. # Finality Source: https://docs.polygon.technology/pos/concepts/finality/finality How finality works on Polygon Chain, including the milestone mechanism and the difference between probabilistic and deterministic finality. With the upgrade to Heimdall v2, deterministic finality is now achieved in 2 to 5 seconds, thanks to 1 to 2 second block times in Heimdall. Milestones are voted on and finalized much faster than the previous checkpoint-only model. ## Types of finality There are two main types of finality in blockchains: probabilistic and deterministic. **Probabilistic finality** means there is a chance of a reorganization where a different chain might become the canonical chain. Bitcoin is a well-known example. **Deterministic finality** means there is no chance of a reorganization once a block is finalized. Ethereum uses deterministic finality via its Casper FFG mechanism. Polygon Chain uses deterministic finality via its milestone mechanism. ## How finality works on Polygon Chain Polygon Chain achieves finality through two distinct mechanisms that serve different purposes: ### Milestones Milestones provide fast deterministic finality on the Polygon chain itself, without waiting for a checkpoint to be submitted to Ethereum. At every Heimdall height, each validator proposes the Bor block hashes they have seen since the last finalized milestone, using vote extensions in the CometBFT consensus. When finalizing Heimdall height H+1, Heimdall looks for the longest common sequence of block hashes from all validators that have 2/3 or more agreement. That sequence is finalized as the new milestone. This means finality is deterministic even before a checkpoint reaches Ethereum. Milestones typically finalize a transaction within 2 to 5 seconds. ### Checkpoints Checkpoints cover a contiguous range of Bor blocks (the length is a governance-set parameter, not a fixed cadence) and are submitted to Ethereum mainnet. They: * Provide proof of burn for asset withdrawals from Polygon to Ethereum. * Anchor Polygon state to Ethereum for additional security. Checkpoints are separate from milestones. A transaction is finalized on Polygon by a milestone well before the next checkpoint is submitted. ## Querying finality Use the standard `eth_getBlockByNumber` JSON-RPC method with the `"finalized"` block tag to retrieve the most recently finalized block on Polygon Chain. Finalized blocks are considered irreversible. ```json theme={null} { "method": "eth_getBlockByNumber", "params": ["finalized", true], "id": 1, "jsonrpc": "2.0" } ``` To check whether a specific transaction has reached finality, compare its block number to the latest finalized block number: ```ts theme={null} async function milestones_checkFinality(client: any, txHash: string): Promise { const tx = await client.getTransaction({ hash: `0x${txHash}` }) if (!tx || !tx.blockNumber) return false const latestBlock: Block = await client.getBlock({ blockTag: 'finalized' }) console.log(`Latest finalized block: ${latestBlock.number}`) console.log(`Your transaction block: ${tx.blockNumber}`) // Returns true if the finalized block has passed the transaction's block number. return (latestBlock.number !== null && latestBlock.number > tx.blockNumber) } ``` *Finality is achieved after a consensus period among validators, approximately 2 to 5 seconds.* # MATIC Source: https://docs.polygon.technology/pos/concepts/tokens/matic What MATIC was as the native token of Polygon Chain, and its relationship to the POL token that replaced it. Polygon network is transitioning from MATIC to POL, which will serve as the gas and staking token on Polygon Chain. Use the links below to learn more: * [Migrate from MATIC to POL](/pos/concepts/tokens/matic-to-pol) * [POL token specs](/pos/concepts/tokens/pol/) [MATIC](https://etherscan.io/token/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0) was the native cryptocurrency of Polygon Chain. It served the same role that ETH serves on Ethereum: users paid MATIC as gas fees for transactions and smart contract interactions, and validators staked MATIC to participate in consensus. MATIC has been superseded by POL through a 1:1 migration. Validator and staking rewards are now distributed in POL. For details on what action you may need to take depending on which chain holds your tokens, see the [MATIC to POL migration guide](/pos/concepts/tokens/matic-to-pol). ## Testing with MATIC on Amoy testnet Testnet MATIC is available on the Amoy testnet for testing purposes. Testnet tokens have no real-world value. To obtain testnet tokens, visit the [Polygon Faucet](https://faucet.polygon.technology/), select MATIC Token, choose the Polygon Chain (Amoy) network, and enter your account address. ## Example: sending MATIC This [example script](https://gist.github.com/rahuldamodar94/ea3bc4c551e6fc2d318767dcd7e5bffe) demonstrates sending MATIC tokens between accounts on the Polygon chain, showing how the token was used before the POL migration. # Migrate to POL Source: https://docs.polygon.technology/pos/concepts/tokens/matic-to-pol How to migrate MATIC tokens to POL, depending on which chain they are currently on. ## Overview The upgrade from MATIC to POL marks a significant change for the Polygon networks. POL is the native gas and staking token on Polygon Chain and supports the network's future expansion as an aggregated network of blockchains. The migration operates on a 1:1 basis. For every MATIC you migrate, you receive one POL. ## Migrate MATIC tokens on Ethereum MATIC stakers and delegators do not need to take any action to migrate from MATIC to POL. 1. Navigate to [Polygon Portal's migration interface](https://portal.polygon.technology/pol-upgrade). 2. Switch to the Ethereum network in your wallet and connect to the Portal UI. 3. Approve the migration action by granting the upgrade contract permission to access your MATIC tokens. 4. Confirm the migration transaction to receive POL in your wallet. ## Migrate MATIC tokens on Polygon Chain MATIC tokens stored on the Polygon Chain chain are automatically converted to POL at a 1:1 ratio. No manual migration is required. However, you may need to update the native token symbol in your wallet's network settings. If the symbol is not updated, your wallet may continue to display "MATIC" instead of "POL." To update the symbol in MetaMask: 1. Open MetaMask in expanded mode by selecting **Expand view** from the options menu in the top-right corner.
change-token-name-1
2. Open the options menu again and select **Settings** from the drop-down list. change-token-name-2 3. Select the **Networks** tab from the left sidebar and switch to **Polygon Mainnet**. The **Currency symbol** field shows the current value, which is **MATIC**. change-token-name-3 4. Change the **Currency symbol** to **POL** and select **Save**. You can ignore the yellow warning in this case.
change-token-name-4
The process to change the token symbol varies by wallet. Refer to your wallet's documentation for the specific steps. ## Migrate MATIC tokens on Polygon zkEVM If your MATIC tokens are on the zkEVM chain: 1. Use [Polygon Portal](https://portal.polygon.technology/bridge) to bridge your tokens to Ethereum. 2. Follow the steps in [Migrate MATIC tokens on Ethereum](#migrate-matic-tokens-on-ethereum). ## Further reading * [Detailed blog post on MATIC to POL migration](https://polygon.technology/blog/save-the-date-matic-pol-migration-coming-september-4th-everything-you-need-to-know) * [POL token reference](/pos/concepts/tokens/pol/) # POL Source: https://docs.polygon.technology/pos/concepts/tokens/pol What POL is, how it works as the native token of Polygon Chain, and how its emission and governance mechanisms are designed. POL is the native token of the Polygon ecosystem, replacing MATIC. It serves as the gas and staking token on Polygon Chain, and is designed to support the network's expansion as an aggregated network of blockchains. Like MATIC, POL is built on OpenZeppelin's ERC-20 implementation and supports [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) for signature-based permit approvals. ## Governance proposals behind POL Community-driven governance shaped POL's design and functionality. The relevant proposals are: 1. [PIP-17: Polygon Ecosystem Token (POL)](https://forum.polygon.technology/t/pip-17-polygon-ecosystem-token-pol/12912) 2. [PIP-18: Polygon 2.0 Phase 0 - Frontier](https://forum.polygon.technology/t/pip-18-polygon-2-0-phase-0-frontier/12913) 3. [PIP-19: Update Polygon Chain Native Token to POL](https://forum.polygon.technology/t/pip-19-update-polygon-pos-native-token-to-pol/12914) 4. [PIP-25: Adjust POL Total Supply](https://forum.polygon.technology/t/pip-25-adjust-pol-total-supply/13008) 5. [PIP-26: Transition from MATIC to POL Validator Rewards](https://forum.polygon.technology/t/pip-26-transition-from-matic-to-pol-validator-rewards/13046) The initial supply of POL is 10 billion tokens, matching the MATIC supply on a 1:1 basis at migration. For migration instructions, see the [MATIC to POL migration guide](/pos/concepts/tokens/matic-to-pol). ## Emission POL has an ongoing emissions schedule. The original proposal set a 2% annual emission rate, with 1% to the community treasury and 1% to validator rewards. Community consensus via [PIP-26](https://forum.polygon.technology/t/pip-26-transition-from-matic-to-pol-validator-rewards/13046) revised the validator reward percentage to: * 2% for year four (2023-2024) * 1.5% for year five (2024-2025) * 1% thereafter This results in an effective 2% annual POL emission beginning after June 2025. The emission rate can be changed through governance by upgrading the `EmissionManager` contract, but cannot exceed the `mintPerSecondCap` defined in the primary POL smart contract. ### How POL is minted The `EmissionManager` smart contract initiates minting using the `INTEREST_PER_YEAR_LOG2` constant to calculate an annual emission rate compounded per year. The contract distributes newly minted tokens to the `StakeManager` and `Treasury` contracts. The `EmissionManager` is upgradeable, allowing governance to change its behavior. ## Token migration and reversal POL migration from MATIC operates on a 1-to-1 conversion. A migration contract accepts MATIC and provides an equal amount of POL. The full supply of MATIC can be upgraded through this contract. After migration, MATIC is held in the migration contract. It is not burned. The migration contract includes an "unmigration" feature that allows users to convert POL back to an equivalent amount of MATIC. Governance controls whether this feature is enabled, providing flexibility in response to network conditions or security concerns. ## Bridging behavior With community approval, the bridge was modified to use POL as the native token: * Bridging POL to Polygon Chain: you receive an equal amount of native POL on Polygon Chain. * Bridging native tokens from Polygon Chain to Ethereum: the bridge disburses POL. Existing contracts that relied on receiving MATIC from the bridge and now receive POL instead may have locked funds. Developers must verify their contracts handle the token change correctly. ## Governance and security The POL contracts are governed by the Polygon decentralized governance model. Community proposals follow the PIP process. Security measures include rate limits on minting and the ability to lock or unlock features such as unmigration. ## Implications for dApp developers Developers generally will not encounter breaking changes from the MATIC to POL transition, because the token implements the same ERC-20 interface. However, any contract that relies on receiving MATIC from the bridge needs to be checked and updated to handle POL. If you have questions, reach out via the [Polygon R\&D Discord](https://discord.com/invite/0xpolygonrnd). Read the [blog post on the POL migration](https://polygon.technology/blog/save-the-date-matic-pol-migration-coming-september-4th-everything-you-need-to-know) for a detailed explanation of the token, its properties, and what the migration means for the ecosystem. ## Avoiding scams Always verify contract addresses through official sources. Exercise caution with any claims about "swaps" or "transfers" from unverified sources, as the token migration has attracted fraudulent activity. # sPOL Source: https://docs.polygon.technology/pos/concepts/tokens/spol What sPOL is, how Polygon's liquid staking token works, and how to stake POL to receive sPOL. sPOL is the liquid staking token for Polygon Chain. When you stake POL through the [Polygon Liquid Staking](https://staking.polygon.technology/lst) interface, you receive sPOL in return. sPOL represents your share of the staking pool, including accumulated rewards. It is a standard ERC-20 token that can be transferred, traded, or used in DeFi protocols while your underlying POL continues earning staking rewards. **Contract address (Ethereum mainnet):** `0x3B790d651e950497c7723D47B24E6f61534f7969` ## How sPOL works Liquid staking pools the POL of many stakers and delegates it to validators on Polygon Chain. The pool receives validator rewards on behalf of all participants. Rather than locking your tokens and managing delegation yourself, you stake through the pool and receive sPOL tokens that represent your proportional claim on the pooled POL plus rewards. The exchange rate between sPOL and POL increases over time as staking rewards accrue. When you unstake, you return sPOL and receive POL at the current exchange rate, which reflects the rewards earned since you staked. ### Validators sPOL delegates to a curated mix of high-performing validators from the Polygon validation set, selected by Polygon Labs. You do not choose individual validators when using liquid staking. ### Reward sources sPOL accumulates rewards from multiple sources, all of which are reflected in the sPOL/POL exchange rate over time: * Standard Polygon PoS staking rewards * Priority fees under PIP-85 * Additional priority fees shared by validators in the pool ## Liquidity and lock-up sPOL is a liquid staking token, so staking POL does not lock your position. You have two ways to exit: * **Via exchange**: Swap or sell sPOL on any exchange or DeFi protocol that supports it. This gives you immediate liquidity at the prevailing market rate. * **Via unstaking**: Return sPOL through the [Polygon Liquid Staking](https://staking.polygon.technology/lst) interface. This follows the same withdrawal delay as regular Polygon PoS staking and redeems sPOL 1:1 at the current exchange rate. ## Staking and unstaking To stake: 1. Go to [staking.polygon.technology/lst](https://staking.polygon.technology/lst). 2. Connect your wallet. 3. Enter the amount of POL you want to stake. 4. Approve the transaction and confirm the staking transaction. You receive sPOL tokens immediately in your connected wallet. The amount of sPOL you receive is calculated using the current exchange rate. sPOL appears as a standard ERC-20 token and is visible in any wallet that supports ERC-20 tokens. To unstake, return sPOL through the same interface. Unstaking is subject to a withdrawal delay determined by the underlying staking contracts. ## Using sPOL in DeFi Because sPOL is a standard ERC-20 token, it can be used in any protocol that supports ERC-20 tokens, such as lending markets, liquidity pools, and collateral vaults. Rewards continue to accrue in the exchange rate regardless of where sPOL is held. ## Contract security Always verify the contract address before interacting with any token claiming to be sPOL. The official contract address on Ethereum mainnet is `0x3B790d651e950497c7723D47B24E6f61534f7969`. Do not interact with contracts at any other address. If you prefer to delegate directly to a specific validator, see [how to stake as a delegator](/pos/how-to/delegate). # EIP-1559 Source: https://docs.polygon.technology/pos/concepts/transactions/eip-1559 How EIP-1559 changed gas estimation and transaction fees on Polygon Chain, and how Type 2 transactions differ from legacy transactions. The [London hard fork](https://blog.polygon.technology/eip-1559-upgrades-are-going-live-on-polygon-mainnet/) introduced [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) to Polygon, modifying how gas estimation and transaction costs work. ## What changed Before EIP-1559, transactions specified a single `gasPrice` field. Miners prioritized transactions with higher `gasPrice` bids. This model made gas estimation unpredictable during periods of high demand. EIP-1559 introduced a new transaction type, Type 2, which splits the old `gasPrice` into two components: * **`baseFee`**: a network-determined fee that is burned, calculated based on how full the previous block was. All transactions must pay this fee. * **`priorityFee` (tip)**: an optional amount offered to the validator to prioritize the transaction. The relationship is: `maxFeePerGas` sets the ceiling on what you are willing to pay. The actual fee paid is `baseFee + priorityFee`, where `priorityFee = min(maxPriorityFeePerGas, maxFeePerGas - baseFee)`. Legacy Type 0 transactions remain compatible but the Type 2 format is recommended. ## Legacy transaction (Type 0) In a legacy transaction, only `gasPrice` is specified: ```jsx theme={null} const sendLegacyTransaction = async () => { const web3 = new Web3('https://polygon-rpc.com'); await web3.eth.sendTransactions({ from: 0x05158d7a59FA8AC5007B3C8BabAa216568Fd32B3, to: 0xD7Fbe63Db5201f71482Fa47ecC4Be5e5B125eF07, value: 1000000000000000000, gasPrice: 200000000000 }) } ``` ## Type 2 transaction (EIP-1559) Type 2 transactions use `maxPriorityFeePerGas` instead of `gasPrice`. The `baseFee` is paid regardless, so only the tip is bid: ```jsx theme={null} const sendEIP1559Transaction = async () => { const web3 = new Web3('https://polygon-rpc.com'); await web3.eth.sendTransactions({ from: 0xFd71Dc9721d9ddCF0480A582927c3dCd42f3064C, to: 0x8C400f640447A5Fc61BFf7FdcE00eCf20b85CcAd, value: 1000000000000000000, maxPriorityFeePerGas: 40000000000 }) } ``` ## Gas estimation The Polygon Gas Station V2 provides current gas fee estimates: ``` https://gasstation.polygon.technology/v2 ``` Sample response: ```json theme={null} { "safeLow": { "maxPriorityFee": 37.181444553750005, "maxFee": 326.2556979087 }, "standard": { "maxPriorityFee": 49.575259405, "maxFee": 435.00759721159994 }, "fast": { "maxPriorityFee": 61.96907425625, "maxFee": 543.7594965144999 }, "estimatedBaseFee": 275.308812719, "blockTime": 6, "blockNumber": 23948420 } ``` ## See also * [How to send transactions with EIP-1559](https://docs.alchemy.com/alchemy/guides/eip-1559/send-tx-eip-1559) - covers both the legacy and EIP-1559 approaches. * [Send an EIP-1559 transaction with ethers.js](https://www.quicknode.com/guides/web3-sdks/how-to-send-an-eip-1559-transaction) - quicknode guide. # EIP-4337 Source: https://docs.polygon.technology/pos/concepts/transactions/eip-4337 How ERC-4337 account abstraction works on Polygon Chain, including UserOperations, Bundlers, EntryPoints, and Paymasters. The ERC-4337 standard, also known as EIP-4337, allows developers to achieve account abstraction on the Polygon Chain. This page provides a simplified overview of the different components of ERC-4337 and how they work together. The ERC-4337 standard consists of four main components: `UserOperation`, `Bundler`, `EntryPoint`, and `Contract Account`. Optional components include `Paymasters` and `Aggregators`. ## Building ERC-4337 transactions ERC-4337 transactions are called `UserOperations` to avoid confusion with the regular transaction type. `UserOperations` are pseudo-transaction objects that are used to execute transactions with contract accounts. ERC-4337 transactions have to be sent to nodes that include ERC-4337 bundlers. The `UserOperation` object has a few fields. | Field | Type | Description | | ---------------------- | --------- | --------------------------------------------------------------------------------------- | | `sender` | `address` | The address of the smart contract account | | `nonce` | `uint256` | Anti-replay protection | | `initCode` | `bytes` | Code used to deploy the account if not yet onchain | | `callData` | `bytes` | Data that's passed to the `sender` for execution | | `callGasLimit` | `uint256` | Gas limit for execution phase | | `verificationGasLimit` | `uint256` | Gas limit for verification phase | | `preVerificationGas` | `uint256` | Gas to compensate the bundler | | `maxFeePerGas` | `uint256` | Similar to EIP-1559 max fee | | `maxPriorityFeePerGas` | `uint256` | Similar to EIP-1559 priority fee | | `paymasterAndData` | `bytes` | `Paymaster Contract` address and any extra data required for verification and execution | | `signature` | `bytes` | Used to validate a `UserOperation` along with the `nonce` during verification | ## ERC-4337 wallets Users must have an ERC-4337 smart contract account to validate `UserOperations`. The core interface for an ERC-4337 wallet is: ```solidity theme={null} interface IAccount { function validateUserOp (UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds) external returns (uint256 sigTimeRange); } ``` ## Resources * [ERC-4337 proposal](https://eips.ethereum.org/EIPS/eip-4337) is the link to the official proposal and technical specification. * [@account-abstraction SDK](https://www.npmjs.com/package/@account-abstraction/sdk) is an npm package for using ERC-4337 developed by the authors of the proposal. * [Stackup](https://docs.stackup.sh/) provides node services with ERC-4337 bundlers and other ERC-4337 infrastructure. * [WalletKit](https://walletkit.com) is an all-in-one platform for adding smart, gasless wallets to your app. It has integrated support for ERC-4337 and comes with a paymaster and bundler included, requiring no extra setup. # Meta transactions Source: https://docs.polygon.technology/pos/concepts/transactions/meta-transactions What meta transactions are, why they matter, and how the relayer model decouples gas payment from transaction signing on Polygon Chain. ## The problem meta transactions solve The traditional transaction model requires users to pay gas fees directly from their wallet. This creates friction: users need to acquire cryptocurrency before they can interact with a dApp. Meta transactions address this by separating the transaction sender from the gas payer. ## What meta transactions are Meta transactions enable users to interact with the blockchain without holding tokens to cover gas fees. Instead of signing a complete Ethereum transaction, the user signs a message describing their intended action. A third-party relayer wraps that signed message into a standard transaction, pays the gas, and submits it to the network. The contract receiving the meta transaction unwraps it by validating the original signature, then executes the action on behalf of the user. In summary, the roles are: * **Sender (intender)**: signs a request with their private key and sends it to a relayer. Does not pay gas. * **Relayer**: validates the signed request, wraps it into a transaction, pays the gas, and submits it to the contract. * **Contract**: unwraps the transaction, validates the original signature, and executes the requested action. A meta transaction is different from a batch transaction. A batch transaction sends multiple transactions at once from a single sender in sequence. A meta transaction is a single action where the gas payer is separated from the signer. The sender becomes an "intender" rather than a payer: they express intent by signing a message, but do not construct or fund the underlying transaction. ## Use cases ### Voting A user wanting to vote in onchain governance signs a meta transaction containing their vote off-chain, then sends it to a relayer. The relayer validates priority, wraps it into a transaction, pays gas, and submits it to the voting contract. The contract validates the user's signature and records the vote. ### Gaming In blockchain-based games, players can perform in-game actions (trading items, upgrading characters) without holding ETH or POL. The relayer covers gas, and the player's signed message is submitted on their behalf. ### Minting NFTs Users can mint or purchase NFTs by signing a request. The relayer submits it, so users do not need to manage gas tokens. This lowers the barrier for users unfamiliar with managing cryptocurrency. ## Technical implementation The meta transaction pattern requires: 1. A contract that inherits `NativeMetaTransactions` and exposes an `executeMetaTransaction` function. For example, see the [ChildERC20 implementation](https://github.com/maticnetwork/pos-portal/blob/34be03cfd227c25b49c5791ffba6a4ffc9b76036/flat/ChildERC20.sol#L1338). 2. A relayer server that accepts signed EIP-712 formatted messages from clients and submits them to the contract. 3. Client-side code that fetches an EIP-712 signature from the user and calls the relayer API. ## Relayer options on Polygon Chain You can use an existing relayer service or run a custom relayer: * [Biconomy](https://docs.biconomy.io/quickstart) * [Gas Station Network (GSN)](https://docs.opengsn.org/#ethereum-gas-station-network-gsn) * [Infura](https://infura.io/product/ethereum/transactions-itx) * [Gelato](https://docs.gelato.network/developer-products/gelato-relay-sdk) # Becoming a validator Source: https://docs.polygon.technology/pos/get-started/becoming-a-validator Requirements, responsibilities, and links to the step-by-step guides for joining and operating the Polygon Chain validator set. Polygon network is transitioning from MATIC to POL, which will serve as the gas and staking token on Polygon Chain. Use the links below to learn more: * [Migrate from MATIC to POL](/pos/concepts/tokens/matic-to-pol) * [POL token specs](/pos/concepts/tokens/pol/) Validators maintain the Polygon Chain network by running nodes, staking POL, producing blocks, and participating in consensus. In exchange, they earn staking rewards and transaction fees. The network supports a maximum of 105 active validators at any time. New validators can join the active set only when an existing validator unbonds or is removed for poor performance. To apply for a validator slot, submit an application at the [Polygon validators hub](https://forms.clickup.com/90131459969/f/2ky41vw1-71033/JLIKCB37C0FLKHJEND). Submitting an application does not guarantee a slot. ## What validators run To participate as a validator, you must operate two types of nodes: * **Sentry node**: a Heimdall node and a Bor node, open to all peers on the network. Acts as a public-facing entry point. * **Validator node**: a Heimdall node and a Bor node, accessible only through its sentry node and closed to the rest of the network. You also stake POL tokens on the staking contracts deployed on Ethereum mainnet. The minimum stake is 10,000 POL (set by governance proposal PIP-4). ### What each layer does **Heimdall** (consensus layer): * Monitors staking contracts on Ethereum mainnet. * Verifies all state transitions on the Bor chain. * Submits Bor chain state checkpoints to Ethereum mainnet. Built on CometBFT and Cosmos SDK. See [GitHub: Heimdall](https://github.com/0xPolygon/heimdall-v2). **Bor** (execution layer): * Produces blocks on Polygon Chain. * Block producers are selected from the validator set by Heimdall based on stake. Built on Go Ethereum. See [GitHub: Bor](https://github.com/0xPolygon/bor). ## Validator responsibilities ### Node operations Node operations that happen automatically: * Block producer selection for each span. * Block validation on Bor. * Checkpoint proposal and signing. * State sync from Ethereum to Bor via the `ack/no-ack` checkpoint mechanism. ### Daily manual operations * Maintain high uptime. Approximately every 34 minutes, a checkpoint transaction must be signed by every validator. Missed signatures reduce your performance score. * Check Heimdall and Bor services and processes daily. Prune nodes regularly to reduce disk usage. * Monitor node health using Grafana dashboards (for example, [Matic-Jagar](https://github.com/vitwit/matic-jagar)) or your own tooling. * Keep an ETH balance of 0.5 to 1 ETH on your signer address. You need ETH to sign and submit checkpoint transactions on Ethereum mainnet. Running low causes delays in checkpoint submission and downstream finality. ### Delegation * Accept delegation from the community. Each validator sets their own commission rate. There is no upper limit. * Communicate commission rates and any rate changes via [Discord](https://discord.com/invite/0xPolygonCommunity) or the [Forum](https://forum.polygon.technology/). ### Communication * Report issues early via [Discord](https://discord.com/invite/0xPolygonCommunity), [Forum](https://forum.polygon.technology/), or [GitHub](https://github.com/0xPolygon). * Monitor [Polygon forum announcements](https://forum.polygon.technology/c/announcement/6) for node and validator updates. ## Join the network as a validator Use one of these guides to set up and run your validator node: * [Start and run nodes with Ansible](/pos/how-to/validator/validator-ansible/) * [Start and run nodes with binaries](/pos/how-to/validator/validator-binaries/) * [Stake as a validator](/pos/how-to/operate-validator-node/next-steps/#stake-tokens) ## Maintain your validator nodes * [Change the signer address](/pos/how-to/operate-validator-node/change-signer-address/) * [Change your commission rate](/pos/how-to/operate-validator-node/next-steps/#changing-your-commission-rate) ## Community assistance * [Discord](https://discord.com/invite/0xPolygonCommunity) * [Forum](https://forum.polygon.technology/) ## Related * **[Polygon Chain overview](/pos/overview)**: the dual-layer Heimdall + Bor architecture validators are securing. * **[Run a node](/pos/how-to/prerequisites)**: hardware requirements and prerequisites for full or validator nodes. # Building on PoS Source: https://docs.polygon.technology/pos/get-started/building-on-polygon How to connect to Polygon Chain, deploy contracts, bridge tokens, and find the right tooling for your project. Polygon network is transitioning from MATIC to POL, which will serve as the gas and staking token on Polygon Chain. Use the links below to learn more: * [Migrate from MATIC to POL](/pos/concepts/tokens/matic-to-pol) * [POL token specs](/pos/concepts/tokens/pol/) Polygon Chain is EVM-compatible. Foundry, Remix, Hardhat, ethers.js, and web3.js all work on Polygon without modification. Point your tooling at the [Polygon RPC](https://polygon-rpc.com/) and deploy. ## Prerequisites * An EVM-compatible wallet (MetaMask or equivalent). See [supported wallets](/tools/wallets/getting-started/). * Network configuration for Polygon mainnet or Amoy testnet. See [RPC endpoints and chain IDs](/pos/reference/rpc-endpoints/). * POL tokens for gas. For testnet, use the [Polygon faucet](/tools/gas/matic-faucet/). ## Connect to Polygon Chain Use any EVM wallet or node provider. The following example uses the Alchemy SDK to read from the network: ```js theme={null} // Setup: npm install alchemy-sdk const { Alchemy, Network } = require("alchemy-sdk"); const settings = { apiKey: "demo", // Replace with your Alchemy API key from https://www.alchemy.com network: Network.MATIC_MAINNET, // Use MATIC_AMOY for testnet }; const alchemy = new Alchemy(settings); async function main() { const latestBlock = await alchemy.core.getBlockNumber(); console.log("The latest block number is", latestBlock); } main(); ``` ## Deploy a contract Polygon Chain supports any contract that runs on Ethereum. There are no changes required to your Solidity code. Configure your deployment tool to target the Polygon RPC: * Mainnet: `https://polygon-rpc.com` (chain ID 137) * Amoy testnet: see [RPC endpoints](/pos/reference/rpc-endpoints/) ## Bridge tokens Most dApps that interact with Ethereum need to move tokens between the two networks. Use the bridge how-to guides: * [Bridge tokens from Ethereum to PoS](/pos/how-to/bridging/ethereum-polygon/ethereum-to-matic/) * [Bridge tokens from PoS to Ethereum](/pos/how-to/bridging/ethereum-polygon/matic-to-ethereum/) * [L1-L2 state transfer](/pos/how-to/bridging/l1-l2-communication/state-transfer/) ## Tooling and SDKs Core tools for building on Polygon Chain: * [Faucets](/tools/gas/matic-faucet/) - test token faucet * [Polygon Gas Station](/tools/gas/polygon-gas-station/) - gas estimation API * [Polygon dApp Launchpad](/tools/dApp-development/launchpad/intro/) - dApp development CLI * [Matic.js library](/tools/matic-js/get-started/) - JavaScript SDK for Polygon * [Popular third-party tooling](/tools/dApp-development/third-party-tutorials/) General Ethereum development resources that work on Polygon: * [Remix](https://remix.ethereum.org/) - browser-based Solidity IDE * [Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started) - development environment * [Foundry](https://github.com/foundry-rs/foundry/blob/master/README.md) - fast Solidity toolchain * [Ethers.js](https://docs.ethers.io/v5/) * [Web3.js](https://www.dappuniversity.com/articles/web3-js-intro) * [thirdweb](https://portal.thirdweb.com) If you are new to dApp development, the [Full Stack dApp Tutorial Series](https://kauri.io/full-stack-dapp-tutorial-series/5b8e401ee727370001c942e3/c) covers the basics end to end. ## Migrating an existing dApp from Ethereum Polygon Chain is EVM-compatible, so any contract that runs on Ethereum can be deployed to Polygon without code changes. The main considerations are: * Update your RPC endpoint and chain ID. * Check that any contracts receiving bridge funds handle the POL token rather than MATIC, since the native token has changed. * [Map your tokens](/pos/how-to/bridging/ethereum-polygon/submit-mapping-request/) if you need them recognized by the bridge. If you encounter issues or have questions, open an issue on the [Polygon Docs GitHub repository](https://github.com/0xPolygon/polygon-docs/issues), or reach out on [Community Discord](https://discord.com/invite/0xPolygonCommunity) or [R\&D Discord](https://discord.com/invite/0xpolygonrnd). # Governance fundamentals Source: https://docs.polygon.technology/pos/governance/governance-fundamentals How Polygon Chain governance works, from off-chain ecosystem consensus through onchain upgrade execution. Polygon Chain is a decentralized network of validator nodes. No single node controls the network, so changes require broad ecosystem consensus. If the ecosystem disagrees over a change, it can result in the network splitting, a protocol-level change generally referred to as forking. Changes to the network require a high level of coordination and ecosystem consensus to execute successfully. A hard fork happens when the node software changes in such a way that the new version is no longer backward-compatible with earlier blocks. This is usually the result of a change in the consensus logic, meaning that blocks validated using the latest software will produce a different hash. A block number is selected before which all nodes in the network should have upgraded to the new version. Nodes running the old version will be disconnected from the canonical chain after the hard fork block. Should there be $`{1/3}`+1$ staked POL in disagreement with the fork, two canonical chains will temporarily form until the end of the current span. Afterwards, Bor will stop producing blocks, and the chain will halt until consensus is reached. In contrast, a soft fork is backward-compatible with the pre-fork blocks. This type of protocol change does not require nodes to upgrade before a deadline, so multiple versions of the node software can run simultaneously and still validate transactions. The key ecosystem stakeholders involved in implementing a change are: * Users * Token holders * Validators * Infrastructure providers * Full nodes * Core developers ## Ecosystem consensus The preliminary ecosystem consensus takes place through an off-chain process involving key stakeholders, including users and core developers. The framework accommodates different perspectives and provides a platform for constructive discussion and community cohesion. The framework has three key components: 1. **[Polygon Improvement Proposals (PIPs)](https://github.com/maticnetwork/Polygon-Improvement-Proposals)**: Outlined in [PIP-1](https://github.com/maticnetwork/Polygon-Improvement-Proposals/blob/main/PIPs/PIP-01.md), PIPs are formal proposals that enable the community to put forward protocol upgrades. The framework is modeled on [Ethereum's EIP process](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1.md), with guiding principles from the [IETF](https://www.ietf.org/about/introduction/) and the broader open-source community. 2. **[Polygon Protocol Governance Call (PPGC)](https://github.com/maticnetwork/Polygon-Improvement-Proposals/tree/main/Project%20Management)**: Synchronous discussions where ["rough consensus"](https://datatracker.ietf.org/doc/html/rfc1603#:~:text=decisions%20through%20a%20%22-,rough%20consensus,-%22%20process.%0A%20%20%20IETF%20consensus) is established. The technical community uses these calls to decide which PIPs are included in a given upgrade and the rollout schedule. 3. **[Polygon Community Forum](https://forum.polygon.technology/)**: A space for long-form discussions ranging from high-level meta discussions to low-level technical details. ## Implementation The PoS network runs two clients simultaneously: * **Heimdall**: the [consensus layer](/pos/architecture/heimdall_v2/introduction/) client. [See GitHub](https://github.com/0xPolygon/heimdall-v2). * **Bor**: the [execution layer](/pos/architecture/bor/introduction/) client. [See GitHub](https://github.com/0xPolygon/bor). These clients are ecosystem focal points rather than control switches operated by core developers. Neither client alone can dictate protocol decisions. When a hard fork is agreed upon through community consensus, the upgrade process generally follows this sequence: * The protocol decision is ratified on a PPGC, and implementation begins in the relevant GitHub repositories. * Core developers create pull requests containing the changes, which are merged into the codebase, and a new release tag is created. * Core developers test new releases on local devnets. If everything functions normally, the tag is marked as beta (pre-release). * The changes are deployed to the Amoy testnet and left to run for at least one week. The Amoy Testing Committee reports on stability in the PPGC. * Once confirmed stable, the upgrade is scheduled for mainnet release on a PPGC. The tag version is marked final. * Validators upgrade their nodes to the latest version. The upgrade becomes canonical through onchain consensus of the validating stake, including delegated stake from token holders. ## Onchain consensus Onchain consensus parameters are inherited from [CometBFT](https://github.com/0xPolygon/cometbft/), which requires at least $`{2/3}`rd$ of the total validating stake to be in favor of the upgrade. For the chain to remain stable after the change becomes canonical, non-validating full nodes must also upgrade to the latest version. Key ecosystem stakeholders such as dApps, exchanges, and RPC providers run full nodes. These nodes propagate transactions and blocks, and can accept or reject blocks. This makes them enforcers of the network consensus rules and vital to the onchain governance process. If these nodes are incompatible with the changes, users and dApps would find their transactions invalid. ## Onchain governance module The Heimdall client has an [in-built governance module](https://github.com/0xPolygon/cosmos-sdk/tree/devel/x/gov) that can carry out consensus parameter changes across the network without a full client upgrade: * Proposals are submitted to the onchain module along with a deposit containing the proposed changes. * Each validator tallies votes cast by the validator set. * When the defined voting parameters are met, each validator applies the upgrade with the proposal data. Current voting parameters (denominated in staked POL): | Parameter | Value | | --------- | ----- | | Quorum | 33.4% | | Threshold | 50% | | Veto | 33.4% | # Ethereum to PoS Source: https://docs.polygon.technology/pos/how-to/bridging/ethereum-polygon/ethereum-to-matic Deploy sender and receiver contracts to transfer arbitrary data from Ethereum to Polygon Chain using the state sync mechanism. This guide walks through deploying a sender contract on Sepolia (Ethereum testnet) and a receiver contract on Amoy (Polygon testnet), then sending and verifying arbitrary data across the bridge. For background on how state sync works, see the [state sync architecture docs](/pos/architecture/bor/state-sync/). The contracts and scripts in this guide are simplified illustrations. Do not deploy them to production without a thorough security review and audit appropriate for your use case. Looking to bridge a token to Polygon Chain using the official bridge? Check out the guide on [how to submit a request to get your token mapped](/pos/how-to/bridging/ethereum-polygon/submit-mapping-request/). ### Deploy sender contract The sender contract calls [`syncState`](https://github.com/0xPolygon/pos-contracts/blob/e999579e9dc898ab6e66ddcb49ee84c2543a9658/contracts/root/stateSyncer/StateSender.sol#L33) on the `StateSender` contract on Ethereum. This emits a `StateSynced` event that Heimdall validators pick up and relay to Bor. `StateSender` is deployed at: * `0x49E307Fa5a58ff1834E0F8a60eB2a9609E6A5F50` on Sepolia * `0x28e4F3a7f651294B9564800b2D01f35189A5bFbE` on Ethereum mainnet ```solidity title="Sender.sol" theme={null} pragma solidity ^0.8.0; interface IStateSender { function syncState(address receiver, bytes calldata data) external; } contract Sender { address public stateSenderContract; address public receiver; uint public states; constructor(address _stateSender, address _receiver) { stateSenderContract = _stateSender; receiver = _receiver; } function sendState(bytes calldata data) external { states += 1; IStateSender(stateSenderContract).syncState(receiver, data); } } ``` Deploy `Sender.sol` to Sepolia, passing in the `StateSender` address and your receiver contract address. Note the deployed address and ABI. ### Deploy receiver contract The receiver contract is invoked by the `StateReceiver` system contract on Bor whenever a matching `StateSynced` event is processed. Always check that `msg.sender` is the `StateReceiver` contract at `0x0000000000000000000000000000000000001001`. Without this check, any address on Polygon can call `onStateReceive` on your contract directly. ```solidity title="Receiver.sol" theme={null} pragma solidity ^0.8.0; interface IStateReceiver { function onStateReceive(uint256 stateId, bytes calldata data) external; } contract Receiver is IStateReceiver { address constant STATE_RECEIVER = 0x0000000000000000000000000000000000001001; uint public lastStateId; bytes public lastData; function onStateReceive(uint256 stateId, bytes calldata data) external { require(msg.sender == STATE_RECEIVER, "unauthorized"); lastStateId = stateId; lastData = data; } } ``` `stateId` is a monotonically increasing counter that uniquely identifies each state sync event. Deploy `Receiver.sol` to Amoy and note the deployed address and ABI. ### Register your sender and receiver contracts Before state sync will trigger your receiver, your sender/receiver pair must be registered with the `StateSender` contract. This registration is what authorizes the pair: only events originating from a registered sender contract targeting a registered receiver will be relayed by Heimdall validators. Registration for new custom sender/receiver pairs is managed by the Polygon team. Contact the team on [Discord](https://discord.com/invite/0xPolygonCommunity) or submit a request via [the mapping form](https://docs.google.com/forms/d/e/1FAIpQLSeq8HTef2dYpRx35_WWYhyr4C146K9dfhyYJQcoD1RuTTVABg/viewform) to initiate the process. If you want to test end-to-end before your custom contracts are registered, you can use the already-registered example contracts referenced in the form. ### Send and verify data With contracts deployed and registered, use the following script to send arbitrary bytes from Sepolia and verify receipt on Amoy. State sync takes approximately 7 to 8 minutes to complete. ```javascript title="test.js" theme={null} const { Web3 } = require('web3'); const main = new Web3(''); const matic = new Web3(''); const privateKey = '0x...'; main.eth.accounts.wallet.add(privateKey); const senderAddress = ''; const senderABI = [/* paste ABI here */]; const receiverAddress = ''; const receiverABI = [/* paste ABI here */]; const sender = new main.eth.Contract(senderABI, senderAddress); const receiver = new matic.eth.Contract(receiverABI, receiverAddress); async function sendData(message) { const data = matic.utils.asciiToHex(message); const tx = await sender.methods.sendState(data).send({ from: main.eth.accounts.wallet[0].address, gas: 200000, }); console.log('Sent from Ethereum:', tx.transactionHash); } async function checkReceiver() { const stateId = await receiver.methods.lastStateId().call(); const rawData = await receiver.methods.lastData().call(); const message = matic.utils.hexToAscii(rawData); console.log('Last state ID:', stateId); console.log('Received message:', message); } async function run() { await sendData('Hello from Ethereum!'); console.log('Waiting ~8 minutes for state sync...'); setTimeout(checkReceiver, 8 * 60 * 1000); } run(); ``` Successful output looks like: ```bash theme={null} Sent from Ethereum: 0x4f64ae4ab4d2b2d2dc82cdd9ddae73af026e5a9c46c086b13bd75e38009e5204 Waiting ~8 minutes for state sync... Last state ID: 453 Received message: Hello from Ethereum! ``` # PoS to Ethereum Source: https://docs.polygon.technology/pos/how-to/bridging/ethereum-polygon/matic-to-ethereum Deploy child and root contracts to transfer data from Polygon Chain back to Ethereum using checkpoints and the RootChainManager. This guide demonstrates transferring a `uint256` value from Polygon Chain to Ethereum. The same approach applies to any data type: encode the data as bytes, emit it from the child contract, then decode it in the root contract after checkpoint verification. For background on the checkpoint and predicate mechanism, see the [state sync architecture docs](/pos/architecture/bor/state-sync/). The contracts in this guide are simplified illustrations. Do not deploy them to production without a thorough security review and audit appropriate for your use case. ## Implementation ### Deploy contracts Create the child contract on Polygon Chain and the root contract on Ethereum. The function that performs the state change must emit an event whose parameters include the data to transfer. ```solidity Child.sol theme={null} pragma solidity ^0.8.0; contract Child { event Data(address indexed from, bytes bytesData); uint256 public data; function setData(bytes memory bytesData) public { data = abi.decode(bytesData, (uint256)); emit Data(msg.sender, bytesData); } } ``` ```solidity Root.sol theme={null} pragma solidity ^0.8.0; contract Root { address public predicate; constructor(address _predicate) { predicate = _predicate; } modifier onlyPredicate() { require(msg.sender == predicate, "unauthorized"); _; } uint256 public data; function setData(bytes memory bytesData) public onlyPredicate { data = abi.decode(bytesData, (uint256)); } } ``` Pass `0x1470E07a6dD1D11eAE439Acaa6971C941C9EF48f` as the `_predicate` value in the `Root` constructor. The `onlyPredicate` modifier ensures that only the predicate contract can update state on the root contract. The predicate is invoked by `RootChainManager` on Ethereum after it verifies the transaction against a Polygon checkpoint, guaranteeing state changes on Ethereum reflect verified Polygon activity. ### Map your contracts Once both contracts are deployed, submit a mapping request so the PoS bridge recognizes the child/root pair. See the [submit a mapping request guide](/pos/how-to/bridging/ethereum-polygon/submit-mapping-request/) for steps. Already working with a widely used token? The [Polygon Portal](/tools/wallets/portal/) lists tokens that are already mapped and available for bridging without a new mapping request. ### Initiate a transfer on Polygon Call `setData` on the child contract with the encoded value you want to transfer. Wait for the transaction to be included in a Polygon checkpoint. Checkpoints are submitted to Ethereum roughly every 30 minutes. You can verify checkpoint inclusion using the [Polygon checkpoint tracker](https://github.com/rahuldamodar94/matic-learn-pos/blob/transfer-matic-ethereum/script/check-checkpoint.js) or by polling the `RootChainManager` on Ethereum. ### Exit on Ethereum After the checkpoint is confirmed, call `exit` on `RootChainManager` on Ethereum to finalize the transfer. You need the transaction hash of the `setData` call on Polygon and the keccak-256 hash of the `Data` event signature. The `logEventSignature` for the `Data(address,bytes)` event is: ``` 0x93f3e547dcb3ce9c356bb293f12e44f70fc24105d675b782bd639333aab70df7 ``` Using `matic.js`: ```javascript theme={null} import { POSClient, use } from "@maticnetwork/maticjs"; const client = new POSClient(); await client.init({ network: "mainnet", version: "v1" }); const txHash = "0x"; const logEventSignature = "0x93f3e547dcb3ce9c356bb293f12e44f70fc24105d675b782bd639333aab70df7"; const tx = await client.exitUtil.buildPayloadForExit(txHash, logEventSignature, false); ``` Alternatively, call `exit(bytes calldata inputData)` directly on `RootChainManager` at `0xA0c68C638235ee32657e8f720a23ceC1bfc77C77` on Ethereum mainnet, constructing the proof payload from the burn receipt. Once the exit transaction is confirmed, query the `data` variable on the root contract to verify the value has been reflected on Ethereum. The full contract source and exit script for this example are available in the [matic-learn-pos repository](https://github.com/rahuldamodar94/matic-learn-pos/tree/transfer-matic-ethereum). # Polygon Portal Source: https://docs.polygon.technology/pos/how-to/bridging/ethereum-polygon/portal-ui Reference overview of the Polygon Portal bridge, including how tokens are locked and minted when crossing between Ethereum and Polygon Chain. The Polygon Portal is the official trustless two-way bridge between Polygon Chain and Ethereum. It lets you transfer tokens without third-party risk or market liquidity constraints. The bridge is available for both the PoS Amoy testnet and mainnet. Access it at [Polygon Portal](https://portal.polygon.technology). To learn more about the features that Polygon Portal offers, and a series of step-by-step instructions that help you with using the platform, check out [the Portal guide](/tools/wallets/portal/). There is no change to the circulating supply of your token when it crosses the bridge. This is what goes on in the background when you bridge your tokens over to Polygon Chain from Ethereum: * When depositing, tokens that leave the Ethereum network are locked and the same number of tokens are minted on Polygon Chain as a pegged token (1:1). * When withdrawing tokens back to the Ethereum network, tokens are burned on Polygon Chain and unlocked on Ethereum during the process. ## Additional resources * [Introduction to Blockchain Bridges](https://ethereum.org/en/bridges/) * [What are Cross-Chain Bridges?](https://www.alchemy.com/overviews/cross-chain-bridges) # Submit mapping request Source: https://docs.polygon.technology/pos/how-to/bridging/ethereum-polygon/submit-mapping-request Steps to submit a token mapping request to enable bridging between Ethereum and Polygon Chain. Submit a mapping request to enable your token for bridging between Ethereum and Polygon Chain. Thinking about bridging a popular token? Refer to the [reference guide on Polygon Portal](/tools/wallets/portal/). ## Steps to submit a mapping request 1. To submit a request for mapping your token on Polygon Chain, start by navigating to [the Google form available here](https://docs.google.com/forms/d/e/1FAIpQLSeq8HTef2dYpRx35_WWYhyr4C146K9dfhyYJQcoD1RuTTVABg/viewform). 2. Choose a **Network** option depending upon the chain on which you're looking to map your token. This would be **Sepolia ↔ Polygon Amoy** for testnet, and **Ethereum ↔ Polygon Chain** for mainnet. form-1 3. Next, input the contract address for the token contract that you've deployed on Sepolia/Ethereum mainnet in the **Root Contract Address (L1)** field. form-2 4. Choose the correct **Token Type** for your token. i.e., [ERC-20](https://eips.ethereum.org/EIPS/eip-20) for a standard token, [ERC-721](https://eips.ethereum.org/EIPS/eip-721) for an NFT, or [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) for a multi token. form-3 5. Finally, select **Submit** to send in your request. The Polygon team will review your mapping request, and get back to you with a response. This generally takes up to 7 days. * Check out the list of supported tokens available in JSON format by following this URL: [https://api-polygon-tokens.polygon.technology/tokenlists/polygonTokens.tokenlist.json](https://api-polygon-tokens.polygon.technology/tokenlists/polygonTokens.tokenlist.json) * Once approved, your token will be added to the above list! # State Transfer Source: https://docs.polygon.technology/pos/how-to/bridging/l1-l2-communication/state-transfer Overview of L1-L2 state transfer options on Polygon Chain, including state sync, FxPortal, and the token bridge. Polygon Chain provides several contracts for passing data between Ethereum (L1) and Polygon (L2). They serve different purposes and operate at different levels of abstraction. The right choice depends on what you are transferring and in which direction. ## Ethereum to Polygon (L1 to L2) Data flows from Ethereum to Polygon via the **state sync** mechanism. A contract on Ethereum calls `syncState` on the `StateSender` contract, which emits a `StateSynced` event. Heimdall validators relay this event to Bor, which calls `onStateReceive` on the target contract on Polygon within a single sprint (approximately 16 blocks, or a few minutes). This mechanism is used internally by both the PoS token bridge and FxPortal. You can also use it directly for arbitrary data, subject to the registration requirement described below. For a step-by-step implementation, see [Ethereum to PoS](/pos/how-to/bridging/ethereum-polygon/ethereum-to-matic/). ### Registration requirement Sender/receiver pairs must be registered with `StateSender` before state sync will relay events between them. This registration authorizes the pair: Heimdall only processes `StateSynced` events from registered sender contracts targeting registered receiver contracts. Registration is managed by the Polygon team. Contact the team on [Discord](https://discord.com/invite/0xPolygonCommunity) or via the [mapping request form](https://docs.google.com/forms/d/e/1FAIpQLSeq8HTef2dYpRx35_WWYhyr4C146K9dfhyYJQcoD1RuTTVABg/viewform) to register a new pair. If you need arbitrary message passing without registering a custom pair, FxPortal provides a pre-registered sender/receiver abstraction. See the [FxPortal contracts](#fxportal) below. ### Security in receiver contracts The `onStateReceive` function on Polygon is called by the `StateReceiver` system contract at address `0x0000000000000000000000000000000000001001`. Always verify `msg.sender` against this address in your receiver implementation. Without this check, any address on Polygon can call `onStateReceive` directly and inject arbitrary data. See the [state sync architecture docs](/pos/architecture/bor/state-sync/) for a full description of how state sync works. ## Polygon to Ethereum (L2 to L1) Data flows from Polygon back to Ethereum via the **checkpoint and exit** mechanism. There is no continuous message relay in this direction. Instead: 1. A transaction on Polygon emits an event containing the data to transfer. 2. The transaction is included in a Polygon checkpoint, which is committed to Ethereum roughly every 30 minutes. 3. After the checkpoint is finalized on Ethereum, anyone can submit an exit proof to `RootChainManager` on Ethereum to trigger the corresponding state change on the root contract. This is how the PoS bridge handles withdrawals: tokens are burned on Polygon, and after checkpoint, the lock is released on Ethereum. The same pattern works for arbitrary data. For a step-by-step implementation, see [PoS to Ethereum](/pos/how-to/bridging/ethereum-polygon/matic-to-ethereum/). ## Choosing the right approach | Need | Recommended approach | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Bridge a standard ERC-20, ERC-721, or ERC-1155 token | [Submit a mapping request](/pos/how-to/bridging/ethereum-polygon/submit-mapping-request/) and use the PoS bridge contracts | | Send arbitrary data from Ethereum to Polygon without custom registration | Use FxPortal (`FxRoot` / `FxChild`) | | Send arbitrary data from Ethereum to Polygon with a custom registered pair | Raw state sync via `StateSender` | | Send data from Polygon back to Ethereum | Checkpoint and exit via `RootChainManager` | If you are building fee collection or settlement infrastructure that bridges tokens from Polygon back to Ethereum from a smart contract, use the PoS bridge withdrawal path: call `withdraw` on the child ERC-20 contract on Polygon to burn tokens, then after checkpoint, call `exit` on `RootChainManager` on Ethereum to release the funds to your target address. ## Contract reference The following contracts are used for L1-L2 messaging on mainnet. Source addresses are from the [canonical network config](https://github.com/0xPolygon/static/blob/master/network/mainnet/v1/index.json). ### State sync These are the low-level contracts that underpin all L1-to-L2 messaging on Polygon Chain. | Contract | Network | Address | Purpose | | --------------- | -------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `StateSender` | Ethereum | `0x28e4F3a7f651294B9564800b2D01f35189A5bFbE` | Entry point for L1→L2 messages. Registered sender contracts call `syncState` here. | | `StateReceiver` | Polygon | `0x0000000000000000000000000000000000001001` | Genesis contract that delivers state sync messages on Polygon by calling `onStateReceive` on the target contract. | ### FxPortal FxPortal is Polygon's pre-registered messaging layer built on top of state sync. `FxRoot` is already registered as a sender in `StateSender`, and `FxChild` is its registered receiver on Polygon. You can inherit from `FxBaseRootTunnel` and `FxBaseChildTunnel` to build bidirectional messaging without needing custom registration. | Contract | Network | Address | Purpose | | --------------------- | -------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `FxRoot` | Ethereum | `0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2` | Ethereum-side entry point for FxPortal. Calls `syncState` on `StateSender`. | | `FxChild` | Polygon | `0x8397259c983751DAf40400790063935a11afa28a` | Polygon-side receiver for FxPortal. Called by `StateReceiver` and routes messages to child tunnel contracts. | | `FxERC20RootTunnel` | Ethereum | `0x0cc2caed31490b546c741bd93dbba8ab387f7f2c` | FxPortal tunnel for bridging ERC-20 tokens via FxRoot. | | `FxERC721RootTunnel` | Ethereum | `0xca1f5ec720eCdA31bE3d80BD3ef4686cBb07eb4D` | FxPortal tunnel for bridging ERC-721 tokens via FxRoot. | | `FxERC20ChildTunnel` | Polygon | `0xd531Cf2142D9b9Dc8b077dF3c4E93B46E7Cf879a` | Polygon-side counterpart to `FxERC20RootTunnel`. | | `FxERC721ChildTunnel` | Polygon | `0x2b4732e448b3023131a7b25046b3A5EF50CfCf71` | Polygon-side counterpart to `FxERC721RootTunnel`. | ### PoS token bridge These contracts handle token deposits and withdrawals through the official PoS bridge. Use these when bridging mapped ERC-20, ERC-721, or ERC-1155 tokens rather than arbitrary data. | Contract | Network | Address | Purpose | | ------------------------ | -------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `RootChainManagerProxy` | Ethereum | `0xA0c68C638235ee32657e8f720a23ceC1bFc77C77` | Main entry point for PoS token bridge on Ethereum. Call `depositFor` to deposit tokens and `exit` to finalize withdrawals after checkpoint. | | `ChildChainManagerProxy` | Polygon | `0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa` | Polygon-side manager. Mints tokens on deposit and processes burns on withdrawal. | | `ERC20PredicateProxy` | Ethereum | `0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf` | Locks ERC-20 tokens on Ethereum on deposit; releases them on exit. | | `ERC721PredicateProxy` | Ethereum | `0xE6F45376f64e1F568BD1404C155e5fFD2F80F7AD` | Locks ERC-721 tokens on Ethereum on deposit; releases them on exit. | | `ERC1155PredicateProxy` | Ethereum | `0x0B9020d4E32990D67559b1317c7BF0C15D6EB88f` | Locks ERC-1155 tokens on Ethereum on deposit; releases them on exit. | # Delegate tokens Source: https://docs.polygon.technology/pos/how-to/delegate Steps to delegate POL tokens to a validator, withdraw rewards, restake, and manage your stake on Polygon Chain. This guide covers delegating tokens, withdrawing rewards, restaking, unbonding, and moving stake on Polygon Chain. ## Prerequisites * POL tokens and ETH on your Ethereum mainnet address. Polygon network is transitioning from MATIC to POL, which will serve as the gas and staking token on Polygon Chain. Use the links below to learn more: * [Migrate from MATIC to POL](/pos/concepts/tokens/matic-to-pol) * [POL token specs](/pos/concepts/tokens/pol/) It is advisable to [migrate your MATIC tokens to POL](/pos/concepts/tokens/matic-to-pol), but if you continue to delegate MATIC tokens, you'll receive the staking rewards in the form of POL tokens. ## Access the dashboard 1. In your wallet (e.g. MetaMask), choose the Ethereum mainnet.
Figure: Choose ERTHh mainnet
2. Log in to [Polygon Staking](https://staking.polygon.technology/). 3. Once you log in, you will see overall statistics along with the list of validators.
img
If you are a validator, use a different non-validating address to log in as delegator. ## Delegate to a validator 1. Select **Become a Delegator**, or scroll down to a specific validator and select **Delegate**.
img
2. Select POL or MATIC from the drop-down list and enter the token amount to delegate. It is recommended to migrate your MATIC tokens to POL and delegate POL tokens. If you choose MATIC, you'll still receive your staking rewards in POL. Then, select **Continue**.
img
3. Approve the delegate transaction from your wallet and select **Delegate**.
img
After the delegation transaction completes, you will see the **Delegation Completed** message.
img
## View your delegations To view your delegations, select **My Account**. img ## Withdraw rewards 1. Select **My Account**.
img
2. Under your delegated validator, select **Withdraw Rewards**.
img
This will withdraw the POL token rewards to your Ethereum address. ## Restake rewards 1. Select **My Account**.
img
2. Under your delegated validator, click **Restake Reward**. img This will restake the POL token rewards to the validator and increase your delegation stake. ## Unbond from a validator 1. Select **My Account**.
img
2. Under your delegated validator, select **Unbond**. img This will withdraw your rewards from the validator and your entire stake from the validator. Your withdrawn rewards will show up immediately in your Ethereum wallet. Your withdrawn stake funds will remain locked for *80 checkpoints*. img The fund locking for the unbonding period is in place to ensure there is no malicious behavior on the network. ## Move stake from one node to another node Moving stake from one node to another node is a single transaction. There are no delays or unbonding periods during this event. 1. Select **My Account** and login to the staking dashboard. 2. Select **Move Stake** under your delegated validator. 3. Select an external validator and select **Stake here**. img 4. Provide the stake amount and select **Move Stake**.
img
This will move the stake. The dashboard will update *after 12 block confirmations*. ## Common questions ### What is the staking dashboard URL? The staking dashboard URL is [https://staking.polygon.technology/](https://staking.polygon.technology/). ### What is the minimum stake amount? There is no minimum stake amount to delegate. However, you can always start with 1 POL token. ### How to stake tokens on Polygon? For staking, you would need to have funds on the Ethereum mainnet (more information [here](https://etherscan.io/gastracker)). Log into your wallet on the Ethereum network using the [staking dashboard](https://staking.polygon.technology/). Please watch this video for a graphical illustration of how this works: **VIDEO\_UNAVAILABLE\_PLACEHOLDER**: `/img/pos/staking.mp4` (removed because it exceeds Cloudflare Workers’ 25MB upload limit). ### Why does my transaction take so long? All staking transactions of Polygon Chain take place on Ethereum for security reasons. The time taken to complete a transaction depends on the gas fees that you have allowed and also the network congestion of Ethereum mainnet at that point in time. You can always use the **Speed Up** option to increase the gas fees so that your transaction can be completed soon. ### I've staked my POL tokens. How can I stake more? Navigate to the **Your Delegations** page and choose one of the stakes. Then click on **Stake More**. Please watch this video for a graphical illustration of how this works: ### Why am I not able to stake? Check if you have funds on the Main Ethereum Network, to delegate your tokens. All staking happens on the Ethereum Network only. ### I am unable to view the staking tab. How do I access staking? You just need to access **[https://staking.polygon.technology/](https://staking.polygon.technology/)**, where you will see the following landing page: img ### How do I know which validator to select for better rewards? It depends on your understanding and research on which validator you would want to stake on. You can find the list of validators here : [https://staking.polygon.technology/validators](https://staking.polygon.technology/validators) ### How to unbond? To unbond from a validator, navigate to **My Account**, where you'll find **Your Delegations**. There you will see an **Unbond** button for each of the validators. Click on the **Unbond** button for the validator that you want to unbond from. img Please watch the video for a graphical illustration of how this works: ### What is the unbonding period? The unbonding period on Polygon Chain is 80 checkpoints. Every checkpoint takes approximately *30 minutes*. However, some checkpoints could be delayed due to congestion on Ethereum. This period applies to the originally delegated amount and re-delegated amounts. It does not apply to any rewards that were not re-delegated. ### How to restake rewards? Go to **My Account** to check **Your Delegations**. Clicking on **Restake Reward** will ask you for confirmation from your wallet account. Once you confirm the transaction in your wallet, the restake transaction is completed. #### Step 1
img
#### Step 2
img
Please watch the video for a graphical illustration of how this works: ### I want to restake rewards but I am unable to. You'll need to have a minimum of **2 POL** to restake rewards. ### How to withdraw rewards? You can claim your rewards by clicking on the **My Account**, all the delegators for a validator are displayed. Click on the **Withdraw Reward** button and the rewards will be transferred to your delegated account in wallet. #### Step 1
img
#### Step 2
img
Please watch the video for a graphical illustration of how this works: ### I want to withdraw rewards but I am unable to. You'll need to have a minimum of **2 POL** available to withdraw rewards. ### How to claim stake? Once the unbonding period is complete, the **Claim Stake** button will be enabled and you can then claim your staked tokens. The tokens will be transferred to your account. #### Step 1
img
#### Step 2
img
#### Step 3
img
Please watch the video for a graphical illustration of how this works: ### Which wallets are currently supported? We have recently upgraded the wallet support to WalletConnect v2.0. Now you can choose from a plethora of wallets, including Metamask, Coinbase, and others, on both desktop and mobile devices to log in.
Figure: Supported wallet
### Are hardware wallets supported? Yes, hardware wallets are supported. You can use the **Connect Hardware Wallet** option on MetaMask and connect your hardware wallet and then continue the delegation process. ### Why can’t I stake directly from Binance? Staking through Binance is not yet supported. There will be an announcement if and when Binance starts supporting it. ### I have completed my delegation, where can I check details? Once you have completed your delegation, wait for 12 block confirmations on Ethereum (approx. 3-5 minutes), then on the dashboard, you can click on **My Account**. Figure: My account ### Where can I check my rewards? On the dashboard, you can click on the **My Account** option on the left-hand side. Figure: My account ### Do I need ETH to pay for gas fees? Yes. You should maintain at least \~0.05-0.1 ETH balance for gas fees to be safe. ### Do I need to deposit POL tokens to the Polygon mainnet network for staking? No. All your funds need to be on the main Ethereum network. ### When I try to do the transaction my **Confirm** button is disabled. Please check if you have enough ETH for the gas fees. ### When do rewards get distributed? The rewards are distributed whenever a checkpoint is submitted. Currently, 71795 POL tokens are distributed proportionately on each successful checkpoint submission to each delegator based on their stake relative to the overall staking pool of all validators and delegators. Also, the percentage for the reward distributed to each delegator will vary with each checkpoint depending on the relative stake of the delegator, validator and the overall stake. Note that there is a 10% proposer bonus that accrues to the validator who submits the checkpoint, but over time, the effect of the extra bonus is nullified over multiple checkpoints by different validators. The checkpoint submission is done by one of the validators approximately every 30 minutes. This time is approximate and may vary based on validator consensus on the Polygon Heimdall layer. This may also vary based on Ethereum Network. Higher congestion in the network may result in delayed checkpoints. You can track checkpoints on the staking contract [here](https://etherscan.io/address/0x86e4dc95c7fbdbf52e33d563bbdb00823894c287) ### Why do rewards keep getting decreased at every checkpoint? Actual rewards earned will depend on the actual total locked supply in the network at each checkpoint. This is expected to vary significantly as more POL tokens get locked in the staking contracts. Rewards will be higher, to begin with, and will keep decreasing as the locked supply % goes up. This change in locked supply is captured at every checkpoint, and rewards are calculated based on this. ### How can I claim my rewards? You can claim your rewards instantly by clicking on the **Withdraw Reward** button. This will transfer the rewards accumulated to your delegated account on Metamask. Figure: Withdraw-reward ### Will I keep receiving rewards after I unbond? No. Once you unbond, you will stop receiving rewards. ### How many transactions does the delegation require? The delegation action takes place in 2 successive transactions. The first transaction to *approve* the request, and the second to *deposit* the approved token amount. Figure: Delegate ### What does re-delegate rewards mean? Re-delegating your rewards simply means that you want to increase your stake by restaking the rewards you have accumulated. ### Can I stake to any validator? Yes, you can delegate to any validator except the ones which are offline. ### Can I move the stake to another validator? Yes, you just have to access **Your Delegations**, click on **Move Stake**, and then choose your new validator. Please watch the video for a graphical illustration of how this works: ### Which browser is compatible with the Polygon earnings calculator? Chrome, Firefox, and Brave. ### My MetaMask is stuck at confirming after login, what do I do? Or nothing happens when I try to login? Check for the following: * If you’re using Brave, please turn off the option for **Use Crypto Wallets** in the settings panel. * Check if you are logged into Metamask * Check if you are logged into MetaMask with Trezor/Ledger. You need to additionally turn on permission to call contracts on your Ledger device, if not enabled already. * Check your system timestamp. If the system time is not correct, you will need to correct it. ### How do I send funds from Binance or other exchanges to Polygon wallet? The Polygon Wallet Suite is a web application. First, you must withdraw your funds from Binance or any other exchange to your Ethereum address on Metamask. If you don't know how to use Metamask, google it a bit. There are plenty of videos and blogs to get started with it. ### When can I become a validator and how many tokens do I need for that? To become a validator in the PoS network, you'll need to hold and stake a minimum of 10,000 POL tokens, and go through an admissions process to ensure network security." ### If I have earned rewards while delegating, and if I add additional funds to the same validator node, what happens? If you have not re-delegated your rewards before delegating additional funds to the same validator node, your rewards will be withdrawn automatically. In case you don't want that to happen, re-delegate your rewards before delegating additional funds. ### I have delegated my tokens via MetaMask on the dashboard. Do I need to keep my system or device on? No. Once your delegation transactions are confirmed, and you can see your tokens reflected in the **Total Stake** and **New Reward** sections, then you are done. There is no need to keep your system or device on. ### I have unbonded, how long will it take to unbond? The unbonding period is currently set to 80 checkpoints. Every checkpoint takes approximately 30 minutes. However, some checkpoints could be delayed up to \~1 hour due to congestion on Ethereum. ### I have unbonded, and I now see the **Claim Stake** button, but it is disabled. The Claim Stake button will only be enabled when your unbonding period is complete. The unbonding period is currently set at 80 checkpoints. ### When will the **Claim Stake** button be enabled? Under the **Claim Stake** button you should see a note on how many checkpoints are pending before the **Claim Stake** button is enabled. Every checkpoint takes approximately 30 minutes. However, some checkpoints could be delayed up to \~1 hour due to congestion on Ethereum. Figure: Unbound ### Have the foundations nodes been turned off? Yes, the foundation nodes had been turned off. ### Will there be any foundation nodes in the future? No, there won't be any Foundation nodes in the future. ### How many transactions do I need to pay for gas when I perform a 'move stake' action? The 'move stake' operation is performed in a single transaction. Since all transactions are executed on the Ethereum blockchain, you will need to spend some ETH for gas fees to complete the transaction. # Run an Erigon archive node Source: https://docs.polygon.technology/pos/how-to/erigon-archive-node Steps to install, configure, and start an Erigon archive node for Polygon Chain mainnet or Amoy testnet. ## System requirements * CPU: 16-core, 64-bit architecture * RAM: 64GB * Storage * Basically `io1` or above with at least 20k+ iops and RAID-0 based disk structure * Mainnet archive node: 15TB * Amoy testnet archive node: 1TB * SSD or NVMe. Bear in mind that SSD performance deteriorates when close to capacity. * Golang: >= v1.20 * GCC: >= v10 On HDDs, Erigon will always remain *N* blocks behind the chain tip, but will not fall further behind. ## Install Erigon client Run the following commands to install Erigon: ```bash theme={null} git clone --recurse-submodules -j8 https://github.com/ledgerwatch/erigon cd erigon git checkout v2.57.3 make erigon ``` This should create the binary at `./build/bin/erigon` ## Start Erigon client If you're deploying to mainnet, run the following command: ```bash theme={null} erigon --chain=bor-mainnet --db.size.limit=12TB --db.pagesize=16KB # remaining flags follow ``` When connecting to Amoy testnet, use the following command to start your Erigon client: ```bash theme={null} erigon --chain=amoy ``` ## Configure Erigon client If you want to store Erigon files in a non-default location, use `-datadir` to specify a new location: ```bash theme={null} erigon --chain=amoy --datadir= ``` If you are not using local **heimdall**, use `-bor.heimdall=`. By default, it will try to connect to `localhost:1317`. ```bash theme={null} erigon --chain=amoy --bor.heimdall= --datadir= ``` ## Node RPC * If you want to connect to PoS Amoy Testnet, use: [https://heimdall-api-amoy.polygon.technology](https://heimdall-api-amoy.polygon.technology) * For PoS mainnet, use: [https://heimdall-api.polygon.technology](https://heimdall-api.polygon.technology) Remote heimdall is better suited for testing, and is not recommended for production use. ## Tips for faster sync * Use the machine with high IOPS and RAM for the faster initial sync * Memory optimized nodes are recommended for faster sync. For example, AWS EC2 `r5` or `r6` series instances. ## Reporting issues In case you encounter any issues and are looking for support, please get in touch with the Erigon team. More details available in [the Erigon GitHub README](https://github.com/ledgerwatch/erigon?tab=readme-ov-file#getting-in-touch). # Using Ansible Source: https://docs.polygon.technology/pos/how-to/full-node/full-node-ansible Steps to deploy and manage a Polygon Chain full node using an Ansible playbook. This guide deploys a full Polygon Chain node using an [Ansible playbook](https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html). ## Prerequisites * Install Ansible on your local machine with Python3.x. The setup doesn't run on Python 2.x. * To install Ansible with Python 3.x, you can use pip. If you do not have pip on your machine, follow the steps outlined [here](https://pip.pypa.io/en/stable/). Run `pip3 install ansible` to install Ansible. * Check the [Polygon Chain Ansible repository](https://github.com/0xPolygon/node-ansible#requirements) for requirements. * You also need to ensure that Go is *not installed* in your environment. You will run into issues if you attempt to set up your full node through Ansible with Go installed as Ansible requires specific packages of Go. * You will also need to make sure that your VM / Machine does not have any previous setups for Polygon Validator or Heimdall or Bor. You will need to delete them as your setup will run into issues. ## Full node setup * Ensure you have access to the remote machine or VM on which the full node is being set up. > Refer to [https://github.com/0xPolygon/node-ansible](https://github.com/0xPolygon/node-ansible) for more details. * Clone the [https://github.com/0xPolygon/node-ansible](https://github.com/0xPolygon/node-ansible) repository. * Navigate into the node-ansible folder: `cd node-ansible` * Edit the `inventory.yml` file and insert your IP(s) in the `sentry->hosts` section. > Refer to [https://github.com/0xPolygon/node-ansible#inventory](https://github.com/0xPolygon/node-ansible#inventory) for more details. * Check if the remote machine is reachable by running: `ansible sentry -m ping` * To test if the correct machine is configured, run the following command: ```bash theme={null} # Mainnet: ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.0.0 heimdall_version=v1.0.3 network=mainnet node_type=sentry" --list-hosts # Testnet: ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.1.0 heimdall_version=v1.0.3 network=amoy node_type=sentry" --list-hosts ``` Figure: Full node testnet * Next, set up the full node with this command: ```bash theme={null} # Mainnet: ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.1.0 heimdall_version=v1.0.3 network=mainnet node_type=sentry" # Testnet: ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.0.0 heimdall_version=v1.0.3 network=amoy node_type=sentry" ``` * In case you run into any issues, delete and clean the whole setup using: ```bash theme={null} ansible-playbook playbooks/clean.yml ``` * Once you initiate the Ansible playbook, log in to the remote machine. * Please *ensure that the value of seeds and bootnodes mentioned [here](https://docs.polygon.technology/pos/reference/seed-and-bootnodes/) is the same value as mentioned in Heimdall and Bor `config.toml` files*. If not, change the values accordingly. * To check if Heimdall is synced * On the remote machine/VM, run `curl localhost:26657/status` * In the output, `catching_up` value should be `false` * Once Heimdall is synced, run: * `sudo service bor start` If you've reached this point, you have successfully set up a full node with Ansible. If Bor presents an error of permission to data, run this command to make the Bor user the owner of the Bor files: ```bash theme={null} sudo chown bor /var/lib/bor ``` ## Logs Logs can be managed by the `journalctl` linux tool. Here is a tutorial for advanced usage: [How To Use Journalctl to View and Manipulate Systemd Logs](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs). ### Check Heimdall node logs ```bash theme={null} journalctl -u heimdalld.service -f ``` ### Check Bor node logs ```bash theme={null} journalctl -u bor.service -f ``` # Using binaries Source: https://docs.polygon.technology/pos/how-to/full-node/full-node-binaries Steps to install Heimdall and Bor binaries, configure, and start a Polygon Chain full node. This guide walks through installing and starting a Polygon Chain full node using binaries. For system requirements, see the [prerequisites guide](/pos/how-to/prerequisites/). Steps in these guide involve waiting for the Heimdall and Bor services to fully sync. This process takes several days to complete. Please use snapshots for faster syncing without having to sync over the network. For detailed instructions, see [Sync node using snapshots](/pos/how-to/snapshots/). ## Overview It is essential to follow the outlined sequence of actions precisely, as any deviation may lead to potential issues. * Prepare the machine. * Install Heimdall and Bor binaries on the full node machine. * Set up Heimdall and Bor services on the full node machine. * Configure the full node machine. * Start the full node machine. * Check node health with the community. ### Install `build-essential` This is *required* for your full node. In order to install, run the below command: ```bash theme={null} sudo apt-get update sudo apt-get install build-essential ``` ## Install binaries Install both Heimdall and Bor in order. Both must be running to operate a full node. ### Heimdall Install the latest version of Heimdall and related services. Make sure you checkout to the correct [release version](https://github.com/0xPolygon/heimdall-v2/releases). To install *Heimdall*, run the following commands: ```bash theme={null} curl -L https://raw.githubusercontent.com/0xPolygon/install/heimdall-v2/heimdall-v2.sh | bash -s -- ``` You can run the above command with the following options: * `heimdall_version`: Valid v0.2+ release tag from [https://github.com/0xPolygon/heimdall-v2/releases](https://github.com/0xPolygon/heimdall-v2/releases) * `network_type`: `mainnet` and `amoy` * `node_type`: `sentry` This will install the `heimdalld` binary. Then, edit the configuration files under `/var/lib/heimdall/config`\ The templates for each supported network are available [here](https://github.com/0xPolygon/heimdall-v2/tree/develop/packaging/templates/config)\ Download the `genesis.json` file and place it under `/var/lib/heimdall/config/` Use the following commands based on your target network: ```bash theme={null} cd /var/lib/heimdall/config curl -fsSL -o genesis.json ``` Where `BUCKET_URL` is * [https://storage.googleapis.com/amoy-heimdallv2-genesis/migrated\_dump-genesis.json](https://storage.googleapis.com/amoy-heimdallv2-genesis/migrated_dump-genesis.json) for amoy * [https://storage.googleapis.com/mainnet-heimdallv2-genesis/migrated\_dump-genesis.json](https://storage.googleapis.com/mainnet-heimdallv2-genesis/migrated_dump-genesis.json) for mainnet Verify the installation by checking the Heimdall version on your machine: ```bash theme={null} heimdalld version ``` It should return the version of Heimdall you installed. ### Bor Install the latest version of Bor, based on valid v2.0+ [released version](https://github.com/0xPolygon/bor/releases). ```bash theme={null} curl -L https://raw.githubusercontent.com/0xPolygon/install/main/bor.sh | bash -s -- ``` You can run the above command with following options: * `bor_version`: valid v2.0+ release tag from [https://github.com/0xPolygon/bor/releases](https://github.com/0xPolygon/bor/releases) * `network_type`: `mainnet` and `amoy` * `node_type`: `sentry` That will install the `bor` binary. Verify the installation by checking the Bor version on your machine: ```bash theme={null} bor version ``` ### Configure Heimdall and Bor seeds The latest bor and heimdall seeds can be found [here](https://docs.polygon.technology/pos/reference/seed-and-bootnodes/). To configure them, update the following lines: * If not done previously, set the `seeds` and `persistent_peers` values in `/var/lib/heimdall/config/config.toml` * Set the `bootnodes` in `/var/lib/bor/config.toml` This will ensure your node connects to the peers. ### (Optional) Start Heimdall from snapshot In case you want to start Heimdall from a snapshot,\ you can download it, and extract in the `data` folder. Examples of snapshots can be found here [https://all4nodes.io/Polygon](https://all4nodes.io/Polygon), and they are managed by the community. e.g.: ```bash theme={null} lz4 -dc polygon-heimdall-24404501-25758577.tar.lz4 | tar -x ``` ### Update service config user permission ```bash theme={null} sed -i 's/User=heimdall/User=root/g' /lib/systemd/system/heimdalld.service sed -i 's/User=bor/User=root/g' /lib/systemd/system/bor.service ``` ## Start services Run the full Heimdall node with these commands on your Sentry Node: ```bash theme={null} sudo service heimdalld start ``` Ensure that Heimdall is fully synced before starting Bor. Initiating Bor without complete synchronization of Heimdall may lead to frequent issues. To check if Heimdall is synced: 1. On the remote machine/VM, run `curl localhost:26657/status`. 2. In the output, `catching_up` value should be `false`. Once Heimdall is synced, run the following command: ```bash theme={null} sudo service bor start ``` ## Logs Logs can be managed by the `journalctl` linux tool. Here is a tutorial for advanced usage: [How To Use Journalctl to View and Manipulate Systemd Logs](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs). ### Check Heimdall node logs ```bash theme={null} journalctl -u heimdalld.service -f ``` ### Check Bor node logs ```bash theme={null} journalctl -u bor.service -f ``` # Using Docker Source: https://docs.polygon.technology/pos/how-to/full-node/full-node-docker Steps to run a Polygon Chain full node using official Docker images, including Heimdall and Bor setup on Linux. This guide runs a full Polygon Chain node using official Docker images. The same steps can be adapted for sentry nodes and validators. ## Initial setup To get started, you'll need to have shell access with root privileges to a linux machine. img ### Install Docker It is likely that your operating system won’t have Docker installed by default. Please follow the instructions for your particular distribution found here: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) We’re following the instructions for Ubuntu. The steps are included below, but please refer to the official instructions in case they’ve been updated. ```bash theme={null} sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin ``` At this point you should have Docker installed. In order to verify, you should be able to run the following command successfully: ```bash theme={null} sudo docker run hello-world ``` img In many cases, it’s inconvenient to run docker as `root` user so we’ll follow the post install steps [here](https://docs.docker.com/engine/install/linux-postinstall/) in order to interact with docker without needing to be `root`: ```bash theme={null} sudo groupadd docker sudo usermod -aG docker $USER ``` Now you should be able to logout and log back in, and run docker commands without `sudo`. ### Disk setup The exact steps required here may vary a lot based on your needs. Most likely you’ll have a root partition running your operating system on one device. You’ll probably want one or more devices for actually holding the blockchain data. For the rest of the walkthrough, we’re going to have that additional device mounted at `/mnt/data`. In this example, we have a device with 4 TB of available space located at `/dev/nvme1n1`. We are going to mount that using the steps below: ```bash theme={null} sudo mkdir /mnt/data sudo mount /dev/nvme1n1 /mnt/data ``` We use `df -h` to make sure the mount looks good. img Once we've verified that successfully, we might as well create the home directories on this mount for Bor and Heimdall. ```bash theme={null} sudo mkdir /mnt/data/bor sudo mkdir /mnt/data/heimdall ``` Depending on your use case and operating system, you’ll likely want to create an entry in `/etc/fstab` in order to make sure your device is mounted when the system reboots. In our case we're following some steps like this: ```bash theme={null} # Use blkid to get the UUID for the device that we're mounting blkid # Edit the fstab file and add a line to mount your device # UUID={your uuid} /mnt/data {your filesystem} defaults 0 1 sudo emacs /etc/fstab # use this to verify the fstab actually works sudo findmnt --verify --verbose ``` At this point you should be able to reboot and confirm that the system loads your mount properly. ### Heimdall setup At this point, we have a host with docker running on it and we have ample mounted storage to run our Polygon node software. So let’s get Heimdall configured and running. First let’s make sure we can run Heimdall with docker. Run the following command: ```bash theme={null} docker run -it 0xpolygon/heimdall-v2:0.2.16 heimdalld version ``` If this is the first time you’ve run Heimdall with docker, it should pull the required image automatically and output the version information. img If you’d like to check the details of the Heimdall image or find a different tag, you can take a look at the repository on Docker Hub: [https://hub.docker.com/repository/docker/0xpolygon/heimdall-v2](https://hub.docker.com/repository/docker/0xpolygon/heimdall-v2) At this point, let’s run the Heimdall `init` command to set up our home directory. Set `--chain-id` to `heimdallv2-80002` for Amoy or `heimdallv2-137` for mainnet. ```bash theme={null} docker run -v /mnt/data/heimdall:/heimdall-home:rw --entrypoint /usr/bin/heimdalld -it 0xpolygon/heimdall-v2:0.2.16 init test-moniker --chain-id= --home=/heimdall-home ``` After running the `init` command, your `/mnt/data/heimdall` directory should have some structure. Now we need to make a few updates before starting Heimdall. Download the genesis file and place it under `HEIMDALL_HOME/config/genesis.json` (replacing the one potentially already present there) ```bash theme={null} curl -L -o "/config/genesis.json" ``` where `BUCKET_URL` is [https://storage.googleapis.com/amoy-heimdallv2-genesis/migrated\_dump-genesis.json](https://storage.googleapis.com/amoy-heimdallv2-genesis/migrated_dump-genesis.json) for amoyand [https://storage.googleapis.com/mainnet-heimdallv2-genesis/migrated\_dump-genesis.json](https://storage.googleapis.com/mainnet-heimdallv2-genesis/migrated_dump-genesis.json) for mainnet Replace `HEIMDALL_HOME` with the actual path to your Heimdall home directory. Please note that the genesis file size is around 50MB for amoy and 3GB for mainnet.\ Hence, the download might take a while, and it’s recommended to use a stable and fast connection. Then, you can customize the configs under `HEIMDALL_HOME/config` (`app.toml`, `client.toml`, `config.toml`), based on your setup. Templates for each supported network are available [here](https://github.com/0xPolygon/heimdall-v2/tree/develop/packaging/templates/config) Make sure to configure your Ethereum and Bor connection parameters based on your infrastructure. ## (Optional) Start Heimdall from snapshot In case you want to start Heimdall from a snapshot,\ you can download it, and extract in the `data` folder. Examples of snapshots can be found here [https://all4nodes.io/Polygon](https://all4nodes.io/Polygon), and they are managed by the community. e.g.: ```bash theme={null} lz4 -dc polygon-heimdall-24404501-25758577.tar.lz4 | tar -x ``` ## Starting Heimdall Before we start Heimdall, we’re going to create a docker network so that the containers can easily network with each other based on names.\ In order to create the network, run the following command: ```bash theme={null} docker network create polygon ``` Now we’re going to start Heimdall. Run the following command: ```bash theme={null} docker run -p 26657:26657 -p 26656:26656 -v /mnt/data/heimdall:/heimdall-home:rw --net polygon --name heimdall --entrypoint /usr/bin/heimdalld -d --restart unless-stopped 0xpolygon/heimdall-v2:0.2.16 start --home=/heimdall-home ``` Check container status and logs: ```bash theme={null} # ps will list the running docker processes. At this point you should see one container running docker ps # This command will print out the logs directly from the heimdall application docker logs -ft heimdall ``` At this point, Heimdall should start syncing. When you look at the logs, you should see a log of information being spit out that looks like this: ``` 2022-12-14T19:43:23.687640820Z INFO [2022-12-14|19:43:23.687] Executed block module=state height=26079 validTxs=0 invalidTxs=0 2022-12-14T19:43:23.721220869Z INFO [2022-12-14|19:43:23.721] Committed state module=state height=26079 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F 2022-12-14T19:43:23.730533414Z INFO [2022-12-14|19:43:23.730] Executed block module=state height=26080 validTxs=0 invalidTxs=0 2022-12-14T19:43:23.756646938Z INFO [2022-12-14|19:43:23.756] Committed state module=state height=26080 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F 2022-12-14T19:43:23.768129711Z INFO [2022-12-14|19:43:23.767] Executed block module=state height=26081 validTxs=0 invalidTxs=0 2022-12-14T19:43:23.794323918Z INFO [2022-12-14|19:43:23.794] Committed state module=state height=26081 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F 2022-12-14T19:43:23.802989809Z INFO [2022-12-14|19:43:23.802] Executed block module=state height=26082 validTxs=0 invalidTxs=0 2022-12-14T19:43:23.830960386Z INFO [2022-12-14|19:43:23.830] Committed state module=state height=26082 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F 2022-12-14T19:43:23.840941976Z INFO [2022-12-14|19:43:23.840] Executed block module=state height=26083 validTxs=0 invalidTxs=0 2022-12-14T19:43:23.866564767Z INFO [2022-12-14|19:43:23.866] Committed state module=state height=26083 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F 2022-12-14T19:43:23.875395744Z INFO [2022-12-14|19:43:23.875] Executed block module=state height=26084 validTxs=0 invalidTxs=0 ``` If you’re not seeing any information like this, your node might not be finding enough peers. The other useful command at this point is an RPC call to check the status of Heimdall syncing: ```bash theme={null} curl localhost:26657/status ``` This will return a response like: ```json theme={null} { "latest_block_hash":"6217E7BDAABE4DF58F8FCA2AAF0BD41BD93A96983F533DA7A0034E514D15BC5B", "latest_app_hash":"BAA894A82E756A2797E3F608F6F7EED295549B8F7FB621E31542D206F5CA740C", "latest_block_height":26191190, "latest_block_time":"2025-08-06T12:50:35.062210407Z", "earliest_block_hash":"14C55F6E824DD1C1D3D9AB424D0A983953F6C2BCA0C9ED692AE2C43498D870CD", "earliest_app_hash":"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", "earliest_block_height":24404501, "earliest_block_time":"2025-07-10T15:20:00Z", "catching_up":false } ``` In this initial setup phase, it’s important to pay attention to the `catching_up` field.\ If `catching_up` is true, it means that Heimdall is not fully synced. ## Starting Bor At this point, you should have a node that’s successfully running Heimdall. You should be ready now to run Bor. Before we get started with Bor, we need to run the Heimdall rest server. This command will start a REST API that Bor uses to retrieve information from Heimdall. The command to start the server is: ```bash theme={null} docker run -p 1317:1317 -v /mnt/data/heimdall:/heimdall-home:rw --net polygon --name heimdallrest --entrypoint /usr/bin/heimdalld -d --restart unless-stopped 0xpolygon/heimdall:1.0.3 rest-server --home=/heimdall-home --node "tcp://heimdall:26657" ``` If this command runs successfully, when you run `docker ps`, you should see two containers running. Verify the REST server is responding: ```bash theme={null} curl localhost:1317/bor/span/1 ``` Bor will rely on this interface. So if you don’t see JSON output, there is something wrong! Now let’s download the `genesis` file for Bor specifically: ```bash theme={null} sudo curl -o /mnt/data/bor/genesis.json 'https://raw.githubusercontent.com/0xPolygon/bor/master/builder/files/genesis-mainnet-v1.json' ``` Let’s verify the `sha256 sum` again for this file: ``` # sha256sum genesis.json 4bacbfbe72f0d966412bb2c19b093f34c0a1bd4bb8506629eba1c9ca8c69c778 genesis.json ``` Generate a default Bor config file, then edit it. Only the changed lines are shown below. Full image details: [https://hub.docker.com/repository/docker/0xpolygon/bor](https://hub.docker.com/repository/docker/0xpolygon/bor). ```bash theme={null} docker run -it 0xpolygon/bor:1.1.0 dumpconfig | sudo tee /mnt/data/bor/config.toml ``` ```bash theme={null} # Similar to moniker, you might want to update this with a name of your own choosing identity = "docker.example" # Setting this to the location of a mount that we'll make datadir = "/bor-home" # We'll want to specify some boot nodes [p2p] [pep.discovery] bootnodes = ["enode://0cb82b395094ee4a2915e9714894627de9ed8498fb881cec6db7c65e8b9a5bd7f2f25cc84e71e89d0947e51c76e85d0847de848c7782b13c0255247a6758178c@44.232.55.71:30303", "enode://88116f4295f5a31538ae409e4d44ad40d22e44ee9342869e7d68bdec55b0f83c1530355ce8b41fbec0928a7d75a5745d528450d30aec92066ab6ba1ee351d710@159.203.9.164:30303"] # Because we're running inside docker, we'll likely need to change the way we connect to heimdall [heimdall] url = "http://heimdallrest:1317" # Assuming you want to access the RPC, you'll need to make a change here as well [jsonrpc] [jsonrpc.http] enabled = true host = "0.0.0.0" ``` At this point, we should be ready to start Bor. We’re going to use this command: ```bash theme={null} docker run -p 30303:30303 -p 8545:8545 -v /mnt/data/bor:/bor-home:rw --net polygon --name bor -d --restart unless-stopped 0xpolygon/bor:1.1.0 server --config /bor-home/config.toml ``` If everything goes well, you should see log entries that look like this: ```bash theme={null} 2022-12-14T19:53:51.989897291Z INFO [12-14|19:53:51.989] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:47:46Z 2022-12-14T19:53:51.989925064Z INFO [12-14|19:53:51.989] Fetching state sync events queryParams="from-id=4&to-time=1590882466&limit=50" 2022-12-14T19:53:51.997640841Z INFO [12-14|19:53:51.997] StateSyncData Gas=0 Block-number=12800 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.021990622Z INFO [12-14|19:53:52.021] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:49:58Z 2022-12-14T19:53:52.022015930Z INFO [12-14|19:53:52.021] Fetching state sync events queryParams="from-id=4&to-time=1590882598&limit=50" 2022-12-14T19:53:52.040660857Z INFO [12-14|19:53:52.040] StateSyncData Gas=0 Block-number=12864 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.064795784Z INFO [12-14|19:53:52.064] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:52:10Z 2022-12-14T19:53:52.064828634Z INFO [12-14|19:53:52.064] Fetching state sync events queryParams="from-id=4&to-time=1590882730&limit=50" 2022-12-14T19:53:52.085029612Z INFO [12-14|19:53:52.084] StateSyncData Gas=0 Block-number=12928 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.132067703Z INFO [12-14|19:53:52.131] ✅ Committing new span id=3 startBlock=13056 endBlock=19455 validatorBytes=f8b6d906822710940375b2fc7140977c9c76d45421564e354ed42277d9078227109442eefcda06ead475cde3731b8eb138e88cd0bac3d9018238a2945973918275c01f50555d44e92c9d9b353cadad54d905822710947fcd58c2d53d980b247f1612fdba93e9a76193e6d90482271094b702f1c9154ac9c08da247a8e30ee6f2f3373f41d90282271094b8bb158b93c94ed35c1970d610d1e2b34e26652cd90382271094f84c74dea96df0ec22e11e7c33996c73fcc2d822 producerBytes=f8b6d906822710940375b2fc7140977c9c76d45421564e354ed42277d9078227109442eefcda06ead475cde3731b8eb138e88cd0bac3d9018238a2945973918275c01f50555d44e92c9d9b353cadad54d905822710947fcd58c2d53d980b247f1612fdba93e9a76193e6d90482271094b702f1c9154ac9c08da247a8e30ee6f2f3373f41d90282271094b8bb158b93c94ed35c1970d610d1e2b34e26652cd90382271094f84c74dea96df0ec22e11e7c33996c73fcc2d822 2022-12-14T19:53:52.133545235Z INFO [12-14|19:53:52.133] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:54:22Z 2022-12-14T19:53:52.133578948Z INFO [12-14|19:53:52.133] Fetching state sync events queryParams="from-id=4&to-time=1590882862&limit=50" 2022-12-14T19:53:52.135049605Z INFO [12-14|19:53:52.134] StateSyncData Gas=0 Block-number=12992 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.152067646Z INFO [12-14|19:53:52.151] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:56:34Z 2022-12-14T19:53:52.152198357Z INFO [12-14|19:53:52.151] Fetching state sync events queryParams="from-id=4&to-time=1590882994&limit=50" 2022-12-14T19:53:52.176617455Z INFO [12-14|19:53:52.176] StateSyncData Gas=0 Block-number=13056 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.191060112Z INFO [12-14|19:53:52.190] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:58:46Z 2022-12-14T19:53:52.191083740Z INFO [12-14|19:53:52.190] Fetching state sync events queryParams="from-id=4&to-time=1590883126&limit=50" 2022-12-14T19:53:52.223836639Z INFO [12-14|19:53:52.223] StateSyncData Gas=0 Block-number=13120 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.236025906Z INFO [12-14|19:53:52.235] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:00:58Z 2022-12-14T19:53:52.236053406Z INFO [12-14|19:53:52.235] Fetching state sync events queryParams="from-id=4&to-time=1590883258&limit=50" 2022-12-14T19:53:52.269611566Z INFO [12-14|19:53:52.269] StateSyncData Gas=0 Block-number=13184 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.283199351Z INFO [12-14|19:53:52.283] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:03:10Z 2022-12-14T19:53:52.283737573Z INFO [12-14|19:53:52.283] Fetching state sync events queryParams="from-id=4&to-time=1590883390&limit=50" 2022-12-14T19:53:52.314141359Z INFO [12-14|19:53:52.314] StateSyncData Gas=0 Block-number=13248 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.325150782Z INFO [12-14|19:53:52.325] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:05:22Z 2022-12-14T19:53:52.325171075Z INFO [12-14|19:53:52.325] Fetching state sync events queryParams="from-id=4&to-time=1590883522&limit=50" 2022-12-14T19:53:52.354470271Z INFO [12-14|19:53:52.354] StateSyncData Gas=0 Block-number=13312 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.372354857Z INFO [12-14|19:53:52.372] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:07:34Z 2022-12-14T19:53:52.372389214Z INFO [12-14|19:53:52.372] Fetching state sync events queryParams="from-id=4&to-time=1590883654&limit=50" 2022-12-14T19:53:52.398246950Z INFO [12-14|19:53:52.398] StateSyncData Gas=0 Block-number=13376 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.413321099Z INFO [12-14|19:53:52.413] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:09:46Z 2022-12-14T19:53:52.413345355Z INFO [12-14|19:53:52.413] Fetching state sync events queryParams="from-id=4&to-time=1590883786&limit=50" 2022-12-14T19:53:52.437176855Z INFO [12-14|19:53:52.437] StateSyncData Gas=0 Block-number=13440 LastStateID=3 TotalRecords=0 2022-12-14T19:53:52.450356966Z INFO [12-14|19:53:52.450] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:11:58Z ``` There are a few ways to check the sync state of Bor. The simplest is using `curl`: ```bash theme={null} curl 'localhost:8545/' \ --header 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"eth_syncing", "params":[], "id":1 }' ``` When you run this command, you'll see an output like this: ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "currentBlock": "0x2eebf", "healedBytecodeBytes": "0x0", "healedBytecodes": "0x0", "healedTrienodeBytes": "0x0", "healedTrienodes": "0x0", "healingBytecode": "0x0", "healingTrienodes": "0x0", "highestBlock": "0x1d4ee3e", "startingBlock": "0x0", "syncedAccountBytes": "0x0", "syncedAccounts": "0x0", "syncedBytecodeBytes": "0x0", "syncedBytecodes": "0x0", "syncedStorage": "0x0", "syncedStorageBytes": "0x0" } } ``` This will indicate the `currentBlock` that’s been synced and also the `highestBlock` that we’re aware of. If the node is already synced, we should get `false`. ## Seeds and Bootnodes The latest bor and heimdall seeds can be found [here](https://docs.polygon.technology/pos/reference/seed-and-bootnodes/). Using them will ensure that bor/heimdall is able to connect to peers quickly. # Using GCP Source: https://docs.polygon.technology/pos/how-to/full-node/full-node-gcp Steps to deploy a Polygon Chain node in a Google Cloud Platform VM instance using the gcloud CLI. This guide deploys a Polygon Chain node in a GCP VM instance. The examples use Ubuntu 20.04, though any modern Debian or Ubuntu LTS release (e.g., Debian 11) works. This setup is currently only supported for mainnet. GCP support for deploying Amoy testnet nodes will be available soon. ## Deploy VM instance You may use any of the following ways to create an instance in Google Cloud: 1. Google Cloud CLI, local or [Cloud Shell](https://cloud.google.com/shell) 2. Web Console We only cover the first case in this guide. Let's start with deployment using Google Cloud CLI. 1. Follow ["Before you begin" section](https://cloud.google.com/compute/docs/instances/create-start-instance#before-you-begin) to install and configure gcloud command-line tool. Pay attention to default region and zone, choose ones closer to you or your customers. You may use [gcping.com](https://gcping.com) to measure latency to choose the closest location. 2. Adjust the following command variables using your favorite editor prior to executing, when required: * `POLYGON_NETWORK` - choose `mainnet` network to run. * `POLYGON_NODETYPE` - choose `archive`,`fullnode` node type to run. * `POLYGON_BOOTSTRAP_MODE` - choose bootstrap mode `snapshot` or `from_scratch`. * `POLYGON_RPC_PORT` - choose JSON RPC bor node port to listen on, the default value is what used on VM instance creation and in firewall rules. * `EXTRA_VAR` - choose Bor and Heimdall branches, use `network_version=mainnet-v1` with `mainnet` network and `network_version=testnet-v4` with `amoy` network. * `INSTANCE_NAME` - the name of a VM instance with Polygon we are going to create. * `INSTANCE_TYPE` - GCP [machine type](https://cloud.google.com/compute/docs/machine-types), default value is recommended, You may change it later if required. * `BOR_EXT_DISK_SIZE` - additional disk size in GB to use with Bor, default value with `fullnode` is recommended, You may expand it later if required. You'll need 8192GB+ with `archive` node though. * `HEIMDALL_EXT_DISK_SIZE` - additional disk size in GB to use with Heimdall, default value is recommended. * `DISK_TYPE` - GCP [disk type](https://cloud.google.com/compute/docs/disks#disk-types), SSD is highly recommended. You may need to increase the total SSD GB quota in the region you are spinning up the node. 3. Use the following command to create an instance with the correct hardware and software requirements. In the example below, we deploy Polygon Chain `mainnet` from `snapshot` in the `fullnode` mode: ```bash theme={null} export POLYGON_NETWORK=mainnet export POLYGON_NODETYPE=fullnode export POLYGON_BOOTSTRAP_MODE=snapshot export POLYGON_RPC_PORT=8747 export GCP_NETWORK_TAG=polygon export EXTRA_VAR=(bor_branch=v1.1.0 heimdall_branch=v1.0.3 network_version=mainnet-v1 node_type=sentry/sentry heimdall_network=${POLYGON_NETWORK}) gcloud compute firewall-rules create "polygon-p2p" --allow=tcp:26656,tcp:30303,udp:30303 --description="polygon p2p" --target-tags=${GCP_NETWORK_TAG} gcloud compute firewall-rules create "polygon-rpc" --allow=tcp:${POLYGON_RPC_PORT} --description="polygon rpc" --target-tags=${GCP_NETWORK_TAG} export INSTANCE_NAME=polygon-0 export INSTANCE_TYPE=e2-standard-8 export BOR_EXT_DISK_SIZE=1024 export HEIMDALL_EXT_DISK_SIZE=500 export DISK_TYPE=pd-ssd gcloud compute instances create ${INSTANCE_NAME} \ --image-project=ubuntu-os-cloud \ --image-family=ubuntu-2004-lts \ --boot-disk-size=20 \ --boot-disk-type=${DISK_TYPE} \ --machine-type=${INSTANCE_TYPE} \ --create-disk=name=${INSTANCE_NAME}-bor,size=${BOR_EXT_DISK_SIZE},type=${DISK_TYPE},auto-delete=no \ --create-disk=name=${INSTANCE_NAME}-heimdall,size=${HEIMDALL_EXT_DISK_SIZE},type=${DISK_TYPE},auto-delete=no \ --tags=${GCP_NETWORK_TAG} \ --metadata=user-data=' #cloud-config bootcmd: - screen -dmS polygon su -l -c bash -c "curl -L https://raw.githubusercontent.com/0xPolygon/node-ansible/master/install-gcp.sh | bash -s -- -n '${POLYGON_NETWORK}' -m '${POLYGON_NODETYPE}' -s '${POLYGON_BOOTSTRAP_MODE}' -p '${POLYGON_RPC_PORT}' -e \"'${EXTRA_VAR}'\"; bash"' ``` The instance should be created and live in a couple of minutes. ## Login to VM It will take a couple of minutes to install all the required software, and a couple of hours to download a snapshot when chosen. * You should see working `bor` and `heimdalld` processes filling up additional drives. You may run the following commands to check it. ```bash theme={null} gcloud compute ssh ${INSTANCE_NAME} # inside the connected session sudo su - ps uax|egrep "bor|heimdalld" df -l -h ``` * You may use the following command to watch the installation progress, it's really handy in case of `snapshot` bootstrap: ```bash theme={null} # inside the connected session screen -dr ``` Use `Control+a d` key combination to disconnect from progress review. * You may use the following commands to get Bor and Heimdall logs: ```bash theme={null} # inside the connected session journalctl -fu bor journalctl -fu heimdalld ``` Blockchain data is saved onto additional drives which are kept by default on VM instance removal. You need to remove additional disks manually if you don't need this data anymore. At the end, you will get an instance as shown in the diagram below. Figure: Mainnet - Polygon instance # Using packages Source: https://docs.polygon.technology/pos/how-to/full-node/full-node-packages Steps to install Heimdall and Bor packages, configure, and start a Polygon Chain full node. ## Overview * Prepare the Full Node machine. * Install Heimdall and Bor packages on the Full Node machine. * Configure the Full node. * Start the Full node. It is essential to follow the outlined sequence of actions precisely, as any deviation may lead to potential issues. ## Install packages ### Prerequisites * One machine is needed. * Bash is installed on the machine. ### Heimdall * Install the default latest version of sentry for Mainnet: ```shell theme={null} curl -L https://raw.githubusercontent.com/0xPolygon/install/heimdall-v2/heimdall-v2.sh | bash -s -- ``` or install a specific version, node type (`sentry` or `validator`), and network (`mainnet` or `amoy`). All release versions can be found on [Heimdall GitHub repository](https://github.com/0xPolygon/heimdall-v2/releases). ```shell theme={null} # Example: # curl -L https://raw.githubusercontent.com/maticnetwork/install/heimdall-v2/heimdall-v2.sh | bash -s -- v0.2.15 mainnet validator ``` ### Bor * Install the default latest version of sentry for Mainnet: ```shell theme={null} curl -L https://raw.githubusercontent.com/0xPolygon/install/main/bor.sh | bash -s -- ``` or install a specific version, node type (`sentry` or `validator`), and network (`mainnet` or `amoy`). All release versions could be found on [Bor Github repository](https://github.com/0xPolygon/bor/releases). ```shell theme={null} # Example: # curl -L https://raw.githubusercontent.com/0xPolygon/install/main/bor.sh | bash -s -- v2.2.9 mainnet sentry ``` ## Configuration ### Configure Heimdall * Initialize Heimdall configs ```shell theme={null} # For mainnet sudo -u heimdall heimdalld init --chain-id= --home /var/lib/heimdall ``` Where `CHAIN_ID` is `heimdallv2-80002` for `amoy` and `heimdallv2-137` for `mainnet` Then, edit the configuration files under `/var/lib/heimdall/config`\ The templates for each supported network are available [here](https://github.com/0xPolygon/heimdall-v2/tree/develop/packaging/templates/config)\ Download the `genesis.json` file and place it under `/var/lib/heimdall/config/` Use the following commands based on your target network: ```bash theme={null} cd /var/lib/heimdall/config curl -fsSL -o genesis.json ``` Where `BUCKET_URL` is * [https://storage.googleapis.com/amoy-heimdallv2-genesis/migrated\_dump-genesis.json](https://storage.googleapis.com/amoy-heimdallv2-genesis/migrated_dump-genesis.json) for amoy * [https://storage.googleapis.com/mainnet-heimdallv2-genesis/migrated\_dump-genesis.json](https://storage.googleapis.com/mainnet-heimdallv2-genesis/migrated_dump-genesis.json) for mainne * You will need to change a few details in the config files. * Templates for each supported network are available [here](https://github.com/0xPolygon/heimdall-v2/tree/develop/packaging/templates/config) ### Configure service files for Bor and Heimdall After successfully installing Bor and Heimdall through [packages](#install-packages), their service file could be found under `/lib/systemd/system`, and Bor's config file could be found under `/var/lib/bor/config.toml`. You will need to check and modify these files accordingly. * Make sure the chain is set correctly in `/lib/systemd/system/heimdalld.service` file. Open the file with following command `sudo vi /lib/systemd/system/heimdalld.service` * In the service file, set `--chain` to `mainnet` or `amoy` accordingly Save the changes in `/lib/systemd/system/heimdalld.service`. * Make sure the chain is set correctly in `/var/lib/bor/config.toml` file. Open the file with following command `sudo vi /var/lib/bor/config.toml` * In the config file, set `chain` to `mainnet` or `amoy` accordingly. * To enable Archive mode you can optionally enable the following flags: ```js theme={null} gcmode "archive" [jsonrpc] [jsonrpc.ws] enabled = true port = 8546 corsdomain = ["*"] ``` Save the changes in `/var/lib/bor/config.toml`. ## (Optional) Start Heimdall from snapshot In case you want to start Heimdall from a snapshot,\ you can download it, and extract in the `data` folder. Examples of snapshots can be found here [https://all4nodes.io/Polygon](https://all4nodes.io/Polygon), and they are managed by the community. e.g.: ```bash theme={null} lz4 -dc polygon-heimdall-24404501-25758577.tar.lz4 | tar -x ``` ## Start services Reloading service files to make sure all changes to service files are loaded correctly. ```shell theme={null} sudo systemctl daemon-reload ``` Verify the installation by checking the Heimdall version on your machine: ```bash theme={null} heimdalld version ``` It should return the version of Heimdall you installed. Start Heimdall, Heimdall rest server, and Heimdall bridge. ```shell theme={null} sudo service heimdalld start ``` You can also check Heimdall logs with the following command: ```shell theme={null} journalctl -u heimdalld.service -f ``` At this point, please make sure that *Heimdall is synced completely*, and only then start Bor. If you start Bor without Heimdall syncing completely, you will run into issues frequently. To check if Heimdall is synced: * On the remote machine/VM, run `curl localhost:26657/status` * In the output, `catching_up` value should be `false` Now, once Heimdall is synced, run: ```shell theme={null} sudo service bor start ``` You can check Bor logs using the following command: ```shell theme={null} journalctl -u bor.service -f ``` # Change owner and signer address Source: https://docs.polygon.technology/pos/how-to/operate-validator-node/change-signer-address Steps to transfer your validator NFT to a new owner address or update the signer address for your Polygon Chain validator node. Steps to change the owner or signer address on your validator node. ## Change the owner address 1. Access StakingNFT smart contract. * Mainnet: [https://etherscan.io/address/0x47Cbe25BbDB40a774cC37E1dA92d10C2C7Ec897F#writeContract](https://etherscan.io/address/0x47Cbe25BbDB40a774cC37E1dA92d10C2C7Ec897F#writeContract) * Amoy: [https://sepolia.etherscan.io/address/0x72CF5618142Eb369E75ec6529A907e9A6Fe99bB7#writeContract](https://sepolia.etherscan.io/address/0x72CF5618142Eb369E75ec6529A907e9A6Fe99bB7#writeContract) 2. Click on the **Connect to Web3** button and login using the owner address of your validator node.
change-owner-1
3. You will see a list of functions appear. Click on **`safeTransferFrom`** function. This will be 5th function in the list. There will be 3 data fields that you will need to add information.
change-owner-2
4. Here: * **from (address)** is your current owner address * **to (address**) is your new owner Address * **tokenId** is your validator ID 5. Fill in the relevant information and select **Write**. You will be prompted to sign a transaction. Ensure that you have sufficient ETH to make the transaction. 6. Upon signing the transaction your validator NFT will be transferred to the new owner address. 7. Log in to the [staking dashboard](https://staking.polygon.technology/) with the new owner address to verify the changes. ## Change the signer address This guide refers to your current validator node as Node 1 and your new validator node as Node 2. 1. Log in to the [staking dashboard](https://staking.polygon.technology/) with the Node 1 address. 2. On your profile, select **Edit Profile**. 3. In the **Signer's address** field, enter the Node 2 address. 4. In the **Signer's public key** field, enter the Node 2 public key. To get the public key, run the following command on the validator node: ```sh theme={null} heimdalld show-account ``` Selecting **Save** will save your new details for your node. This essentially means that Node 1 will be your address that controls the stake, where the rewards will be sent to, etc. And Node 2 will now be performing activities like signing blocks, signing checkpoints, etc. Alternatively, the signer address can be updated via contract using the following process: 1. Access StakeManagerProxy smart contract. * Mainnet: [https://etherscan.io/address/0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908#writeProxyContract](https://etherscan.io/address/0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908#writeProxyContract) * Amoy: [https://sepolia.etherscan.io/address/0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908#writeProxyContract](https://sepolia.etherscan.io/address/0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908#writeProxyContract) 2. Click on the **Connect to Web3** button and login using the owner address of your validator node.
change-owner-1
3. You will see a list of functions appear. Click on **`updateSigner`** function. This will be 47th function in the list. There will be 2 data fields that you will need to add information. 4. Here: * **validatorId** is the associated ID with your validator * **signerPubkey** is the signer pubkey for your new signer address. To get the public key, run the following command on the validator node: ```sh theme={null} heimdalld show-account ``` Please note that the first 2 characters after `0x` should be removed when you enter the new signer pubkey in the contract. 5. Fill in the relevant information and select **Write**. You will be prompted to sign a transaction. Ensure that you have sufficient ETH to make the transaction. # Next steps Source: https://docs.polygon.technology/pos/how-to/operate-validator-node/next-steps Steps to stake tokens, set commission, claim rewards, and complete onboarding after your validator node joins the Polygon Chain active set. There is limited space for accepting new validators. New validators can only join the active set when an already active validator unbonds. Check out the following links for more information and to apply for a validator slot: * [Admission form](https://polygoncommunity.typeform.com/validatorshub?typeform) * [Admission dashboard](https://play.validatrium.club/public-dashboards/1b29d3bbdcd14007a0858b68dee76bdd?orgId=1) Once your validator node is onboarded into the active set, do the following: * Log in to the [staking dashboard](https://staking.polygon.technology/) with the owner address. * Go to my account, and click on edit details below the validator name. * Click on Profile Details and update your **name**, **website**, **description**, **logo URL**, and click on **Save Profile Details**. ## Stake tokens ### Initial staking 1. Access the [validator dashboard](https://staking.polygon.technology/validators/). 2. Log in with your wallet. You can use a popular wallet such as MetaMask. Make sure you login using the owner address, and that you have POL tokens in the wallet. 3. Select **Become a Validator**. You will be asked to set up your node. If you haven't already set up your node by now, you will need to do so, else if you proceed ahead you will receive an error when you attempt to stake. 4. On the next screen, add your validator details, the commission rate, and the staking amount. 5. Select **Stake Now**. 6. Now, you'll be prompted for three confirmations to send the transaction. Once complete, your POL tokens will be added to the staked amount on the validator node. The three confirmations include: * Approve Transaction: This approves your stake transaction. * Stake: Confirms your stake transaction. * Save: Saves your validator details. For the changes to take effect on the [staking dashboard](https://staking.polygon.technology/account), it requires a *minimum of 12 block confirmations*. ### Add stake 1. Access the [validator dashboard](https://staking.polygon.technology/validators/). 2. Log in with your wallet. You can use a popular wallet such as MetaMask. Make sure you login using the owner address, and that you have POL tokens in the wallet. 3. Select **Add more Stake**. 4. Enter the amount, and select **Add More Stake**. 5. Now, you'll be prompted for three confirmations to send the transaction. Once complete, your POL tokens will be added to your staked amount on the validator node. The three confirmations include: * Approve Transaction: This approves your stake transaction. * Stake: Confirms your stake transaction. * Save: Saves your validator details. For the changes to take effect on the [staking dashboard](https://staking.polygon.technology/account), it requires a *minimum of 12 block confirmations*. ## Set commission rate You can set up and change your commission as a validator. A validator is entitled to charge any commission rate. The minimum commission would be 0% and the maximum commission would be 100% of the rewards earned. You set up the commission rate as part of your initial [validator staking process](#initial-staking). ## Changing your commission rate You are allowed to freely adjust the commission rate as and when necessary. As a validator, it is one of your responsibilities to inform the community on commission changes. See [Validator Responsibilities](/pos/get-started/becoming-a-validator/#validator-responsibilities). Follow the steps below to change your commission rate: 1. With your owner address, login to the [staking dashboard](https://staking.polygon.technology/). 2. On your profile, select **Edit Profile**. 3. In the **Commission** field, enter your new commission rate. Once you have confirmed and signed the transaction your commission rate will be set. Note that once the commission is updated, there is a cool down period of *80 checkpoints*. ## Claim validator rewards Once you are set up and staked as a validator, you will earn rewards for performing validator duties. When you perform validator duties dutifully, you get rewarded. To claim rewards you can go to your [validator dashboard](https://staking.polygon.technology/account). You will see two buttons on your profile: * **Withdraw Reward** * **Restake Reward** ### Withdraw Reward As a validator, you earn rewards as long as you are performing your validator duties correctly. Selecting **Withdraw Reward** will get your rewards back to your wallet. The dashboard will update after *12 block confirmations*. ### Restake Reward Restaking your rewards is an easy way to increase your stake as a validator. Selecting **Restake Reward** will restake your reward and increase your stake. The dashboard will update after *12 block confirmations*. ## Common operations You can use the following commands to check if your validator node is set up correctly. ### Check validator account Run the following command *on your validator node* to check if the account is set up correctly: ```sh theme={null} heimdalld show-account ``` The output should appear in the following format: ```json theme={null} { "address": "0x6c468CF8c9879006E22EC4029696E005C2319C9D", "pub_key": "0x04b12d8b2f6e3d45a7ace12c4b2158f79b95e4c28ebe5ad54c439be9431d7fc9dc1164210bf6a5c3b8523528b931e772c86a307e8cff4b725e6b4a77d21417bf19" } ``` This will display your address and public key for your validator node. Note that *this address must match with your signer address on Ethereum*. ### Show private key Run the following command *on your validator node* to check if the private key configured correctly: ```sh theme={null} heimdalld show-privatekey ``` The output should appear in the following format: ```json theme={null} { "priv_key": "0x********************************************************" } ``` ### Check the balance To check the balance of your address, run the following command: ```sh theme={null} heimdalld query auth account SIGNER_ADDRESS [flags] ``` where, * `SIGNER_ADDRESS`: Your signer address. The following output should appear: ```bash theme={null} '@type': /cosmos.auth.v1beta1.BaseAccount account_number: "0" address: 0x6c468cf8c9879006e22ec4029696e005c2319c9d pub_key: '@type': /cosmos.crypto.secp256k1.PubKey key: ... sequence: "0" ``` # Top up Heimdall fee Source: https://docs.polygon.technology/pos/how-to/operate-validator-node/topup-heimdall-fee Steps to top up the Heimdall fee for a Polygon Chain validator using the Staking UI or Etherscan. Top up your Heimdall fee using the [Polygon Staking UI](https://staking.polygon.technology/account) (recommended) or manually via Etherscan. 1. Go to [https://staking.polygon.technology/](https://staking.polygon.technology/) and login using your owner address. 2. Go to My Account Section and click on Add Heimdall Fees. Figure: Add Heimdall Fees 3. Enter the amount you want to add and click on continue. Figure: Click Continue 4. Click on Delegate. Figure: Delegate 5. Approve the transaction on your wallet and you will see a message when your heimdall fee is added. Please note that it takes some time to reflect the fee in the account. Figure: Added Alternatively, you can also do it manually by following the steps below. This requires basic Etherscan knowledge and key details like the validator signer address. 1. Head over to [Etherscan.io](https://etherscan.io) 2. Goto `POL Ecosystem Token` Contract. Mainnet - [0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6](https://etherscan.io/address/0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6#code), Amoy - [0x44499312f493F62f2DFd3C6435Ca3603EbFCeeBa](https://sepolia.etherscan.io/address/0x44499312f493F62f2DFd3C6435Ca3603EbFCeeBa#code) 3. Under the **Code** menu, select **Write Contract** and connect your web3 wallet using **Connect to Web3** button. Figure: Connect wallet etherscan 4. Goto `approve` function, enter the spender as the address you want to use to topup your heimdall fee, and the amount. Figure: approve 5. Select **Write** to sign the transaction. 6. Now enter the `StakeManagerProxy` contract address in the search box: Mainnet - [0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908](https://etherscan.io/address/0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908), Amoy - [0x4AE8f648B1Ec892B6cc68C89cc088583964d08bE](https://sepolia.etherscan.io/address/0x4AE8f648B1Ec892B6cc68C89cc088583964d08bE#code) 7. Under the **Code** menu, select the **Write as Proxy** tab. Connect your Web3 wallet using the **Connect to Web3** button. Figure: Connect wallet etherscan 8. Scroll down to the `topUpForFee` method (#26 in the list) and select it. You will then see a display similar to the screenshot below. Figure: Top up Heimdall fee 9. Fill in the details: * `user`: Validator's Signer Address * `heimdallFee`: Top-up fee (**minimum 1 POL**) 10. After filling in the details, select **Write** to sign the transaction. Your Heimdall fee will be updated soon after the transaction completes. # Validator performance requirements Source: https://docs.polygon.technology/pos/how-to/operate-validator-node/validator-performance Reference for the validator performance parameters, benchmarks, grace periods, and forced unstaking process defined in PIP-4. Reference for validator performance parameters agreed to in [PIP-4](https://snapshot.org/#/polygonvalidators.eth/proposal/0x1e9d79b6406870ebd0b66ac256b59012f97f3f4d652052681c6fb1077e251804). These parameters and health statuses provide objective performance benchmarks for validators. ### Parameters A validator’s performance is measured based on the checkpoints it signed over a fixed monitoring period. Performance is measured on a rolling basis at each new checkpoint to provide an objective figure. This figure is then measured against a benchmark of the total network performance in the monitoring period, as detailed below. * Monitoring Period (“MP") = previous 700 checkpoints, updated every new checkpoint. * Take % of checkpoints signed by each validator in the MP and find the median. * Multiply the median average by an agreed multiple = Performance Benchmark (“PB”). * At each checkpoint, calculate the % of checkpoints signed in the MP by single validators and measure against the PB. ### Performance benchmark Figure: performance benchmark To facilitate the transition, there will be a slightly lower benchmark around the first two months while validators become accustomed to the parameters. * PB1 → 95% of the median average of the last 700 checkpoints signed by the validator set (first 2,800 checkpoints) * PB2 → 98% of the median average of last checkpoints signed by validator set (continues thereafter) ### Deficient validator process * If validator performance is below PB in the MP → Grace Period 1 (“GP1”). * If validator is in GP and still below PB after 700 checkpoints → Notice of Deficiency (“NOD”), validator enters into Grace Period 2 (“GP2”). * If validator is in GP2 and still below PB after 700 checkpoints → Final Notice (“FN”), the validator will be unstaked per PIP-4. Each GP is an additional 700 checkpoints, allowing a validator to bring their performance back above the PB. If the deficiency is corrected within the GP, there will be no further action. Failure to match, or out perform the performance benchmark, at the end of GP2 will result in a public NOD that will be displayed on the staking dashboard. The validator will have a 700 checkpoint period to correct the deficiency in GP2. If the deficiency is fixed within the NOD period, then no further action will occur. However, the NOD would remain public. Failure to improve after GP2 would result in the issuance of an FN of the community's intent to implement a forced exit procedure by offboarding the validator from the network by unbonding their stake. You can quickly check a validator's health status, whether it is *Healthy*, on *Grace Period*, or \*\* on the **All Validators** table or on each validator page.
Figure: Health status
### Public notices The **Public Notices** page shows the recent notices and messages sent to the community of validators. Figure: Topup Heimdall fee This is what a notice message should look like: Figure: Notice ### Forced unstaking The unstaking of the deficient validator would be done as follows: Call the `ForceUnstake` function in Polygon Commitchain Contract: `0xFa7D2a996aC6350f4b56C043112Da0366a59b74c` # Prerequisites Source: https://docs.polygon.technology/pos/how-to/prerequisites System requirements, port configuration, and setup checklist for running a Polygon Chain full node or validator node. ## Node system requirements Please note that all system requirements listed below are presented in a *Minimum/Recommended* format. ### Mainnet specs | Node type | RAM | CPU | Storage | Network bandwidth | | --------------------- | ----------- | -------------- | ----------------------------------------------------------------------------- | ----------------- | | Full Node/Sentry Node | 32 GB/64 GB | 8 core/16 core | 4 TB/6 TB | 1 Gbit/s | | Validator Node | 32 GB/64 GB | 8 core/16 core | 4 TB/6 TB | 1 Gbit/s | | Archive Node (Erigon) | 64 GB | 16 core | 16 TB(`io1` or above with at least 20k+ iops and RAID-0 based disk structure) | 1 Gbit/s | ### Testnet (Amoy) specs | Node type | RAM | CPU | Storage | Network bandwidth | | --------------------- | ---------- | -------------- | ---------------------------------------------------------------------------------- | ----------------- | | Full Node/Sentry Node | 8 GB/16 GB | 8 core/16 core | 1 TB/2 TB | 1 Gbit/s | | Validator Node | 8 GB/16 GB | 8 core/16 core | 1 TB/2 TB | 1 Gbit/s | | Archive Node (Erigon) | 16 GB | 16 core | 1 TB/2 TB (`io1` or above with at least 20k+ iops and RAID-0 based disk structure) | 1 Gbit/s | ## Downloading the snapshot It is recommended that you keep your snapshots handy before setting up the node. Link to the snapshot documentation [here](https://docs.polygon.technology/pos/how-to/snapshots/). ## Open necessary ports ### Sentry/full nodes | Port | Description | | :---------------------: | --------------------------------------------------------------------------------------------------------------------------------------------- | | `26656` | Heimdall service connects your node to another node’s Heimdall service using this port. | | `30303` | Bor service connects your node to another node’s Bor service using this port. | | `22` | For the validator to be able to SSH from wherever they are. | | `26660` | Prometheus port for Tendermint/Heimdall. Not required to be opened to the public. Only allow for the monitoring systems (Prometheus/Datadog). | | `7071` | Metric port for Bor. Only needs to be opened for the Monitoring system. | | `8545`, `8546`,  `1317` | Can be opened for Bor HTTP RPC, Bor WS RPC, and Heimdall API respectively; but only if really necessary. | ### Validator nodes | Port | Description | | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `22` | Opening this to the public is not a good idea as the default SSH port 22 is prone to attacks. It is better to secure it by allowing it only in a closed network (VPN). | | `30303` | To be opened to only Sentry to which the validator is connected for Bor P2P discovery. | | `26656` | To be opened to only Sentry to which the validator is connected for Heimdall/Tendermint P2P discovery. | | `26660` | Prometheus port for Tendermint/Heimdall. Not required to be opened to the public. Only allow for the monitoring systems (Prometheus/Datadog). | | `7071` | Metric port for Bor. Only needs to be opened for the monitoring system. | ## Install RabbitMQ This step is only relevant for validator nodes. Before setting up your validator node, it’s advisable to install the RabbitMQ service. You can use the following commands to set up RabbitMQ (if it’s not already installed): ```bash theme={null} sudo apt-get update sudo apt install build-essential sudo apt install erlang wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.10.8/rabbitmq-server_3.10.8-1_all.deb sudo dpkg -i rabbitmq-server_3.10.8-1_all.deb ``` ## Connect to Ethereum RPC endpoint This step is only relevant for validator nodes. Validator nodes need to connect to an Ethereum RPC endpoint. You may use your own Ethereum node, or utilize [external infrastructure providers](https://www.alchemy.com/chain-connect/chain/ethereum). ## Mandatory checklist for validators Please follow the below checklist in order to set up your validator node using binaries, Ansible, or packages. | Checklist | Binaries | Ansible | Packages | | ------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Machines required | 2 Machines - *sentry* & *validator* | 3 Machines - *local machine*, *sentry* and *validator* | 2 Machines - *sentry* & *validator* | | Install Go packages | Yes | No | No | | Install Python | No | Yes (only on the local machine where the Ansible Playbook runs) | No | | Install Ansible | No | Yes (only on one machine) | No | | Install Bash | No | No | Yes | | Run Build Essential | Yes | No | No | | Node setup | [Using binaries](https://docs.polygon.technology/pos/how-to/validator/validator-binaries/) | [Using Ansible](https://docs.polygon.technology/pos/how-to/validator/validator-ansible/) | [Using packages](https://docs.polygon.technology/pos/how-to/validator/validator-packages/) | ## Related * **[Polygon Chain overview](/pos/overview)**: what the node is participating in: dual-layer architecture, milestones, checkpoints. * **[Become a validator](/pos/get-started/becoming-a-validator)**: staking, delegation, and the full path from node to active validator. * **[RPC endpoints](/pos/reference/rpc-endpoints)**: if you only need a connection to the network without running your own node. # Sync node using snapshots Source: https://docs.polygon.technology/pos/how-to/snapshots Steps to download and apply community snapshots to sync a Polygon Chain sentry, validator, or full node without syncing from scratch. Use snapshots to sync a new node in hours rather than days. This applies to sentry, validator, and full nodes for both Heimdall and Bor. ## Community snapshots Polygon Chain has transitioned to a community-driven model for snapshots. Active community members now contribute to provide snapshots. Some of these members include: | Name | Available snapshots | Note | | ---------------------------------------------------------------------- | --------------------- | -------------------------------------------- | | Stakecraft | Mainnet, Amoy, Erigon | Support for Erigon archive snapshot | | [PublicNode (by Allnodes)\*](https://publicnode.com/snapshots#polygon) | Mainnet, Amoy | Support for PBSS + PebbleDB enabled snapshot | | Stakepool | Mainnet, Amoy | - | | Vaultstaking | Mainnet | - | | Girnaar Nodes | Amoy | - | > *\*The PBSS + PebbleDB snapshot provided by PublicNode is currently in the beta phase.* Visit [All4nodes.io](https://all4nodes.io/Polygon) for a comprehensive list of community snapshots. ## Downloading and using client snapshots To begin, ensure that your node environment meets the **prerequisites** outlined [here](/pos/how-to/full-node/full-node-binaries/). The majority of snapshot providers have also outlined the steps that need to be followed to download and use their respective client snapshots. Navigate to [All4nodes](https://all4nodes.io/Polygon) to view the snapshot source. In case the steps are unavailable or the procedure is unclear, the following tips will come in handy: * You can use the `wget` command to download and extract the `.tar` snapshot files. For example: ```bash theme={null} wget -O - snapshot_url_here | tar -xvf -C /target/directory ``` * Configure your client's `datadir` setting to match the directory where you downloaded and extracted the snapshot data. This ensures the `systemd` services can correctly register the snapshot data when the client is spun up. * To maintain your client's default configuration settings, consider using symbolic links (symlinks). ## Example Let's say you have mounted your block device at `~/snapshots` and have downloaded and extracted the chain data into the `heimdall_extract` directory for Heimdall, and into the `bor_extract` directory for Bor. Use the following commands to register the extracted data for Heimdall and Bor `systemd` services: ```bash theme={null} # remove any existing datadirs for Heimdall and Bor rm -rf /var/lib/heimdall/data rm -rf /var/lib/bor/data/bor/chaindata # rename and setup symlinks to match default client datadir configs mv ~/snapshots/heimdall_extract ~/snapshots/data mv ~/snapshots/bor_extract ~/snapshots/chaindata sudo ln -s ~/snapshots/data /var/lib/heimdall sudo ln -s ~/snapshots/chaindata /var/lib/bor/data/bor # bring up clients with all snapshot data properly registered sudo service heimdalld start # wait for Heimdall to fully sync then start Bor sudo service bor start ``` Ensure that the Bor and Heimdall user files have appropriate permissions to access the `datadir`. To set correct permissions for Bor, execute `sudo chown -R bor:nogroup /var/lib/heimdall/data`. Similarly, for Heimdall, run `sudo chown -R heimdall:nogroup /var/lib/bor/data/bor` ## Recommended disk size guidance ### Polygon Amoy testnet | Metric | Calculation Breakdown | Value | | --------------------------------- | ------------------------------------ | ------- | | approx. compressed total | 250 GB (Bor) + 35 GB (Heimdall) | 285 GB | | approx. data growth daily | 10 GB (Bor) + 0.5 GB (Heimdall) | 10.5 GB | | approx. total extracted size | 350 GB (Bor) + 50 GB (Heimdall) | 400 GB | | suggested disk size (2.5x buffer) | 400 GB \* 2.5 (natural chain growth) | 1 TB | ### Polygon mainnet | Metric | Calculation Breakdown | Value | | --------------------------------- | --------------------------------- | ------- | | approx. compressed total | 3000 GB (Bor) + 500 GB (Heimdall) | 3500 GB | | approx. data growth daily | 100 GB (Bor) + 5 GB (Heimdall) | 105 GB | | approx. total extracted size | 4 TB (Bor) + 500 GB (Heimdall) | 4.5 TB | | suggested disk size (2.5x buffer) | 4 TB \* 2 (natural chain growth) | 8 TB | ### Polygon Amoy Erigon archive | Metric | Calculation Breakdown | Value | | --------------------------------- | ------------------------------------ | ------ | | approx. compressed total | 210 GB (Erigon) + 35 GB (Heimdall) | 245 GB | | approx. data growth daily | 4.5 GB (Erigon) + 0.5 GB (Heimdall) | 5 GB | | approx. total extracted size | 875 GB (Erigon) + 50 GB (Heimdall) | 925 GB | | suggested disk size (2.5x buffer) | 925 GB \* 2.5 (natural chain growth) | 2.5 TB | ## Recommended disk type and IOPS guidance * Disk IOPS will affect the speed of downloading/extracting snapshots, getting in sync, and performing LevelDB compaction. * To minimize disk latency, direct-attached storage is ideal. * In AWS, when using gp3 disk types, we recommend provisioning IOPS of 16,000 and throughput of 1,000. This minimizes costs while providing significant performance benefits. io2 EBS volumes with matching IOPS and throughput values offer similar performance. * For GCP, we recommend using performance (SSD) persistent disks (`pd-ssd`) or extreme persistent disks (`pd-extreme`) with similar IOPS and throughput values as mentioned above. # Known issues and errors Source: https://docs.polygon.technology/pos/how-to/troubleshoot/known-issues Diagnosis and resolution steps for common Bor and Heimdall errors encountered while running a Polygon Chain validator node. Reference for diagnosing and resolving common issues on Polygon Chain validator nodes, organized by service. ## Bor ### Bor is unable to connect to peers Bor stops importing new blocks, with logs displaying messages similar to the following: ```js theme={null} Aug 19 13:33:35 polygon-mainnet-validator-backup-4 bor[124475]: INFO [08-19|13:33:35.123] Looking for peers peercount=0 tried=0 static=7 Aug 19 13:33:36 polygon-mainnet-validator-backup-4 bor[124475]: INFO [08-19|13:33:36.916] Whitelisting milestone deferred err="chain out of sync" Aug 19 13:33:48 polygon-mainnet-validator-backup-4 bor[124475]: INFO [08-19|13:33:48.916] Whitelisting milestone deferred err="chain out of sync" ``` ##### Solution * Increase `maxpeer` count to 200 * Add the bootnodes under static and trusted nodes * If this doesn’t resolve the issue, try adding the peers manually using the IPC console ### Error: Bad block/Invalid Merkle A bad block or invalid Merkle root error occurs when the Heimdall and Bor layers are not in sync. Heimdall, as the consensus layer for Polygon POS chain, directs Bor to create blocks accordingly. A bad block error occurs when the Bor moves ahead to create a block which has not been directed by Heimdall. This causes an invalid hash being created, and hence results in an invalid Merkle root. ##### Solution 1 Restart the Bor service using the following command: ```bash theme={null} sudo service bor restart ``` Typically a restart of the Bor service should resolve the problem, and that's because restarting causes Bor to reconnect with Heimdall, start syncing, and create blocks correctly. If restarting the Bor service does not fix the problem, try the next option. ##### Solution 2 Make the following checks: * Check if your Heimdall and REST servers are running. The Heimdall service might have stopped, and thus causing the bad block issue on Bor. * Check the logs for your Heimdall first using the following command: ```bash theme={null} journalctl -u heimdalld -f ``` * Check if everything is working correctly. * Restart the services that are not running. This should cause Bor to automatically resolve the problem If restarting both the Bor and Heimdall services doesn't solve the problem, it could be that Bor is stuck on a particular block. ##### Solution 3 Check the bad block in logs for Bor. * Check Bor logs with this command: ```bash theme={null} journalctl -u bor -f ``` The bad block is typically displayed in the logs as shown in the below figure: Figure: Bad block * Note the bad block number. * Convert the block number to a hexadecimal number. Use this [tool](https://www.rapidtables.com/convert/number/decimal-to-hex.html) to convert the block number to a hexadecimal number. * Roll back the chain by a few hundred blocks, i.e., set Bor at the right block height using the `debug.setHead()` function. Use the following command: ```bash theme={null} bor attach ./.bor/data/bor.ipc > debug.setHead("0xE92570") ``` The `debug.setHead()` function allows Bor to set the tip at a particular block height, resyncing from a previous block. The command should return a `null` upon successful execution. Once this occurs, you can resume monitoring Bor to verify whether the chain progresses beyond the previously problematic block. If none of these solutions work for you, please contact the Polygon Support team immediately. ### Issue: Bor synchronization is slow If Bor synchronization is slow, it may be caused by one or more of the following factors: * The node is running on a fork - means at certain point the block production was done by forking on a different block and that has impacted the further block production. * The machine is not working at optimum levels and could be with insufficient resources. This can be addressed by checking the following: * IOPS * IOPS stands for Input/Output state of cycle. * The rate of reading is usually higher than write speed. * 6000 is the recommended range for IOPS. * Processing power * Processor has to be 8 or 16 core. * RAM: 32 GB is the minimum; 64 GB is recommended. * Block import should be more than 2 block for every second. * Node sync rate should be at 15-20 blocks every 8 secs. ##### Solution Since the issue is likely due to insufficient hardware resources, consider upgrading to double the current specs. ### Validator Bor is stuck on a block for a long time This implies that the Bor service on your sentry node is also stuck because your validator gets information from your sentry. ##### Solution * Please check the Bor logs on your sentry and see if everything is normal and functional. * Restart the Bor service on your sentry node, then simultaneously restart the Bor service on your validator. ### Retrying again in 5 seconds to fetch data from Heimdall path=bor/span/1 These logs in Bor mean that it cannot connect to Heimdall. Heimdall appears to be out of sync, and thus it lacks the data needed by Bor. ##### Solution The recommended approach is to clear the historical data from both Heimdall and Bor, then [resync using a snapshot](/pos/how-to/snapshots/). Verify the following: 1. Are Heimdall logs normal, or do they show any errors? 2. Confirm Heimdall is fully synced by running: curl localhost:26657/status 3. Check whether Heimdall is connected to other peers. ```bash theme={null} curl localhost:26657/net_info? | jq .result.n_peers ``` If there are no peers, verify that the *seeds or persistent peers are correctly configured on Heimdall*, and ensure that *port 26656 is open*. ### etherbase missing: etherbase must be explicitly specified To fix this issue, the signer address that is used to mine must be added in the `miner.etherbase` section in the `config.toml` file. ### Error: Failed to unlock account (0x…) No key for given address or file This error occurs because the path to the `password.txt` file is incorrect. Follow the steps below to resolve this issue. ##### Solution 1. Kill the Bor process. 2. Copy the Bor keystore file to: `/var/lib/bor/keystore/` 3. And the `password.txt` file to: `/var/lib/bor/password.txt` 4. Ensure that the user 'Bor' has permission to access the password.txt file. You can do this by running the following command: `sudo chown -R bor:nogroup /var/lib/bor/` ### Steps to prune the node Follow the steps below to prune your node: 1. Check your Bor data size before pruning using the following command: ```bash theme={null} du -sh /usr/bin/bor ``` 2. Stop Bor. ```bash theme={null} sudo service bor stop ``` 3. Start `tmux` to ensure that even if your SSH connection is reset, the process is running on the remote machine using `tmux`. 4. Start pruning. ```bash theme={null} sudo bor snapshot prune-state --datadir /usr/bin/bor ``` The default --datadir is `/usr/bin/bor`. 5. Once the pruning is completed, you will see success logs and details. Then start Bor again using: ```bash theme={null} sudo service bor start ``` 6. Check your Bor data size after pruning using: ```bash theme={null} du -sh /usr/bin/bor ``` ## Heimdall ### Log: Error dialing seed/Looking for peers or stopping peer for error/Dialing failed This log is expected when you first start Heimdall, as it takes some time to find and connect to peers. If the issue persists, check the following: * Verify that your Heimdall node is configured with the latest seeds as listed in the [node setup documentation](/pos/how-to/full-node/full-node-binaries/#configure-heimdall-seeds-mainnet). If the error persists after updating to the latest seeds or confirming that you are using the correct ones, follow these steps: 1. Increase `max_num_inbound_peers` and `max_num_outbound_peers` in `/var/lib/heimdall/config/config.toml`: ```toml theme={null} max_num_inbound_peers = 300 max_num_outbound_peers = 100 ``` 2. Start `heimdalld` service using the following command: ```bash theme={null} sudo service heimdalld start ``` ### Issue: Validator Heimdall is unable to connect to peers This typically means that your sentry Heimdall is running into issues. ##### Solution * Check your sentry Heimdall to ensure that the service is running properly. * If the service is stopped, restarting it on your sentry node should resolve the issue. * Likewise, after addressing any issues with your sentry, restarting your Heimdall service should also help resolve the problem. # Reporting issues Source: https://docs.polygon.technology/pos/how-to/troubleshoot/reporting-issues Where to report bugs, security vulnerabilities, and node issues for Polygon Chain, including bug bounty programs and support channels. ## Where to report a bug Any discovered bugs or vulnerabilities related to our Bug Bounty Program should be reported as follows: * For websites and applications: [https://hackerone.com/polygon-technology](https://hackerone.com/polygon-technology) * For smart contracts: [https://immunefi.com/bounty/polygon](https://immunefi.com/bounty/polygon) * For security inquiries, please contact us at [security@polygon.technology](mailto:security@polygon.technology). (Please disclose vulnerabilities through the bug bounty program) Performing an attack and not providing submission of your proof will result in disqualification of your attempt. Make sure you add all relevant details such as your email address and Discord ID. Providing ample details creates a rapport of communication, and helps the Polygon team evaluate your submission appropriately. ## What happens after submitting a report Once an issue is reported, the Polygon team reviews it, comments, and updates on the status of the issue. After evaluation, the Polygon team reports the outcome of the submission. The severity of the issue also gets tagged as per the evaluation. ## Contact us for all other questions ### Via E-mail * For node operators: [node-support@polygon.technology](mailto:node-support@polygon.technology) * For validators: [validator-support@polygon.technology](mailto:validator-support@polygon.technology) ### Via support portal * Visit the support portal at [support.polygon.technology](https://support.polygon.technology/). * Sign in using your email address and select **Submit a Ticket** located in the top right corner. Polygon support portal * Tha above page appears. To ensure an accurate response, please include the following details when submitting your ticket: * The versions of Bor and Heimdall you are using. * At least one hour of logs related to the affected services. * The `config.toml` files for the affected services. ### Via Discord * Please visit the **#pos-full-node-queries** channel on our [Discord](https://discord.com/invite/0xPolygonCommunity) server and feel free to post your questions there. We will address your queries directly in that channel. # Technical FAQs Source: https://docs.polygon.technology/pos/how-to/troubleshoot/technical-faqs Reference answers to common technical questions about running Polygon Chain validator nodes, including key management, default directories, and troubleshooting commands. ### 1. Are the private keys same for Heimdall and Bor keystore? Yes, the private key used for generating Validator keys and Bor Keystore is the same. The private key used in this instance is your Wallet's ETH address where your Polygon testnet tokens are stored. ### 2. List of Common Commands Refer to the [list of common commands](/pos/reference/commands/) that might come in handy while troubleshooting. ### 3. Default Directories * Heimdall genesis file: `/var/lib/heimdall/config/genesis.json` * Heimdall app.toml file: `/var/lib/heimdall/config/app.toml` * Heimdall config.toml file: `/var/lib/heimdall/config/config.toml` * Heimdall client.toml file: `/var/lib/heimdall/config/client.toml` * Heimdall data directory: `/var/lib/heimdall/data/` * Bor config.toml file: `/var/lib/bor/config.toml` * Bor data directory: `/var/lib/bor/data/bor/chaindata` ### 4. From where do I create the API key? You can access this link: [https://infura.io/register](https://infura.io/register) . Make sure that once you have setup your account and project, you copy the API key for Sepolia and not mainnet. Mainnet is selected by default. ### 5. How do I delete remnants of Heimdall and Bor? Run the following commands to delete the remnants of Heimdall and Bor from your machines. For the Linux package, run: `$ sudo dpkg -i bor` And delete the Bor directory using: `$ sudo rm -rf /var/lib/bor` For binaries, run: `$ sudo rm -rf /var/lib/bor` And then run: `$ sudo rm /var/lib/heimdall` ### 6. How many validators can be active concurrently? Under the current limit, a maximum of 105 validators can be active at any given time. It's important to note that active validators are primarily those with high uptime, while participants with significant downtime may be removed. ### 7. How much should I stake? A minimum stake of 10,000 POL tokens is required (as per PIP-4). We recommend setting a Heimdall fee of 10 POL. ### 8. I'm not clear on which Private Key should I add when I generate validator key. The private key to be used is your wallet's ETH address where your Polygon testnet tokens are stored. You can complete the setup with one public-private key pair tied to the address submitted on the form. ### 9. Is there a way to know if Heimdall is synced? You can run the following command to check it: ```bash theme={null} $ curl [http://localhost:26657/status](http://localhost:26657/status) ``` Check the value of the `catching_up` flag. If it is `false` then the node is all synced up. ### 10. Which file do I add the API key in? Once you have created the API key, you need to add it to the `app.toml` file. ### 11. How to check if the correct signer address is used for validator setup? To check the signer address, run the following command *on the validator node*: ```bash theme={null} heimdalld show-account ``` ### 12. `Error: Failed to unlock account (0x...) No key for given address or file` This error occurs because the path for the `password.txt` file is incorrect. You can follow the below steps to rectify this: 1. Copy the Bor keystore file to `/var/lib/bor/keystore` 2. Copy `password.txt` to `/var/lib/bor/` 3. Make sure you have added correct address in `/var/lib/bor/config.toml`. 4. Ensure that the `priv_validator_key.json` and `UTC-