Payments
omnigate can take payments for prepaid credit by itself: the holder of a prepaid key asks for a payment, pays it with a crypto asset of their choice, and the key is credited when the payment has settled. An agent whose credit is spent can do this without a person.
This is opt-in. Without a "payments" section in the config there are
no payment endpoints (they answer 404), nothing is asked of any provider,
and nothing on this page applies. omnigate stays a model gateway.
There is one payment provider today: NEAR Intents, through its 1Click
API. Payments are built as a provider interface with that one adapter, the
way media providers are; the config names the provider. Other ways of
taking money still work as before, from outside: anything that can call
POST /admin/keys/{name}/topup
can credit a key.
- What the gateway does and does not do
- The flow
- Switching it on
- The API
- States of a payment
- What is credited, and when
- What is checked, and what is trusted
- Fees, limits and refunds
- For operators
- What has been tested
- Trying it with real funds
What the gateway does and does not do
- It holds no keys, signs nothing and moves no funds. It asks the provider for a quote and, later, for the status of that quote. Both are reads.
- Funds never pass through the gateway. The payer sends them to a deposit address the provider gives; the provider swaps them and delivers the settle asset straight to the operator's own account.
- It credits a key: a number in the key store, under the rule in Prepaid credit. Credit cannot be withdrawn or refunded.
- What the operator does with what arrives in their account is outside the gateway.
The flow
payer (a person or an agent) omnigate NEAR Intents (1Click) operator's account
| POST /v1/payments | | |
| {amount_usd, asset, | quote: "exactly N USDC to | |
| refund_to} --------------> | the operator; how much of | |
| | this asset, sent where?" --> | |
| | <-- deposit address, amount, | |
| <-- deposit address, amount, | deadline (signed) | |
| deadline | | |
| sends the funds ------------------------------------------> | swaps, delivers -------> |
| | status? (every few seconds)->| |
| | <-- SUCCESS, amount delivered | |
| | credits the key, once | |
| GET /v1/payments/{id} -----> | | |
| <-- settled, credited_usd | | |
Whatever the payer pays with (the provider lists a few hundred assets on a few dozen chains), the operator receives one asset: the settle asset, native USDC on NEAR unless configured otherwise.
Switching it on
"payments": {
"provider": "near_intents",
"recipient": "your-account.near",
"min_usd": 1,
"max_usd": 1000
}
config/omnigate.payments.json is a
complete example (a text route, a burn multiplier, a bonus rate, payments).
| Field | Default | Meaning |
|---|---|---|
provider |
required | "near_intents". The only one there is. |
recipient |
required | The operator's account at the provider: for near_intents, the NEAR account that receives the settle asset. Check it twice: it is where the money goes. It must be able to receive the settle asset (for a NEAR token: registered with the token's contract, which holding the token once takes care of). |
min_usd, max_usd |
1, 10000 |
Bounds on one payment. |
quote_ttl_s |
1800 |
How long a payer has to send the deposit. 60 to 86,400. |
bonus_eligible |
true |
Whether credit bought this way earns the weekly bonus. |
poll_interval_s |
15 |
How often an open payment's status is asked for. After twenty polls they are spaced further apart. |
settle_asset |
native USDC on NEAR | The asset delivered to the operator, as a 1Click asset id. It should be a dollar stablecoin: what arrives is credited one to one. |
settle_decimals |
6 |
That asset's decimals. |
slippage_bps |
100 |
Tolerance on the amount the payer is asked to send (1%). What the swap does not need is refunded to the payer by the provider. |
jwt_env |
none | Name of an environment variable (of the control plane) holding a 1Click partner token. Without one, 1Click adds 0.25% to every quote. |
base_url, public_key |
1Click's | Where the provider is and the key it signs quotes with. Only ever changed to point at a test double. |
An unknown field, or a provider that does not exist, is refused when the
config is loaded. The control plane needs outbound HTTPS to
1click.chaindefuser.com. Payments are kept in OMNIGATE_PAYMENTS
(default data/payments.json; /data/payments.json in the image).
Only prepaid managed keys can be paid for: keys created through the
admin API with a total budget ("budget_usd": 0 is the usual start). A key
from the config file, a key without a budget and a key with a monthly or
daily budget are answered 409 not_prepaid.
The API
On the gateway's port, with a gateway key, like every other endpoint. A key whose credit is spent can still use all of these; that is what they are for.
| Method and path | Purpose |
|---|---|
GET /v1/payments/assets |
What can be paid with. ?chain= and ?symbol= narrow the list. |
POST /v1/payments/quote |
What a payment would take, without starting one. |
POST /v1/payments |
Start a payment: get a deposit address and the amount to send. |
GET /v1/payments/{id} |
The payment and its state. |
GET /v1/payments |
The key's payments, newest first (up to 50). |
POST /v1/payments/{id}/tx |
Optional: tell the provider which transaction carries the deposit. |
Assets
curl -s "http://127.0.0.1:8080/v1/payments/assets?chain=near" -H "Authorization: Bearer $KEY"
{ "assets": [ { "asset": "nep141:wrap.near", "symbol": "wNEAR", "chain": "near", "decimals": 24, "price_usd": 3.7 } ] }
The list is the provider's, fetched at most every five minutes. asset is
what the other calls take.
A quote, without starting anything
curl -s http://127.0.0.1:8080/v1/payments/quote -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"amount_usd": 5, "asset": "nep141:wrap.near", "refund_to": "payer.near"}'
{ "amount_usd": 5.0, "asset": "nep141:wrap.near", "symbol": "wNEAR", "chain": "near",
"amount": "1.3638", "amount_units": "1363800000000000000000000", "min_amount_units": "1350200000000000000000000",
"deposit_usd": "5.06", "time_estimate_s": 20, "provider": "near_intents" }
Nothing is created at the provider and there is no deposit address.
Starting a payment
curl -s http://127.0.0.1:8080/v1/payments -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"amount_usd": 5, "asset": "nep141:wrap.near", "refund_to": "payer.near"}'
| Field | Meaning |
|---|---|
amount_usd |
How much credit to buy, in US dollars, between min_usd and max_usd. Rounded to whole cents. |
asset |
The asset to pay with, from the assets list. |
refund_to |
An address on the chain of that asset that gets the funds back if the payment fails, and anything the swap did not need. Use the address the payment is sent from. |
The answer is 201:
{
"id": "pay_3f9c1e0a5b7d2c4e6f8a0b1c",
"status": "awaiting_deposit",
"amount_usd": 5.0,
"credited_usd": null,
"provider": "near_intents",
"deposit": {
"address": "8f3a…c21e",
"memo": null,
"asset": "nep141:wrap.near", "symbol": "wNEAR", "chain": "near",
"amount": "1.3638", "amount_units": "1363800000000000000000000",
"min_amount_units": "1350200000000000000000000", "amount_usd": "5.06"
},
"refund_to": "payer.near",
"deadline": "2026-11-11T21:00:00.000Z",
"time_estimate_s": 20,
"detail": null, "provider_status": null, "tx": [],
"created_at": "2026-11-11T20:30:00Z", "updated_at": "2026-11-11T20:30:00Z"
}
Send exactly deposit.amount of the asset to deposit.address before
deadline, from a wallet of your own. When deposit.memo is not null the
chain needs it with the transfer (Stellar, for one): a deposit without it is
not recognized. Sending less than min_amount_units is refunded by the
provider when the deadline passes; sending more is swapped and the excess
refunded.
Then ask for the payment until its status is an end state. An agent's
loop, with the three calls, is
examples/agent_pays.py.
Errors
| Status | code |
When |
|---|---|---|
| 400 | invalid_request |
amount_usd outside the bounds or not a number; asset or refund_to not well-formed. |
| 400 | payment_quote_refused |
The provider refused the request. Its own words are in the message: an address that is not one on that chain, an amount below what a bridge takes, a chain's minimum, a pair it does not quote. |
| 404 | not_found |
Payments are not configured on this gateway. |
| 404 | payment_not_found |
No such payment, or it is another key's. |
| 409 | not_prepaid |
The key is not a prepaid managed key. |
| 409 | payment_closed |
POST …/tx on a payment that has ended. |
| 429 | too_many_open_payments |
The key has five payments waiting for a deposit. |
| 429 | rate_limited |
More than six quotes or payments a minute for one key (the key's own rpm also applies). |
| 502 | payment_provider_error |
The provider answered something that was not used: an answer that could not be read, or a quote that is not signed by its key or does not repeat what was asked. No deposit address is shown in that case. |
| 503 | payment_provider_unavailable |
The provider could not be reached, or is rate limiting. |
States of a payment
awaiting_deposit → processing → settled credited_usd is set: the key was credited
→ refunded the provider gave the funds back to refund_to
→ failed the provider could not complete the swap
awaiting_deposit → expired nothing arrived by the deadline (plus 15 minutes)
provider_statusis the provider's own word (PENDING_DEPOSIT,KNOWN_DEPOSIT_TX,PROCESSING,INCOMPLETE_DEPOSIT,SUCCESS,REFUNDED,FAILED);detailsays in a few words what a payer should know (a deposit that fell short, why something was refunded).txlists transaction hashes the provider reported, to look the payment up on a chain explorer.- A payment that is
settledwithcredited_usdnull reached the operator but could not be credited (the key was deleted, or is no longer prepaid);detailsays so. Nothing is credited anywhere else. That one is between the payer and the operator.
What is credited, and when
- What arrived: the amount of the settle asset the provider reports as delivered, as US dollars one to one. For a $5 payment that is $5 (the quote is for an exact output); if less arrives, less is credited. An amount more than 5% above what was asked for is not believed beyond that.
- Once: the credit goes through the same idempotent top-up as every
other, under the reference
near_intents:<deposit address>(with:<memo>where there is one). The provider reporting success a hundred times, a restart in the middle, two polls racing: one credit. - As eligible credit from source
near_intentsunlesspayments.bonus_eligibleis false; see Two kinds of credit. - When: within one poll interval of the provider reporting success, and the data plane unblocks the key with its next config poll (a second or two by default).
What is checked, and what is trusted
Checked by the gateway:
- A quote is signed by the provider. 1Click signs each quote with an Ed25519 key; the gateway verifies it against that key (pinned in the source, taken from 1Click's own SDK) before the deposit address is shown to anyone. The signature covers the deposit address, the recipient, the amounts and the refund address.
- The quote repeats what was asked: the operator's recipient, the settle asset, the exact amount, the payer's refund address.
- A status answer is about our quote: it carries the quote it refers to, which must be the one stored (same signature, same deposit address, same recipient). An answer about another quote is not believed and the payment stays open.
- Nothing a client says is trusted: not an amount, not a status, not a transaction hash (which is only passed on to the provider as a hint).
Trusted, and you should know it:
- That the funds arrived.
SUCCESSand the delivered amount are the provider's word, received over TLS from the configured host. The status itself is not signed, and the gateway does not look at the NEAR chain to confirm the transfer. Someone who controls the provider's API, or can impersonate it to the control plane, can make the gateway credit keys: credit, not money, and never more than the amount of a quote that the provider itself signed plus 5%. Reconcile whatGET /admin/paymentslists against what your account received. - The provider's asset list and prices, for what a payer is shown.
One oddity of the real service: some assets are listed twice (Bitcoin as
nep141:btc.omft.near and as 1cs_v1:btc:native:coin), and a quote asked
for under the first id is signed as if it had been asked for under the
second. The gateway therefore accepts a signature that names the paying
asset by another id the provider's own list gives the same coin on the same
chain. Everything else in the signature must be exactly what was asked.
Fees, limits and refunds
All of these are the provider's; the gateway adds nothing of its own to a payment. (What a request then burns of the credit is the burn multiplier, which is the operator's.)
- Fees are inside the quote: the payer is asked to send a little more
than the dollar amount. Without a partner token 1Click adds 0.25% to
every quote; with one (
jwt_env), 0.20%, or 0.01% for stablecoin pairs. Network fees of the paying chain are the payer's. - Minimums: the provider refuses amounts below what a bridge takes
(about $6 for Bitcoin when this was written) and has per-chain minimums
($1,000 on BNB Chain, $100 from Tron, when this was written). The refusal
comes back as
400 payment_quote_refusedwith the provider's reason. - Refunds go from the provider to
refund_to, on the paying chain: when less than the required amount arrives by the deadline, when the swap fails, and for whatever a swap did not need. The gateway never refunds anything, and credit that was applied is not taken back. - A deposit after the deadline may not be recognized by the provider. Start a new payment instead.
For operators
GET /admin/payments?limit=<n>: every payment, newest first, with the key's name, the reference it is credited under and the recipient it was quoted for;"configured": falsewhen payments are off.- Audit log:
payment.create,payment.settle(outcomeok, oruncreditedwhen the key could not be credited),payment.refunded,payment.failed; all written by the gateway itself ("auth": "system"). The credit is also akey.topup-like line in the top-up history (GET /admin/topups) and acreditevent in the usage feed. - Metrics:
omnigate_payments{status}andomnigate_payments_credited_usd. - Storage:
OMNIGATE_PAYMENTS, rewritten atomically on every change. At most 5,000 payments are kept; ones that ended more than 90 days ago are dropped. Open payments survive a restart and are asked about again. Back the file up with the key store: a payment that is forgotten while open is never credited by itself (credit it by hand with a top-up under the same reference,near_intents:<deposit address>, which stays idempotent). - Changing
recipientaffects new payments; open ones keep the recipient they were quoted for. - The dashboard does not show payments or credit balances yet; the admin API does.
What has been tested
- Against the real service, without funds (
tools/near_intents_live.py, 11 November 2026): the asset list (229 assets on 39 chains) and dry quotes for NEAR, Ether, Bitcoin, SOL and USDC on Base and Arbitrum through a running gateway, each with its signature verified; the provider's refusals relayed. A dry quote creates nothing. - Against a mock (
tools/mock_intents.py, in the end-to-end suite and in the control plane's tests): everything after a deposit. A blocked key paying and being unblocked, settlement seen many times and credited once, refunds, failures, short deposits, quotes and statuses that are forged or about another quote, a memo chain, a restart in the middle of a payment, the agent example. - Not done: a real payment. No funds have been sent through this code. The status answers of the real service after a deposit (their exact shape, how long they take) have only been read in its documentation.
Trying it with real funds
One small payment, by hand. There is no test network for NEAR Intents: this uses real funds, so use a dollar or two.
-
Put your own NEAR account in
payments.recipientofconfig/omnigate.payments.json, and make sure it can receive USDC (hold a little of it once). -
Start the gateway with that config and a prepaid key:
python tools/near_intents_live.py --serveIt prints the recipient, a key with no credit and the admin token, and the commands below with the key filled in. It sends nothing itself.
-
See what $1 would take, without starting anything:
curl -s http://127.0.0.1:8080/v1/payments/quote -H "authorization: Bearer $KEY" -H "content-type: application/json" -d '{"amount_usd": 1, "asset": "nep141:wrap.near", "refund_to": "<your NEAR account>"}' -
Start the payment (this creates a deposit address at NEAR Intents):
curl -s http://127.0.0.1:8080/v1/payments -H "authorization: Bearer $KEY" -H "content-type: application/json" -d '{"amount_usd": 1, "asset": "nep141:wrap.near", "refund_to": "<your NEAR account>"}' -
From your own wallet, send exactly
deposit.amountof the asset todeposit.addressbeforedeadline. -
Watch it:
GET /v1/payments/<id>should go fromawaiting_deposittosettledwithcredited_usdof 1, andGET /admin/keysshould show the key with a budget of 1 and one dollar of eligible credit. Your account should hold one more USDC.
Paying with wrapped NEAR from a NEAR account keeps the fees lowest. If the
payment ends as refunded or expired, nothing was credited and the
provider returns what it received to refund_to.